#!/usr/bin/python from optparse import OptionParser import subprocess import re import os.path import fnmatch import os import sys # subtract 8 for the leading tabstop fill_column = 74 - 8 path_to_root = None all_changelogs = {} def git (command, *args): popen = subprocess.Popen (["git", command] + list (args), stdout = subprocess.PIPE) output = popen.communicate () [0] if popen.returncode != 0: print >> sys.stderr, "Error: git failed" exit (1) return output def changelog_path (changelog): global path_to_root if not path_to_root: path_to_root = git ("rev-parse", "--show-cdup").strip () (pathname, filename) = changelog return path_to_root + "./" + pathname + "/" + filename def changelog_for_file (filename): while filename != "": dirname = os.path.dirname (filename) if dirname in all_changelogs: return (dirname, all_changelogs [dirname]) filename = dirname assert False def changelogs_for_file_pattern (pattern, changed_files): changelogs = set () for filename in changed_files: suffix = filename while suffix != "": # FIXME: fnmatch doesn't support the {x,y} pattern if fnmatch.fnmatch (suffix, pattern): changelogs.add (changelog_for_file (filename)) (_, _, suffix) = suffix.partition ("/") return changelogs def format_paragraph (paragraph): lines = [] words = paragraph.split () current = words [0] for word in words [1:]: if len (current) + 1 + len (word) <= fill_column: current += " " + word else: lines.append ("\t" + current) current = word lines.append ("\t" + current) return lines def format_changelog_paragraph (files, paragraph): files_string = "" for (filename, entity) in files: if len (files_string) > 0: files_string += ", " files_string += filename if entity: files_string += " (" + entity + ")" return format_paragraph ("* " + files_string + ": " + paragraph) def append_paragraph (lines, paragraph): if len (lines): lines.append ("") lines += paragraph def format_changelog_entries (commit, changed_files, prefix, file_entries, all_paragraphs): changelogs = set () for f in changed_files: changelogs.add (changelog_for_file (f)) marked_changelogs = set () author_line = git ("log", "-n1", "--date=short", "--format=%ad %an <%ae>", commit).strip () paragraphs = {} for changelog in changelogs: paragraphs [changelog] = [author_line] for (files, comments) in file_entries: changelog_entries = {} for (filename, entity) in files: entry_changelogs = changelogs_for_file_pattern (filename, changed_files) if len (entry_changelogs) == 0: print "Warning: could not match file %s in commit %s" % (filename, commit) for changelog in entry_changelogs: if changelog not in changelog_entries: changelog_entries [changelog] = [] changelog_entries [changelog].append ((filename, entity)) marked_changelogs.add (changelog) for (changelog, files) in changelog_entries.items (): append_paragraph (paragraphs [changelog], format_changelog_paragraph (files, comments [0])) for paragraph in comments [1:]: append_paragraph (paragraphs [changelog], format_paragraph (paragraph)) unmarked_changelogs = changelogs - marked_changelogs for changelog in unmarked_changelogs: if len (prefix) == 0: print "Warning: empty entry in %s for commit %s" % (changelog_path (changelog), commit) insert_paragraphs = all_paragraphs else: insert_paragraphs = prefix for paragraph in insert_paragraphs: append_paragraph (paragraphs [changelog], format_paragraph (paragraph)) return paragraphs def debug_print_commit (commit, raw_message, prefix, file_entries, changed_files, changelog_entries): print "===================== Commit" print commit print "--------------------- RAW" print raw_message print "--------------------- Prefix" for line in prefix: print line print "--------------------- File entries" for (files, comments) in file_entries: files_str = "" for (filename, entity) in files: if len (files_str): files_str = files_str + ", " files_str = files_str + filename if entity: files_str = files_str + " (" + entity + ")" print files_str for line in comments: print " " + line print "--------------------- Files touched" for f in changed_files: print f print "--------------------- ChangeLog entries" for ((dirname, filename), lines) in changelog_entries.items (): print "%s/%s:" % (dirname, filename) for line in lines: print line def process_commit (commit): changed_files = map (lambda l: l.split () [2], git ("diff-tree", "--numstat", commit).splitlines () [1:]) if len (filter (lambda f: re.search ("(^|/)Change[Ll]og$", f), changed_files)): return None raw_message = git ("log", "-n1", "--format=%B", commit) # filter SVN migration message message = re.sub ("(^|\n)svn path=[^\n]+revision=\d+(?=$|\n)", "", raw_message) # filter ChangeLog headers message = re.sub ("(^|\n)\d+-\d+-\d+[ \t]+((\w|[.-])+[ \t]+)+<[^\n>]+>(?=$|\n)", "", message) # filter leading whitespace message = re.sub ("^\s+", "", message) # filter trailing whitespace message = re.sub ("\s+$", "", message) # paragraphize - first remove whitespace at beginnings and ends of lines message = re.sub ("[ \t]*\n[ \t]*", "\n", message) # paragraphize - now replace three or more consecutive newlines with two message = re.sub ("\n\n\n+", "\n\n", message) # paragraphize - replace single newlines with a space message = re.sub ("(?" parser = OptionParser (usage) parser.add_option ("-r", "--root", dest = "root", help = "Root directory of the working tree to be changed") (options, args) = parser.parse_args () if len (args) != 1: parser.error ("incorrect number of arguments") start_commit = args [0] if options.root: global path_to_root path_to_root = options.root + "/" # MonkeyWrench uses a shared git repo but sets BUILD_REVISION, # if present we use it instead of HEAD HEAD = "HEAD" if 'BUILD_REVISION' in os.environ: HEAD = os.environ['BUILD_REVISION'] #see if git supports %B in --format output = git ("log", "-n1", "--format=%B", HEAD) if output.startswith ("%B"): print >> sys.stderr, "Error: git doesn't support %B in --format - install version 1.7.2 or newer" exit (1) for filename in git ("ls-tree", "-r", "--name-only", HEAD).splitlines (): if re.search ("(^|/)Change[Ll]og$", filename): (path, name) = os.path.split (filename) all_changelogs [path] = name commits = git ("rev-list", "--no-merges", HEAD, "^%s" % start_commit).splitlines () touched_changelogs = {} for commit in commits: entries = process_commit (commit) if entries == None: continue for (changelog, lines) in entries.items (): if not os.path.exists (changelog_path (changelog)): continue if changelog not in touched_changelogs: touched_changelogs [changelog] = start_changelog (changelog) append_lines (touched_changelogs [changelog], lines) for (changelog, file) in touched_changelogs.items (): finish_changelog (changelog, file) if __name__ == "__main__": main ()