aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/linden/scripts/make_changelog.py
blob: c71f48f0d40f04fbabd5b88c9923abe617654f5d (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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
#!/usr/bin/env python
# 
# @file   make_changelog.py
# @brief  Generates Imprudence's ChangeLog.txt from Git commit messages.
#
# Copyright (c) 2010, Jacek Antonelli
#
# The source code in this file is provided to you under the terms of
# the GNU General Public License, version 2.0 ("GPL"). Terms of the
# GPL can be found in doc/GPL-license.txt in this distribution, or
# online at http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
#
# By copying, modifying or distributing this software, you acknowledge
# that you have read and understood your obligations described above,
# and agree to abide by those obligations.
#
# ALL SOURCE CODE IS PROVIDED "AS IS." THE AUTHOR MAKES NO
# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
# COMPLETENESS OR PERFORMANCE.
# 


# This script generates ChangeLog.txt (in the the imprudence root
# directory) from the commit messages in the git repository. It parses
# the output from 'git log', then prints the log entries in a nicely
# formatted way, omitting unimportant or unwanted commits.
# 
# A commit will be omitted if any of the following are true:
# 
#   * It is listed in SPECIAL_COMMITS below with "-".
#   * It has "@nochangelog" in its commit message (but not
#     "@forcechangelog" or "@shortchangelog").
#   * It is a merge commit (i.e. has multiple parent commits).
#   * It is a plain SL source commit ("Second Life viewer sources ...").
# 
# But even if the above are true, it WILL be included if any of the
# following are true:
# 
#   * It is listed in SPECIAL_COMMITS below with "+" or "S".
#   * It has "@forcechangelog" or "@shortchangelog" in its commit
#     message (but not "@nochangelog").
# 
# By default, the ChangeLog will list the files that were modified,
# added, or deleted for each commit. But, that list will be omitted
# (or summarized) if any of the following are true:
# 
#   * More than 10 files were changed.
#   * It is listed in SPECIAL_COMMITS below with "S".
#   * It has "@shortchangelog" in its commit message.
# 


import re, os, sys, subprocess


SCRIPT_DIR = os.path.abspath( os.path.dirname( sys.argv[0] ) )
ROOT_DIR   = os.path.normpath( os.path.join(SCRIPT_DIR, "..", "..") )
CHANGELOG  = os.path.join(ROOT_DIR, "ChangeLog.txt")


# Commit IDs that should be treated specially.
#
# IDs with "+" will always be included, even if they would have been
# omitted for some other reason (such as being a merge commit).
#
# IDs with "-" will be omitted (equivalent to @nochangelog)
#
# IDs with "S" will always be included (like "+"), but will hide or
# summarize the list of changed files (equivalent to @shortchangelog)

SPECIAL_COMMITS = {

    # Merge commits that ARE worth mentioning.
    "13c27361136819af0d0a0f72eeb807f56829337f" : "+",
    "499afbab7be4c4136eea0e1897319c3ac4e9799d" : "+",
    "4bbee8774d743fb4787fc2435a20d89539011fa4" : "+",
    "6a5aab98892df74f60743f5b789959c9593d6647" : "+",
    "6e1e243da8c06ddd6847576c55e134d72ef42491" : "+",
    "87c760f959788e3ec9dc06cbd2207d0242b6a4c9" : "+",
    "8f50d81693ff9463ae49c36935977a2b70e6bf45" : "+",
    "94a57bea1504f2f7024264b15c3ed532d49d3ab0" : "+",
    "96aaf4408601768d1d277bd63e6a1c91295c4c5a" : "+",
    "9a4f5702473e7540cde1bbff2d7189d9ed71fd86" : "+",

    # Mention the first SL version Imprudence is based on (1.20.15)
    "31ba61e675d42675c6c6cd1077a93e0c5e055114" : "S",

    # Project early setup. Not worth mentioning.
    "6f0d1dc6b922f1b103a8933a03c4ed5e10b290ef" : "-",
    "993bca391adf825dcf139d516c17dfdca0832bc8" : "-",
    "e8ca04117d66356e8fe514d617d1da2d9b49a927" : "-",
    "ec8b17013071896e7228c7d0032d3bfc44697c3a" : "-",
    "f258e5d9af1087cab2be36a8c143815300187d62" : "-",
    "f37093d4dda55fd77bc9228c9fb359d46d4f1715" : "-",

    # Whitespace and line ending changes. Not worth mentioning.
    "285dcc2660ef0ed31bfbdc4f0a5cc53f40e90e36" : "-",
    "44dc53ef5d8f770412c2f7675b9ebfdeb3c2b698" : "-",
    "451aad0c993856f380d976de3d7fe343ad5f9811" : "-",
    "45beb0b1d8a16522dfc33894f4b69ee9a4b33efc" : "-",
    "567586af40a1683a3c89863c9dbedd2b3cc90897" : "-",
    "5bd80f31b2a6d06f6b84e444ed84b744bfebc311" : "-",
    "65272bae7013a785cb4e1dccb810e58e9f7fdfda" : "-",
    "8186bd3db550d2a5cafd840679e8b13ff10a82b5" : "-",
    "87494eab8a1221ccb35e91c14d242e4cfaab28c8" : "-",
    "93eb46dcb67e35e99e3f68b4cb23ee9bf2fbb2d8" : "-",
    "a86dba93c641056fc08ff9dc4bd173bc4b56036f" : "-",
    "b055fc86474cce8854cadadccec61f10dc6ef003" : "-",
    "c05810ec6ce1cb1fb40915b2b16f44c2600c6483" : "-",
    "cf4ee03e4415d599c10729d920a1c085aed411ed" : "-",
    "e20f3bb7c3310deca86cfc66af0a086261930bcf" : "-",

    # ChangeLog edits. Not worth mentioning.
    "0ee6701062d0506a725cd4ba9cd533ec4c46eb68" : "-",
    "197b6a954d37e46af91a474de1565b43e24e8c60" : "-",
    "2027db4808f36bcc230686f31dea3810d594701e" : "-",
    "2583bbaf227abd654cd87760bf6c646909b38229" : "-",
    "388810f1cb7505c7888f6aa77306554b99430202" : "-",
    "6b270891b1b64e26a5c07e9239b925527e7be4c2" : "-",
    "6e99cd9128a04ce8b5daa9b1c5c17c7b86e8a058" : "-",
    "78cff2cb53235979d452b877192523a030686f89" : "-",
    "818367f405a550e95c265d26c7011a5a1f892ec7" : "-",
    "87c7622724089f7a06b6559c9e1103e2f7bac2ef" : "-",
    "925975639a2438bc9bdf43a0ceaab1c4555052b0" : "-",
    "96aaf4408601768d1d277bd63e6a1c91295c4c5a" : "-",
    "9f89d959d4596a5e4918806efc2e5dd044cabc62" : "-",
    "a03abda0f0671ce69b9579b9fcf3d9d499080a5d" : "-",
    "b2efe398cd2a506fe47a6143ea54e08b1f4cbbc7" : "-",
    "b31f72f11b75cf0c7965d8170600e1352988dc25" : "-",
    "b33c4cfbbc49bb6c66e7bdd53d3a2245e1c6a2c9" : "-",
    "b5068c60260646572d3dad166e11c943050b8bd6" : "-",
    "cdd95787ae40cd1ea3776d2322ec529a5a232fc7" : "-",
    "d53da842e2f259282fb83c01ea742d80f7d2735e" : "-",
    "d6552c4c9c10a886ea19768e0097b7344809ea02" : "-",
    "ded2589eae6e392d2ff973d882a0182d2af199cf" : "-",
    "ea8040185053bd2807d0cfdf5cd018f08df10ed1" : "-",
    "ebce3d7682489151a6fc48a923c7154dad401219" : "-",
    "ed6a76513af6a7de5963517e19982293d418ddef" : "-",
    "eeb428b559f482b03ac1002a391a5df3e1512e9a" : "-",
    "f0acc222f33936756297a8240bcfc72c2f9ba891" : "-",

    # Release notes edits. Not worth mentioning.
    "08161bf33c3e17ec81c0d66cb7d2fda678ff6a17" : "-",
    "4f1e0a28875a8c54790d07442bfc3875f903b3c2" : "-",
    "60c7bcbed46bbc266beb642a71018b18c213078e" : "-",
    "8a662850a3ddae4485270c59e9f93c95691f5050" : "-",
    "c34a99c0b18d1122d5613c0048e572d70e7ce751" : "-",

    # Duplicates of other commits. Not worth mentioning.
    "23ab8d114f3038b8425613cd0c74adb160d0bf2f" : "-",
    "3a84d8017df08447b14161cee3c24382f8f96013" : "-",
    "6a7a1881a3fde403e9dacd638d10d457c50ab19e" : "-",
    "d345613880689d38fe6418f47dc19ae855849f92" : "-",

    # Miscellaneous insignificant things.
    "16751a56c0c465f90cb14f65483f967acb1220ae" : "-",
    "25919b3c9601918dbddaccb07ef48fc14a546cf1" : "-",
    "25ee74981392a1f1fde299f8ecf243a6b257e3ae" : "-",
    "5be6de589be48be699d8aa46593d0a0d3e88f46a" : "-",
    "686b994f11a5e7ec2c11e40dc3ca8178c47decdf" : "-",
    "9625d9f3253e0aa4ca8a68380c6303f0b8d36dbe" : "-",
    "a4ecbdbba79e49ce5653101c6c71f8b5df7e0fc5" : "-",
    "e563b107a2b58935ef8c3ab2cfdfdbc2d7cdfa2d" : "-",
    "e7c2d187818158c1c27f87056fba82b0e8077263" : "-",

    # Spammy and uninformative.
    "7f090f7bec5264ca9e203c27dfb6b2992bb2bcbd" : "-",
    "844025196a1b9a5944cac292dbc162bdd24ac5af" : "-",

    # Tsk tsk, McCabe has a potty mouth.
    "7624d729930c331494c7391e3fbc596b31309782" : "-",
    "a67f7ec260b20474cee3c2edca3c3f4ea331c815" : "-",

}



class LogEntry:
    "Stores and formats a Git log entry"

    # A horizontal line to put between log entries
    separator = "-" * 79


    def __init__(self, git_text):
        "Creates a LogEntry from a commit output from `git log`"

        self.id          = ""     # commit id
        self.merge       = False  # is a merge commit?
        self.author_name = ""
        self.author_mail = ""
        self.author_date = ""
        self.commit_name = ""     # committer name
        self.commit_mail = ""
        self.commit_date = ""
        self.message     = []     # lines of the message
        self.files       = []     # modified/added/deleted files
        self.special     = ""     # handle this commit in a special way?

        lines = git_text.splitlines()

        # First line is always commit id.
        self.id = lines[0]

        for line in lines[1:]:

            # is it a merge?
            if re.match("Merge: +([0-9a-f]+ *){2,}", line):
                self.merge = True

            # author name
            match = re.match("Author: +([^<]+) <([^>]+)>", line)
            if match:
                self.author_name = match.group(1)
                self.author_mail = match.group(2)

            # author date
            match = re.match("AuthorDate: +(.+)", line)
            if match:
                self.author_date = match.group(1)

            # commit name
            match = re.match("Commit: +([^<]+) <([^>]+)>", line)
            if match:
                self.commit_name = match.group(1)
                self.commit_mail = match.group(2)

            # commit date
            match = re.match("CommitDate: +(.+)", line)
            if match:
                self.commit_date = match.group(1)

            # message (and notes)
            if re.match("[\t ]+", line):
                self.message.append(line[4:])

            # modified/added/deleted files (but ignore ChangeLog.txt)
            if re.match("[MAD][\t ]+", line) and \
                    not re.match("[MAD][\t ]+ChangeLog.txt", line):
                self.files.append(line)

        self.author_name, self.author_mail = \
            self.fix_name_email( self.author_name, self.author_mail )

        self.commit_name, self.commit_mail = \
            self.fix_name_email( self.commit_name, self.commit_mail )

        try:
            self.special = SPECIAL_COMMITS[self.id]
        except KeyError:
            pass

        # If there's no special already, scan the message for @directives.
        if not self.special:
            for line in self.message:
                # Omit commits with @nochangelog in the message
                if("@nochangelog" in line):
                    self.special = "-"
                # Don't show file changes for commits with @shortchangelog
                elif("@shortchangelog" in line):
                    self.special = "S"
                # Always include commits with @forcechangelog
                elif("@forcechangelog" in line):
                    self.special = "+"


    def fix_name_email( self, name, email ):
        """Some commits have a bad name or email address.
           This function returns the proper name and address."""

        if email in ["Adric@.(none)", "hakushakukun@gmail.com"]:
            return ["McCabe Maxsted", "hakushakukun@gmail.com"]

        if email in ["Kakurady@.(none)", "kakurady@gmail.com"]:
            return ["Kakurady (Geneko Nemeth)", "kakurady@gmail.com"]

        # Nothing to fix.
        return [name, email]


    def should_be_included(self):
        """Returns True if the commit should be included in the
        ChangeLog, or False if it should be omitted."""

        # Include commits marked with "+" or "S" in special_commits.txt
        if self.special in ["+", "S"]:
            return True

        # Omit commits marked with "-" in special_commits.txt
        if self.special == "-":
            return False

        # Omit merge commits
        if self.merge:
            return False

        # Omit vanilla SL source commits
        if self.message[0].startswith("Second Life viewer sources"):
            return False

        # Include everything else
        return True


    def format(self):
        "Formats the LogEntry prettily."
        texts=[LogEntry.separator]

        texts.append("""
Date:       %(ad)s                                                (%(id)s)
Author:     %(an)s  <%(ae)s>""" % { "ad" : self.author_date,
                                    "an" : self.author_name,
                                    "ae" : self.author_mail,
                                    "id" : self.id[0:7] })

        if self.commit_name != self.author_name:
            texts.append("Committer:  %(cn)s  <%(ce)s>" % \
                             { "cn" : self.commit_name,
                               "ce" : self.commit_mail })

        texts.append("\n")

        for line in self.message:
            # Remove @...changelog directives
            rxp = re.compile("@(short|no)changelog")
            if rxp.search(line):
                line = rxp.sub("", line)
                # Skip this line if it was empty except for @...changelog
                if not line.strip():
                    continue

            # Skip modified/deleted/new file lines.
            if re.match("[ \t]+(modified|deleted|new file): +.+", line):
                continue

            # Skip all the rest of the lines.
            if re.match("Conflicts:$", line):
                break

            texts.append( (" "*2 + line.replace("\t"," "*4)).rstrip() )

        # Delete all empty lines at the end
        while not texts[-1].strip():
            del texts[-1]

        # Don't list files if it's a short changelog or there are more
        # than ten files to list.
        if self.special == "S" or len(self.files) > 10:
            texts.append("\n  (File changes omitted for brevity.)")
        else:
            if self.files:
                texts.append("\n")
                for line in self.files:
                    texts.append( " "*2 + line.replace("\t"," "*2) )

        texts.append("\n")

        return "\n".join(texts)



def fail( reason, abort=False ):
    """Prints a message that the ChangeLog couldn't be generated, then
    exits the script. If abort is True, exit with status code 1 (to
    indicate that Make/VisualStudio/Xcode/etc. should abort),
    otherwise exit with status code 0."""

    if abort:
        print "Error: Could not generate ChangeLog.txt: " + reason
        exit(1)
    else:
        print "Warning: Could not generate ChangeLog.txt: " + reason
        exit(0)
        


def main():
    commits = sys.argv[1:]
    if not commits:
        commits = ["HEAD"]


    # Set PATH to help find the git executable on Mac OS X.
    if sys.platform == "darwin":
        os.environ["PATH"] += ":/usr/local/bin:/usr/local/git/bin:/sw/bin:/opt/bin:~/bin"


    # Fetch the log entries from git in one big chunk.
    cmd = ["git", "log", "--pretty=fuller", "--name-status",
           "--date=short", "--date-order"] + commits

    try:
        proc = subprocess.Popen( [" ".join(cmd)], 
                                cwd    = ROOT_DIR,
                                stdout = subprocess.PIPE,
                                stderr = subprocess.STDOUT,
                                shell  = True)
    except OSError:
        fail("The 'git' command is not available.")
    
    output = proc.communicate()[0]
    status = proc.returncode


    # If the git command failed, print the reason and exit.
    if status != 0:
        fail(output)

    # Split it up into individual commits.
    logs = re.compile("^commit ", re.MULTILINE).split(output)[1:]


    # Introductory header that goes at the top of the ChangeLog.
    header="""

                                  CHANGELOG
                           for the Imprudence Viewer
                          http://imprudenceviewer.org


     This is a log of revisions to the Imprudence Viewer source code.
     If you are looking for an overview of new features and changes in
     each release, please see RELEASE_NOTES.txt instead.

     This file is automatically generated from the Git commit history.
     Be aware that it is NOT ORDERED BY DATE, but rather by ancestry.
     Some commits have been omitted from this list for brevity.

     File changes are annotated as follows:

       A  =  the file was added
       M  =  the file was modified
       D  =  the file was deleted

     For a full history of the source code, including diffs, please see
     the Git repository.


"""

    output = [header]

    for log in logs:
        entry = LogEntry(log)
        if entry.should_be_included():
            output.append( entry.format() )

    text = "\n".join(output)

    changelog = open(CHANGELOG, "w")
    changelog.write(text)
    changelog.close()



if __name__ == "__main__":
    main()