3 from __future__
import print_function
4 from optparse
import OptionParser
12 # subtract 8 for the leading tabstop
19 def git (command
, *args
):
20 popen
= subprocess
.Popen (["git", command
] + list (args
), stdout
= subprocess
.PIPE
)
21 output
= popen
.communicate () [0]
22 if popen
.returncode
!= 0:
23 print ("Error: git failed", file=sys
.stderr
)
27 def changelog_path (changelog
):
30 path_to_root
= git ("rev-parse", "--show-cdup").strip ()
31 (pathname
, filename
) = changelog
32 return path_to_root
+ "./" + pathname
+ "/" + filename
34 def changelog_for_file (filename
):
36 dirname
= os
.path
.dirname (filename
)
37 if dirname
in all_changelogs
:
38 return (dirname
, all_changelogs
[dirname
])
42 def changelogs_for_file_pattern (pattern
, changed_files
):
44 for filename
in changed_files
:
47 # FIXME: fnmatch doesn't support the {x,y} pattern
48 if fnmatch
.fnmatch (suffix
, pattern
):
49 changelogs
.add (changelog_for_file (filename
))
50 (_
, _
, suffix
) = suffix
.partition ("/")
53 def format_paragraph (paragraph
):
55 words
= paragraph
.split ()
59 for word
in words
[1:]:
60 if len (current
) + 1 + len (word
) <= fill_column
:
63 lines
.append ("\t" + current
)
65 lines
.append ("\t" + current
)
68 def format_changelog_paragraph (files
, paragraph
):
70 for (filename
, entity
) in files
:
71 if len (files_string
) > 0:
73 files_string
+= filename
75 files_string
+= " (" + entity
+ ")"
76 return format_paragraph ("* " + files_string
+ ": " + paragraph
)
78 def append_paragraph (lines
, paragraph
):
83 def format_changelog_entries (commit
, changed_files
, prefix
, file_entries
, all_paragraphs
):
85 for f
in changed_files
:
86 changelogs
.add (changelog_for_file (f
))
87 marked_changelogs
= set ()
89 author_line
= git ("log", "-n1", "--date=short", "--format=%ad %an <%ae>", commit
).strip ()
92 for changelog
in changelogs
:
93 paragraphs
[changelog
] = [author_line
]
95 for (files
, comments
) in file_entries
:
96 changelog_entries
= {}
97 for (filename
, entity
) in files
:
98 entry_changelogs
= changelogs_for_file_pattern (filename
, changed_files
)
99 if len (entry_changelogs
) == 0:
100 print ("Warning: could not match file {0} in commit {1}".format (filename
, commit
))
101 for changelog
in entry_changelogs
:
102 if changelog
not in changelog_entries
:
103 changelog_entries
[changelog
] = []
104 changelog_entries
[changelog
].append ((filename
, entity
))
105 marked_changelogs
.add (changelog
)
107 for (changelog
, files
) in changelog_entries
.items ():
108 append_paragraph (paragraphs
[changelog
], format_changelog_paragraph (files
, comments
[0]))
109 for paragraph
in comments
[1:]:
110 append_paragraph (paragraphs
[changelog
], format_paragraph (paragraph
))
112 unmarked_changelogs
= changelogs
- marked_changelogs
113 for changelog
in unmarked_changelogs
:
114 if len (prefix
) == 0:
115 print ("Warning: empty entry in {0} for commit {1}".format (changelog_path (changelog
), commit
))
116 insert_paragraphs
= all_paragraphs
118 insert_paragraphs
= prefix
119 for paragraph
in insert_paragraphs
:
120 append_paragraph (paragraphs
[changelog
], format_paragraph (paragraph
))
124 def debug_print_commit (commit
, raw_message
, prefix
, file_entries
, changed_files
, changelog_entries
):
125 print ("===================== Commit")
127 print ("--------------------- RAW")
129 print ("--------------------- Prefix")
132 print ("--------------------- File entries")
133 for (files
, comments
) in file_entries
:
135 for (filename
, entity
) in files
:
137 files_str
= files_str
+ ", "
138 files_str
= files_str
+ filename
140 files_str
= files_str
+ " (" + entity
+ ")"
142 for line
in comments
:
144 print ("--------------------- Files touched")
145 for f
in changed_files
:
147 print ("--------------------- ChangeLog entries")
148 for ((dirname
, filename
), lines
) in changelog_entries
.items ():
149 print ("{0}/{1}:".format (dirname
, filename
))
153 def process_commit (commit
):
154 changed_files
= map (lambda l
: l
.split () [2], git ("diff-tree", "--numstat", commit
).splitlines () [1:])
155 if len (filter (lambda f
: re
.search ("(^|/)Change[Ll]og$", f
), changed_files
)):
157 raw_message
= git ("log", "-n1", "--format=%B", commit
)
158 # filter SVN migration message
159 message
= re
.sub ("(^|\n)svn path=[^\n]+revision=\d+(?=$|\n)", "", raw_message
)
160 # filter ChangeLog headers
161 message
= re
.sub ("(^|\n)\d+-\d+-\d+[ \t]+((\w|[.-])+[ \t]+)+<[^\n>]+>(?=$|\n)", "", message
)
162 # filter leading whitespace
163 message
= re
.sub ("^\s+", "", message
)
164 # filter trailing whitespace
165 message
= re
.sub ("\s+$", "", message
)
166 # paragraphize - first remove whitespace at beginnings and ends of lines
167 message
= re
.sub ("[ \t]*\n[ \t]*", "\n", message
)
168 # paragraphize - now replace three or more consecutive newlines with two
169 message
= re
.sub ("\n\n\n+", "\n\n", message
)
170 # paragraphize - replace single newlines with a space
171 message
= re
.sub ("(?<!\n)\n(?!\n)", " ", message
)
172 # paragraphize - finally, replace double newlines with single ones
173 message
= re
.sub ("\n\n", "\n", message
)
175 # A list of paragraphs (strings without newlines) that occur
176 # before the first file comments
179 # A list of tuples of the form ([(filename, entity), ...], [paragraph, ...]).
181 # Each describes a file comment, containing multiple paragraphs.
182 # Those paragraphs belong to a list of files, each with an
183 # optional entity (usually a function name).
187 current_files_comments
= None
189 message_lines
= message
.splitlines ()
190 for line
in message_lines
:
191 if re
.match ("\*\s[^:]+:", line
):
193 file_entries
.append ((current_files
, current_files_comments
))
195 (files
, _
, comments
) = line
.partition (":")
197 current_files_comments
= [comments
.strip ()]
200 for f
in re
.split ("\s*,\s*", files
[1:].strip ()):
201 m
= re
.search ("\(([^()]+)\)$", f
)
203 filename
= f
[:m
.start (0)].strip ()
204 entity
= m
.group (1).strip ()
208 current_files
.append ((filename
, entity
))
211 current_files_comments
.append (line
)
215 file_entries
.append ((current_files
, current_files_comments
))
217 changelog_entries
= format_changelog_entries (commit
, changed_files
, prefix
, file_entries
, message_lines
)
219 #debug_print_commit (commit, raw_message, prefix, file_entries, changed_files, changelog_entries)
221 return changelog_entries
223 def start_changelog (changelog
):
224 full_path
= changelog_path (changelog
)
225 old_name
= full_path
+ ".old"
226 os
.rename (full_path
, old_name
)
227 return open (full_path
, "w")
229 def finish_changelog (changelog
, file):
230 old_file
= open (changelog_path (changelog
) + ".old")
231 file.write (old_file
.read ())
235 def append_lines (file, lines
):
237 file.write (line
+ "\n")
241 usage
= "usage: %prog [options] <start-commit>"
242 parser
= OptionParser (usage
)
243 parser
.add_option ("-r", "--root", dest
= "root", help = "Root directory of the working tree to be changed")
244 (options
, args
) = parser
.parse_args ()
246 parser
.error ("incorrect number of arguments")
247 start_commit
= args
[0]
251 path_to_root
= options
.root
+ "/"
253 # MonkeyWrench uses a shared git repo but sets BUILD_REVISION,
254 # if present we use it instead of HEAD
256 if 'BUILD_REVISION' in os
.environ
:
257 HEAD
= os
.environ
['BUILD_REVISION']
259 #see if git supports %B in --format
260 output
= git ("log", "-n1", "--format=%B", HEAD
)
261 if output
.startswith ("%B"):
262 print ("Error: git doesn't support %B in --format - install version 1.7.2 or newer", file=sys
.stderr
)
265 for filename
in git ("ls-tree", "-r", "--name-only", HEAD
).splitlines ():
266 if re
.search ("(^|/)Change[Ll]og$", filename
):
267 (path
, name
) = os
.path
.split (filename
)
268 all_changelogs
[path
] = name
270 commits
= git ("rev-list", "--no-merges", HEAD
, "^{0}".format (start_commit
)).splitlines ()
272 touched_changelogs
= {}
273 for commit
in commits
:
274 entries
= process_commit (commit
)
277 for (changelog
, lines
) in entries
.items ():
278 if not os
.path
.exists (changelog_path (changelog
)):
280 if changelog
not in touched_changelogs
:
281 touched_changelogs
[changelog
] = start_changelog (changelog
)
282 append_lines (touched_changelogs
[changelog
], lines
)
283 for (changelog
, file) in touched_changelogs
.items ():
284 finish_changelog (changelog
, file)
286 if __name__
== "__main__":