Welcome to mirror list, hosted at ThFree Co, Russian Federation.

commits-to-changelog.py « scripts - github.com/mono/mono.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 744528fffb1f673cdbdd496465224976af86b334 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/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 ("(?<!\n)\n(?!\n)", " ", message)
    # paragraphize - finally, replace double newlines with single ones
    message = re.sub ("\n\n", "\n", message)

    # A list of paragraphs (strings without newlines) that occur
    # before the first file comments
    prefix = []

    # A list of tuples of the form ([(filename, entity), ...], [paragraph, ...]).
    #
    # Each describes a file comment, containing multiple paragraphs.
    # Those paragraphs belong to a list of files, each with an
    # optional entity (usually a function name).
    file_entries = []

    current_files = None
    current_files_comments = None

    message_lines = message.splitlines ()
    for line in message_lines:
        if re.match ("\*\s[^:]+:", line):
            if current_files:
                file_entries.append ((current_files, current_files_comments))

            (files, _, comments) = line.partition (":")

            current_files_comments = [comments.strip ()]

            current_files = []
            for f in re.split ("\s*,\s*", files [1:].strip ()):
                m = re.search ("\(([^()]+)\)$", f)
                if m:
                    filename = f [:m.start (0)].strip ()
                    entity = m.group (1).strip ()
                else:
                    filename = f
                    entity = None
                current_files.append ((filename, entity))
        else:
            if current_files:
                current_files_comments.append (line)
            else:
                prefix.append (line)
    if current_files:
        file_entries.append ((current_files, current_files_comments))

    changelog_entries = format_changelog_entries (commit, changed_files, prefix, file_entries, message_lines)

    #debug_print_commit (commit, raw_message, prefix, file_entries, changed_files, changelog_entries)

    return changelog_entries

def start_changelog (changelog):
    full_path = changelog_path (changelog)
    old_name = full_path + ".old"
    os.rename (full_path, old_name)
    return open (full_path, "w")

def finish_changelog (changelog, file):
    old_file = open (changelog_path (changelog) + ".old")
    file.write (old_file.read ())
    old_file.close ()
    file.close ()

def append_lines (file, lines):
    for line in lines:
        file.write (line + "\n")
    file.write ("\n")

def main ():
    usage = "usage: %prog [options] <start-commit>"
    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 + "/"

    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 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 ()