3 from optparse
import OptionParser
11 # subtract 8 for the leading tabstop
18 def git (command
, *args
):
19 popen
= subprocess
.Popen (["git", command
] + list (args
), stdout
= subprocess
.PIPE
)
20 output
= popen
.communicate () [0]
21 if popen
.returncode
!= 0:
22 print >> sys
.stderr
, "Error: git failed"
26 def changelog_path (changelog
):
29 path_to_root
= git ("rev-parse", "--show-cdup").strip ()
30 (pathname
, filename
) = changelog
31 return path_to_root
+ "./" + pathname
+ "/" + filename
33 def changelog_for_file (filename
):
35 dirname
= os
.path
.dirname (filename
)
36 if dirname
in all_changelogs
:
37 return (dirname
, all_changelogs
[dirname
])
41 def changelogs_for_file_pattern (pattern
, changed_files
):
43 for filename
in changed_files
:
46 # FIXME: fnmatch doesn't support the {x,y} pattern
47 if fnmatch
.fnmatch (suffix
, pattern
):
48 changelogs
.add (changelog_for_file (filename
))
49 (_
, _
, suffix
) = suffix
.partition ("/")
52 def format_paragraph (paragraph
):
54 words
= paragraph
.split ()
56 for word
in words
[1:]:
57 if len (current
) + 1 + len (word
) <= fill_column
:
60 lines
.append ("\t" + current
)
62 lines
.append ("\t" + current
)
65 def format_changelog_paragraph (files
, paragraph
):
67 for (filename
, entity
) in files
:
68 if len (files_string
) > 0:
70 files_string
+= filename
72 files_string
+= " (" + entity
+ ")"
73 return format_paragraph ("* " + files_string
+ ": " + paragraph
)
75 def append_paragraph (lines
, paragraph
):
80 def format_changelog_entries (commit
, changed_files
, prefix
, file_entries
, all_paragraphs
):
82 for f
in changed_files
:
83 changelogs
.add (changelog_for_file (f
))
84 marked_changelogs
= set ()
86 author_line
= git ("log", "-n1", "--date=short", "--format=%ad %an <%ae>", commit
).strip ()
89 for changelog
in changelogs
:
90 paragraphs
[changelog
] = [author_line
]
92 for (files
, comments
) in file_entries
:
93 changelog_entries
= {}
94 for (filename
, entity
) in files
:
95 entry_changelogs
= changelogs_for_file_pattern (filename
, changed_files
)
96 if len (entry_changelogs
) == 0:
97 print "Warning: could not match file %s in commit %s" % (filename
, commit
)
98 for changelog
in entry_changelogs
:
99 if changelog
not in changelog_entries
:
100 changelog_entries
[changelog
] = []
101 changelog_entries
[changelog
].append ((filename
, entity
))
102 marked_changelogs
.add (changelog
)
104 for (changelog
, files
) in changelog_entries
.items ():
105 append_paragraph (paragraphs
[changelog
], format_changelog_paragraph (files
, comments
[0]))
106 for paragraph
in comments
[1:]:
107 append_paragraph (paragraphs
[changelog
], format_paragraph (paragraph
))
109 unmarked_changelogs
= changelogs
- marked_changelogs
110 for changelog
in unmarked_changelogs
:
111 if len (prefix
) == 0:
112 print "Warning: empty entry in %s for commit %s" % (changelog_path (changelog
), commit
)
113 insert_paragraphs
= all_paragraphs
115 insert_paragraphs
= prefix
116 for paragraph
in insert_paragraphs
:
117 append_paragraph (paragraphs
[changelog
], format_paragraph (paragraph
))
121 def debug_print_commit (commit
, raw_message
, prefix
, file_entries
, changed_files
, changelog_entries
):
122 print "===================== Commit"
124 print "--------------------- RAW"
126 print "--------------------- Prefix"
129 print "--------------------- File entries"
130 for (files
, comments
) in file_entries
:
132 for (filename
, entity
) in files
:
134 files_str
= files_str
+ ", "
135 files_str
= files_str
+ filename
137 files_str
= files_str
+ " (" + entity
+ ")"
139 for line
in comments
:
141 print "--------------------- Files touched"
142 for f
in changed_files
:
144 print "--------------------- ChangeLog entries"
145 for ((dirname
, filename
), lines
) in changelog_entries
.items ():
146 print "%s/%s:" % (dirname
, filename
)
150 def process_commit (commit
):
151 changed_files
= map (lambda l
: l
.split () [2], git ("diff-tree", "--numstat", commit
).splitlines () [1:])
152 if len (filter (lambda f
: re
.search ("(^|/)Change[Ll]og$", f
), changed_files
)):
154 raw_message
= git ("log", "-n1", "--format=%B", commit
)
155 # filter SVN migration message
156 message
= re
.sub ("(^|\n)svn path=[^\n]+revision=\d+(?=$|\n)", "", raw_message
)
157 # filter ChangeLog headers
158 message
= re
.sub ("(^|\n)\d+-\d+-\d+[ \t]+((\w|[.-])+[ \t]+)+<[^\n>]+>(?=$|\n)", "", message
)
159 # filter leading whitespace
160 message
= re
.sub ("^\s+", "", message
)
161 # filter trailing whitespace
162 message
= re
.sub ("\s+$", "", message
)
163 # paragraphize - first remove whitespace at beginnings and ends of lines
164 message
= re
.sub ("[ \t]*\n[ \t]*", "\n", message
)
165 # paragraphize - now replace three or more consecutive newlines with two
166 message
= re
.sub ("\n\n\n+", "\n\n", message
)
167 # paragraphize - replace single newlines with a space
168 message
= re
.sub ("(?<!\n)\n(?!\n)", " ", message
)
169 # paragraphize - finally, replace double newlines with single ones
170 message
= re
.sub ("\n\n", "\n", message
)
172 # A list of paragraphs (strings without newlines) that occur
173 # before the first file comments
176 # A list of tuples of the form ([(filename, entity), ...], [paragraph, ...]).
178 # Each describes a file comment, containing multiple paragraphs.
179 # Those paragraphs belong to a list of files, each with an
180 # optional entity (usually a function name).
184 current_files_comments
= None
186 message_lines
= message
.splitlines ()
187 for line
in message_lines
:
188 if re
.match ("\*\s[^:]+:", line
):
190 file_entries
.append ((current_files
, current_files_comments
))
192 (files
, _
, comments
) = line
.partition (":")
194 current_files_comments
= [comments
.strip ()]
197 for f
in re
.split ("\s*,\s*", files
[1:].strip ()):
198 m
= re
.search ("\(([^()]+)\)$", f
)
200 filename
= f
[:m
.start (0)].strip ()
201 entity
= m
.group (1).strip ()
205 current_files
.append ((filename
, entity
))
208 current_files_comments
.append (line
)
212 file_entries
.append ((current_files
, current_files_comments
))
214 changelog_entries
= format_changelog_entries (commit
, changed_files
, prefix
, file_entries
, message_lines
)
216 #debug_print_commit (commit, raw_message, prefix, file_entries, changed_files, changelog_entries)
218 return changelog_entries
220 def start_changelog (changelog
):
221 full_path
= changelog_path (changelog
)
222 old_name
= full_path
+ ".old"
223 os
.rename (full_path
, old_name
)
224 return open (full_path
, "w")
226 def finish_changelog (changelog
, file):
227 old_file
= open (changelog_path (changelog
) + ".old")
228 file.write (old_file
.read ())
232 def append_lines (file, lines
):
234 file.write (line
+ "\n")
238 usage
= "usage: %prog [options] <start-commit>"
239 parser
= OptionParser (usage
)
240 parser
.add_option ("-r", "--root", dest
= "root", help = "Root directory of the working tree to be changed")
241 (options
, args
) = parser
.parse_args ()
243 parser
.error ("incorrect number of arguments")
244 start_commit
= args
[0]
248 path_to_root
= options
.root
+ "/"
250 # MonkeyWrench uses a shared git repo but sets BUILD_REVISION,
251 # if present we use it instead of HEAD
253 if 'BUILD_REVISION' in os
.environ
:
254 HEAD
= os
.environ
['BUILD_REVISION']
256 #see if git supports %B in --format
257 output
= git ("log", "-n1", "--format=%B", HEAD
)
258 if output
.startswith ("%B"):
259 print >> sys
.stderr
, "Error: git doesn't support %B in --format - install version 1.7.2 or newer"
262 for filename
in git ("ls-tree", "-r", "--name-only", HEAD
).splitlines ():
263 if re
.search ("(^|/)Change[Ll]og$", filename
):
264 (path
, name
) = os
.path
.split (filename
)
265 all_changelogs
[path
] = name
267 commits
= git ("rev-list", "--no-merges", HEAD
, "^%s" % start_commit
).splitlines ()
269 touched_changelogs
= {}
270 for commit
in commits
:
271 entries
= process_commit (commit
)
274 for (changelog
, lines
) in entries
.items ():
275 if not os
.path
.exists (changelog_path (changelog
)):
277 if changelog
not in touched_changelogs
:
278 touched_changelogs
[changelog
] = start_changelog (changelog
)
279 append_lines (touched_changelogs
[changelog
], lines
)
280 for (changelog
, file) in touched_changelogs
.items ():
281 finish_changelog (changelog
, file)
283 if __name__
== "__main__":