git-obsolete-branch.py 6.5 KB

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