As promised on IRC last Friday, here is a POC implementation of the "generate changelog from commit messages" functionality, added to release.py (I'm not very experienced in Python; I mainly depend on google, SO, copy-paste, ... so don't focus on coding style etc).
This patch is intended to enable further discussion of this idea (which was already discussed on this list 4 years ago [1]). The idea is that we agree on some structured syntax for adding (optionally) a changelog text to commit messages, to make it easier for RMs to generate the text for CHANGES (and push the responsibility for writing a good CHANGES entry first and foremost to the original committer; and keep the relevant info coupled with the commit). RMs can use the output of write-changelog as a draft, they can still edit it, summarize items, shuffle things around, ... But it gives them a rough version to start from. Proposal for changelog syntax in commit messages (as implemented in this patch): [[[ Changelog lines are lines with the following format: '['<visibility>:<section>']' <message> where <visibility> = U (User-visible) or D (Developer-visible) <section> = general|major|minor|client|server|client-server|other|api|bindings (section is treated case-insensitively) <message> = the actual text for CHANGES Examples: [U:major] Better interactive conflict resolution for tree conflicts [U:minor] ra_serf: Adjustments for serf versions with HTTP/2 support [U:client] Fix 'svn diff URL@REV WC' wrongly looks up URL@HEAD (issue #4597) [U:client-server] Fix bug with canonicalizing Window-specific drive-relative URL [D:api] New svn_ra_list() API function [D:bindings] JavaHL: Allow access to constructors of a couple JavaHL classes Q: Shorter prefix syntax ([U:C], [U:CS], [U:MJ], ...) to keep lines short, or longer (and put message on next line) to make it more human-readable? While making it easily rememberable for devs so they don't have to look it up all the time when they just want to commit ... Q: What to do with merged revisions? Use 'log -g', or make sure relevant changelog entry is part of the commit message of the merge? I vote for the latter, it's simpler and has less surprises. In case of feature branches, a generic "Add feature foo" message on the reintegrate commit usually suffices. In case of backports perhaps our backport script can scrape the relevant changelog entries from the revisions-to-be-merged and add them to the commit message of the merge. ]]] To get a rough idea: since we don't have any commit messages containing such lines in our history, I've added a --pocfirstlines option, which just takes the first line of every log message (ignoring lines with 'STATUS', 'CHANGES', 'bump', or starting with '*'), putting them in the "User -> General" section. Here is the usage output: [[[ $ ./release.py write-changelog -h usage: release.py write-changelog [-h] [--pocfirstlines] branch previous positional arguments: branch The branch (or tag or trunk), relative to ^/subversion/, of which to generate the changelog, when compared to "previous". previous The "previous" branch or tag, relative to ^/subversion/, to compare "branch" against. optional arguments: -h, --help show this help message and exit --pocfirstlines Proof of concept: just take the first line of each relevant commit messages (except if it contains 'STATUS', 'CHANGES' or 'bump' or starts with '*'), and put it in User:General. ]]] Example output: [[[ $ ./release.py write-changelog --pocfirstlines branches/1.9.x tags/1.9.7 User-visible changes: - General: * Merge r1804691 and r1804692 from trunk: (r1804698) - Client-side bugfixes: (none) - Server-side bugfixes: (none) - Bindings bugfixes: (none) Developer-visible changes: - General: (none) - API changes: (none) $ ./release.py write-changelog --pocfirstlines branches/1.8.x tags/1.8.19 User-visible changes: - General: * Merge r1804691 from trunk: (r1804723) * On the 1.8.x branch: Merge r1804692 from trunk. (r1804737) - Client-side bugfixes: (none) - Server-side bugfixes: (none) - Bindings bugfixes: (none) Developer-visible changes: - General: (none) - API changes: (none) $ ./release.py write-changelog --pocfirstlines trunk tags/1.9.7 User-visible changes: - General: * A bug fix and minor tweaks in 'svnmover'. (r1715781) * A cosmetic tweak: add a final comma to lists of tests in a few test files (r1706965) * A few FSFS-only tests apply to FSX just as well. So, run them for (r1667524) * A follow-up to r1715354. (r1715358) * A minor code cleanup in FSFS. (r1740722) * A minor tweak in 'svnmover'. (r1717793) * A small step towards making 'svnmover merge' operate into a new temporary (r1717957) * Abbreviate the potentially rather long list of revisions shown for tree (r1736063) * Actually use some helpful error messaging that we bother to create in (r1683161) * Add "merge_" prefix to the names of conflict resolver merge test sandboxes. (r1749457) * Add '--include' and '--exclude' options to 'svnadmin dump'. (r1811992) * Add '--search' option support to 'svnbench null-list'. (r1767202) * Add 'http-compression=auto' mode on the client, now used by default. (r1803899) ... ]]] Thoughts? [1] https://lists.apache.org/thread.html/c80dd19a7bbafc4f535382b3f361f76ba6535ab3d447a8b988594bfc@1377814810@%3Cdev.subversion.apache.org%3E -- Johan
Index: release.py =================================================================== --- release.py (revision 1817073) +++ release.py (working copy) @@ -1174,6 +1174,124 @@ fd.seek(0) subprocess.check_call(['gpg', '--import'], stdin=fd) +def add_to_changes_dict(changes_dict, section, change, revision): + if section in changes_dict: + changes = changes_dict[section] + if change in changes: + revset = changes[change] + revset.add(revision) + else: + changes[change] = set([revision]) + else: + changes_dict[section] = dict() + changes_dict[section][change] = set([revision]) + +def print_section(changes_dict, section, title, mandatory=False): + if mandatory or (section in changes_dict): + print(' - %s:' % title) + + if section in changes_dict: + print_changes(changes_dict[section]) + elif mandatory: + print(' (none)') + +def print_changes(changes): + # Print in alphabetical order, so entries with the same prefix are together + for change in sorted(changes): + revs = changes[change] + rev_string = 'r' + str(min(revs)) + (' et al' if len(revs) > 1 else '') + print(' * %s (%s)' % (change, rev_string)) + +def write_changelog(args): + 'Write changelog, parsed from commit messages' + branch = secure_repos + '/' + args.branch + previous = secure_repos + '/' + args.previous + poc = args.pocfirstlines + + mergeinfo = subprocess.check_output(['svn', 'mergeinfo', '--show-revs', + 'eligible', '--log', branch, previous]).splitlines() + + separator_pattern = re.compile('^-{72}$') + revline_pattern = re.compile('^r(\d+) \| \w+ \| [^\|]+ \| \d+ lines?$') + # Changelog lines are lines with the following format: + # '['<visibility>:<section>']' <message> + # where <visibility> = U (User-visible) or D (Developer-visible) + # <section> = general|major|minor|client|server|client-server|other|api|bindings + # (section is treated case-insensitively) + # <message> = the actual text for CHANGES + # + # Examples: + # [U:major] Better interactive conflict resolution for tree conflicts + # [U:minor] ra_serf: Adjustments for serf versions with HTTP/2 support + # [U:client] Fix 'svn diff URL@REV WC' wrongly looks up URL@HEAD (issue #4597) + # [U:client-server] Fix bug with canonicalizing Window-specific drive-relative URL + # [D:api] New svn_ra_list() API function + # [D:bindings] JavaHL: Allow access to constructors of a couple JavaHL classes + # + ### TODO: Support continuation of changelog message on multiple lines + ### TODO: Shorter prefix syntax ([U:C], [U:CS], [U:MJ], ...) to keep lines short, + ### or longer (and put message on next line) to make it more human-readable? + ### While making it easily rememberable for devs so they don't have to look + ### it up all the time when they just want to commit ... + changelog_pattern = re.compile('^\[(U|D):([^\]]+)\](.*)$') + + user_changes = dict() # section -> (change -> set(revision)) + dev_changes = dict() # section -> (change -> set(revision)) + revision = -1 + poc_get_nextline = False + + for line in mergeinfo: + if separator_pattern.match(line): + revision = -1 + continue + + if line == '': + continue + + if poc_get_nextline: + poc_get_nextline = False + if not re.search('status|changes|bump|^\*', line, re.IGNORECASE): + add_to_changes_dict(user_changes, 'general', line, revision) + continue + + revmatch = revline_pattern.match(line) + if revmatch != None and revision == -1: + # A revision line: get the revision number; reset changelog_lines + revision = int(revmatch.group(1)) + logging.debug('Changelog processing revision r%d' % revision) + if poc: + poc_get_nextline = True + continue + + logmatch = changelog_pattern.match(line) + if logmatch != None: + # A changelog line: get visibility, section and rest of the line. + visibility = logmatch.group(1).upper() + section = logmatch.group(2).lower() + change = logmatch.group(3).strip() + if visibility == 'U': + add_to_changes_dict(user_changes, section, change, revision) + if visibility == 'D': + add_to_changes_dict(dev_changes, section, change, revision) + + # Output the sorted changelog entries + # 1) User-visible changes + print(' User-visible changes:') + print_section(user_changes, 'general', 'General') + print_section(user_changes, 'major', 'Major new features') + print_section(user_changes, 'minor', 'Minor new features and improvements') + print_section(user_changes, 'client', 'Client-side bugfixes', mandatory=True) + print_section(user_changes, 'server', 'Server-side bugfixes', mandatory=True) + print_section(user_changes, 'client-server', 'Client-side and server-side bugfixes') + print_section(user_changes, 'other', 'Other tool improvements and bugfixes') + print_section(user_changes, 'bindings', 'Bindings bugfixes', mandatory=True) + print + # 2) Developer-visible changes + print(' Developer-visible changes:') + print_section(dev_changes, 'general', 'General', mandatory=True) + print_section(dev_changes, 'api', 'API changes', mandatory=True) + print_section(dev_changes, 'bindings', 'Bindings') + #---------------------------------------------------------------------- # Main entry point for argument parsing and handling @@ -1338,6 +1456,24 @@ separate subcommand.''') subparser.set_defaults(func=cleanup) + # write-changelog + subparser = subparsers.add_parser('write-changelog', + help='''Output to stdout changelog entries parsed from + commit messages.''') + subparser.set_defaults(func=write_changelog) + subparser.add_argument('branch', + help='''The branch (or tag or trunk), relative to + ^/subversion/, of which to generate the + changelog, when compared to "previous".''') + subparser.add_argument('previous', + help='''The "previous" branch or tag, relative to + ^/subversion/, to compare "branch" against.''') + subparser.add_argument('--pocfirstlines', action='store_true', default=False, + help='''Proof of concept: just take the first line of + each relevant commit messages (except if it + contains 'STATUS', 'CHANGES' or 'bump' or starts + with '*'), and put it in User:General.''') + # Parse the arguments args = parser.parse_args()