git-obsolete-branch.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/python
  2. #
  3. # Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
  4. #
  5. # This Source Code Form is subject to the terms of the Mozilla Public
  6. # License, v. 2.0. If a copy of the MPL was not distributed with this
  7. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  8. #
  9. # This script lists obsolete (fully merged) branches. It is useful for periodic
  10. # maintenance of our GIT tree.
  11. #
  12. # It is good idea to use following command before running this script:
  13. #
  14. # git pull
  15. # git remote prune origin
  16. #
  17. # This script requires python 2.7 or 3.
  18. #
  19. # I have limited experience in Python. If things are done in a strange or
  20. # uncommon way, there are no obscure reasons to do it that way, just plain
  21. # lack of experience.
  22. #
  23. # tomek
  24. import string
  25. import subprocess
  26. import sys
  27. from optparse import OptionParser
  28. class Branch:
  29. MERGED = 1
  30. NOTMERGED = 2
  31. name = None
  32. status = NOTMERGED
  33. last_commit = None
  34. def branch_list_get(verbose):
  35. """ Generates a list of available remote branches and
  36. checks their status (merged/unmerged). A branch is merged
  37. if all changes on that branch are also on master. """
  38. # call git branch -r (list of remote branches)
  39. txt_list = subprocess.check_output(["git", "branch", "-r"])
  40. txt_list = txt_list.split(b"\n")
  41. # we will store list of suitable branches here
  42. out = []
  43. for branch in txt_list:
  44. # skip empty lines
  45. if len(branch) == 0:
  46. continue
  47. # skip branches that are aliases (something -> something_else)
  48. if branch.find(b"->") != -1:
  49. continue
  50. # don't complain about master
  51. if branch == b"origin/master":
  52. continue
  53. branch_info = Branch()
  54. # get branch name
  55. branch_info.name = branch.strip(b" ")
  56. branch_info.name = branch_info.name.decode("utf-8")
  57. # check if branch is merged or not
  58. if verbose:
  59. print("Checking branch %s" % branch_info.name)
  60. # get a diff with changes that are on that branch only
  61. # i.e. all unmerged code.
  62. cmd = ["git", "diff", "master..." + branch_info.name ]
  63. diff = subprocess.check_output(cmd)
  64. if len(diff) == 0:
  65. # No diff? Then all changes from that branch are on master as well.
  66. branch_info.status = Branch.MERGED
  67. # let's get the last contributor with extra formatting
  68. # see man git-log and search for PRETTY FORMATS.
  69. # %ai = date, %ae = author e-mail, %an = author name
  70. cmd = [ "git" , "log", "-n", "1", "--pretty=\"%ai,%ae,%an\"",
  71. branch_info.name ]
  72. offender = subprocess.check_output(cmd)
  73. offender = offender.strip(b"\n\"")
  74. # comment out this 2 lines to disable obfuscation
  75. offender = offender.replace(b"@", b"(at)")
  76. # Obfuscating a dot does not work too well for folks that use
  77. # initials
  78. #offender = offender.replace(b".", b"(dot)")
  79. branch_info.last_commit = offender.decode("utf-8")
  80. else:
  81. # diff is not empty, so there is something to merge
  82. branch_info.status = Branch.NOTMERGED
  83. out.append(branch_info)
  84. return out
  85. def branch_print(branches, csv, print_merged, print_notmerged, print_stats):
  86. """ prints out list of branches with specified details (using
  87. human-readable (or CSV) format. It is possible to specify,
  88. which branches should be printed (merged, notmerged) and
  89. also print out summary statistics """
  90. # counters used for statistics
  91. merged = 0
  92. notmerged = 0
  93. # compact list of merged/notmerged branches
  94. merged_str = ""
  95. notmerged_str = ""
  96. for branch in branches:
  97. if branch.status == Branch.MERGED:
  98. merged = merged + 1
  99. if not print_merged:
  100. continue
  101. if csv:
  102. print("%s,merged,%s" % (branch.name, branch.last_commit) )
  103. else:
  104. merged_str = merged_str + " " + branch.name
  105. else:
  106. # NOT MERGED
  107. notmerged = notmerged + 1
  108. if not print_notmerged:
  109. continue
  110. if csv:
  111. print("%s,notmerged,%s" % (branch.name, branch.last_commit) )
  112. else:
  113. notmerged_str = notmerged_str + " " + branch.name
  114. if not csv:
  115. if print_merged:
  116. print("Merged branches : %s" % (merged_str))
  117. if print_notmerged:
  118. print("NOT merged branches: %s" % (notmerged_str))
  119. if print_stats:
  120. print("#----------")
  121. print("#Merged : %d" % merged)
  122. print("#Not merged: %d" % notmerged)
  123. def parse_args(args=sys.argv[1:], Parser=OptionParser):
  124. parser = Parser(description="This script prints out merged and/or unmerged"
  125. " branches of a GIT tree.")
  126. parser.add_option("-c", "--csv", action="store_true",
  127. default=False, help="generates CSV output")
  128. parser.add_option("-u", "--unmerged", action="store_true",
  129. default=False, help="lists unmerged branches")
  130. parser.add_option("-m", "--skip-merged", action="store_true",
  131. default=False, help="omits listing merged branches")
  132. parser.add_option("-s", "--stats", action="store_true",
  133. default=False, help="prints also statistics")
  134. (options, args) = parser.parse_args(args)
  135. if args:
  136. parser.print_help()
  137. sys.exit(1)
  138. return options
  139. def main():
  140. usage = """%prog
  141. Lists all obsolete (fully merged into master) branches.
  142. """
  143. options = parse_args()
  144. csv = options.csv
  145. merged = not options.skip_merged
  146. unmerged = options.unmerged
  147. stats = options.stats
  148. if csv:
  149. print("branch name,status,date,last commit(mail),last commit(name)")
  150. branch_list = branch_list_get(not csv)
  151. branch_print(branch_list, csv, merged, unmerged, stats)
  152. if __name__ == '__main__':
  153. main()