3 from optparse
import OptionParser
10 # subtract 8 for the leading tabstop
17 def git (command
, *args
):
18 return subprocess
.Popen (["git", command
] + list (args
), stdout
= subprocess
.PIPE
).communicate () [0]
20 def changelog_path (changelog
):
23 path_to_root
= git ("rev-parse", "--show-cdup").strip ()
24 (pathname
, filename
) = changelog
25 return path_to_root
+ "./" + pathname
+ "/" + filename
27 def changelog_for_file (filename
):
29 dirname
= os
.path
.dirname (filename
)
30 if dirname
in all_changelogs
:
31 return (dirname
, all_changelogs
[dirname
])
35 def changelogs_for_file_pattern (pattern
, changed_files
):
37 for filename
in changed_files
:
40 # FIXME: fnmatch doesn't support the {x,y} pattern
41 if fnmatch
.fnmatch (suffix
, pattern
):
42 changelogs
.add (changelog_for_file (filename
))
43 (_
, _
, suffix
) = suffix
.partition ("/")
46 def format_paragraph (paragraph
):
48 words
= paragraph
.split ()
50 for word
in words
[1:]:
51 if len (current
) + 1 + len (word
) <= fill_column
:
54 lines
.append ("\t" + current
)
56 lines
.append ("\t" + current
)
59 def format_changelog_paragraph (files
, paragraph
):
61 for (filename
, entity
) in files
:
62 if len (files_string
) > 0:
64 files_string
+= filename
66 files_string
+= " (" + entity
+ ")"
67 return format_paragraph ("* " + files_string
+ ": " + paragraph
)
69 def append_paragraph (lines
, paragraph
):
74 def format_changelog_entries (commit
, changed_files
, prefix
, file_entries
):
76 for f
in changed_files
:
77 changelogs
.add (changelog_for_file (f
))
78 marked_changelogs
= set ()
80 author_line
= git ("log", "-n1", "--date=short", "--format=%ad %an <%ae>", commit
).strip ()
83 for changelog
in changelogs
:
84 paragraphs
[changelog
] = [author_line
]
86 for (files
, comments
) in file_entries
:
87 changelog_entries
= {}
88 for (filename
, entity
) in files
:
89 entry_changelogs
= changelogs_for_file_pattern (filename
, changed_files
)
90 if len (entry_changelogs
) == 0:
91 print "Warning: could not match file %s in commit %s" % (filename
, commit
)
92 for changelog
in entry_changelogs
:
93 if changelog
not in changelog_entries
:
94 changelog_entries
[changelog
] = []
95 changelog_entries
[changelog
].append ((filename
, entity
))
96 marked_changelogs
.add (changelog
)
98 for (changelog
, files
) in changelog_entries
.items ():
99 append_paragraph (paragraphs
[changelog
], format_changelog_paragraph (files
, comments
[0]))
100 for paragraph
in comments
[1:]:
101 append_paragraph (paragraphs
[changelog
], format_paragraph (paragraph
))
103 unmarked_changelogs
= changelogs
- marked_changelogs
104 for changelog
in unmarked_changelogs
:
105 if len (prefix
) == 0:
106 print "Warning: empty entry in %s for commit %s" % (changelog_path (changelog
), commit
)
107 append_paragraph (paragraphs
[changelog
], format_paragraph ("FIXME: empty entry!"))
108 for paragraph
in prefix
:
109 append_paragraph (paragraphs
[changelog
], format_paragraph (paragraph
))
113 def debug_print_commit (commit
, raw_message
, prefix
, file_entries
, changed_files
, changelog_entries
):
114 print "===================== Commit"
116 print "--------------------- RAW"
118 print "--------------------- Prefix"
121 print "--------------------- File entries"
122 for (files
, comments
) in file_entries
:
124 for (filename
, entity
) in files
:
126 files_str
= files_str
+ ", "
127 files_str
= files_str
+ filename
129 files_str
= files_str
+ " (" + entity
+ ")"
131 for line
in comments
:
133 print "--------------------- Files touched"
134 for f
in changed_files
:
136 print "--------------------- ChangeLog entries"
137 for ((dirname
, filename
), lines
) in changelog_entries
.items ():
138 print "%s/%s:" % (dirname
, filename
)
142 def process_commit (commit
):
143 changed_files
= map (lambda l
: l
.split () [2], git ("diff-tree", "--numstat", commit
).splitlines () [1:])
144 if len (filter (lambda f
: re
.search ("(^|/)Change[Ll]og$", f
), changed_files
)):
146 raw_message
= git ("log", "-n1", "--format=%B", commit
)
147 # filter SVN migration message
148 message
= re
.sub ("(^|\n)svn path=[^\n]+revision=\d+(?=$|\n)", "", raw_message
)
149 # filter ChangeLog headers
150 message
= re
.sub ("(^|\n)\d+-\d+-\d+[ \t]+((\w|[.-])+[ \t]+)+<[^\n>]+>(?=$|\n)", "", message
)
151 # filter leading whitespace
152 message
= re
.sub ("^\s+", "", message
)
153 # filter trailing whitespace
154 message
= re
.sub ("\s+$", "", message
)
155 # paragraphize - first remove whitespace at beginnings and ends of lines
156 message
= re
.sub ("[ \t]*\n[ \t]*", "\n", message
)
157 # paragraphize - now replace three or more consecutive newlines with two
158 message
= re
.sub ("\n\n\n+", "\n\n", message
)
159 # paragraphize - replace single newlines with a space
160 message
= re
.sub ("(?<!\n)\n(?!\n)", " ", message
)
161 # paragraphize - finally, replace double newlines with single ones
162 message
= re
.sub ("\n\n", "\n", message
)
164 # A list of paragraphs (strings without newlines) that occur
165 # before the first file comments
168 # A list of tuples of the form ([(filename, entity), ...], [paragraph, ...]).
170 # Each describes a file comment, containing multiple paragraphs.
171 # Those paragraphs belong to a list of files, each with an
172 # optional entity (usually a function name).
176 current_files_comments
= None
178 for line
in message
.splitlines ():
179 if re
.match ("\*\s[^:]+:", line
):
181 file_entries
.append ((current_files
, current_files_comments
))
183 (files
, _
, comments
) = line
.partition (":")
185 current_files_comments
= [comments
.strip ()]
188 for f
in re
.split ("\s*,\s*", files
[1:].strip ()):
189 m
= re
.search ("\(([^()]+)\)$", f
)
191 filename
= f
[:m
.start (0)].strip ()
192 entity
= m
.group (1).strip ()
196 current_files
.append ((filename
, entity
))
199 current_files_comments
.append (line
)
203 file_entries
.append ((current_files
, current_files_comments
))
205 changelog_entries
= format_changelog_entries (commit
, changed_files
, prefix
, file_entries
)
207 #debug_print_commit (commit, raw_message, prefix, file_entries, changed_files, changelog_entries)
209 return changelog_entries
211 def start_changelog (changelog
):
212 full_path
= changelog_path (changelog
)
213 old_name
= full_path
+ ".old"
214 os
.rename (full_path
, old_name
)
215 return open (full_path
, "w")
217 def finish_changelog (changelog
, file):
218 old_file
= open (changelog_path (changelog
) + ".old")
219 file.write (old_file
.read ())
223 def append_lines (file, lines
):
225 file.write (line
+ "\n")
229 usage
= "usage: %prog [options] <start-commit>"
230 parser
= OptionParser (usage
)
231 parser
.add_option ("-r", "--root", dest
= "root", help = "Root directory of the working tree to be changed")
232 (options
, args
) = parser
.parse_args ()
234 parser
.error ("incorrect number of arguments")
235 start_commit
= args
[0]
239 path_to_root
= options
.root
+ "/"
241 for filename
in git ("ls-tree", "-r", "--name-only", "HEAD").splitlines ():
242 if re
.search ("(^|/)Change[Ll]og$", filename
):
243 (path
, name
) = os
.path
.split (filename
)
244 all_changelogs
[path
] = name
246 commits
= git ("rev-list", "--no-merges", "HEAD", "^%s" % start_commit
).splitlines ()
248 touched_changelogs
= {}
249 for commit
in commits
:
250 entries
= process_commit (commit
)
251 for (changelog
, lines
) in entries
.items ():
252 if changelog
not in touched_changelogs
:
253 touched_changelogs
[changelog
] = start_changelog (changelog
)
254 append_lines (touched_changelogs
[changelog
], lines
)
255 for (changelog
, file) in touched_changelogs
.items ():
256 finish_changelog (changelog
, file)
258 if __name__
== "__main__":