diff options
author | Rob Browning <rlb@defaultvalue.org> | 2010-12-06 08:30:22 +0100 |
---|---|---|
committer | Guido Günther <agx@sigxcpu.org> | 2010-12-06 09:17:04 +0100 |
commit | 3640569c90a3326aa8b882fa72d63ed881785de4 (patch) | |
tree | de2c96284fadad8e87b4c258202b68a526d7b905 | |
parent | e8757040c20697c7cce1180127e257f9f17ab713 (diff) |
Add git-dch --customizations FILE to allow changelog entry customization
Add support for git-dch --customizations FILE. FILE must be Python
code, and for now, the only useful thing it can do is define a
format_changelog_entry() function which will override
gbp.dch.format_changelog_entry().
Add a new customization option group for --customizations.
Create a gbp.dch module and move the changelog entry formatting
functions there. Create separate procedures to handle extracting
metadata from the git log, and use them in the default
format_changelog_entry(). These functions are also available for use
by custom formatters: extract_git_dch_cmds(),
filter_ignore_rx_matches(), extract_bts_cmds(), extract_thanks_info(),
etc.
Add a GitRepository.get_commit_info() method, and use it in git-dch
parse_commit().
Signed-off-by: Rob Browning <rlb@defaultvalue.org>
-rw-r--r-- | gbp/config.py | 1 | ||||
-rw-r--r-- | gbp/dch.py | 116 | ||||
-rw-r--r-- | gbp/git.py | 16 | ||||
-rwxr-xr-x | git-dch | 143 |
4 files changed, 180 insertions, 96 deletions
diff --git a/gbp/config.py b/gbp/config.py index aced5b1d..ff75a19f 100644 --- a/gbp/config.py +++ b/gbp/config.py @@ -79,6 +79,7 @@ class GbpOptionParser(OptionParser): 'arch' : '', 'interactive' : 'True', 'color' : 'auto', + 'customizations' : '' } help = { 'debian-branch': diff --git a/gbp/dch.py b/gbp/dch.py new file mode 100644 index 00000000..8ce8f2bc --- /dev/null +++ b/gbp/dch.py @@ -0,0 +1,116 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2010 Rob Browning <rlb@defaultvalue.org> +"""provides git-dch helpers""" + +import re + + +def extract_git_dch_cmds(lines, options): + """Return a dictionary of all Git-Dch: commands found in lines. + The command keys will be lowercased, i.e. {'ignore' : True, + 'short': True}. For now, all the options are binary. Also return + all of the lines that do not contain Git-Dch: commands.""" + commands = {} + other_lines = [] + for line in lines: + if line.startswith('Git-Dch: '): + cmd = line.split(' ', 1)[1].strip().lower() + commands[cmd] = True + else: + other_lines.append(line) + return (commands, other_lines) + + +def filter_ignore_rx_matches(lines, options): + """Filter any lines that match options.ignore_regex + (i.e. --ignore-regex).""" + if options.ignore_regex: + ignore_re = re.compile(options.ignore_regex) + return [line for line in lines if not ignore_re.match(line)] + else: + return lines + + +_bug_r = r'(?:bug|issue)?\#?\s?\d+' +_bug_re = re.compile(_bug_r, re.I) + +def extract_bts_cmds(lines, opts): + """Return a dictionary of the bug tracking system commands + contained in the the given lines. i.e. {'closed' : [1], 'fixed': + [3, 4]}. Right now, this will only notice a single directive + clause on a line. Also return all of the lines that do not + contain bug tracking system commands.""" + bts_rx = re.compile(r'(?P<bts>%s):\s+%s' % (opts.meta_closes, _bug_r), re.I) + commands = {} + other_lines = [] + for line in lines: + m = bts_rx.match(line) + if m: + bug_nums = [ bug.strip() for bug in _bug_re.findall(line, re.I) ] + try: + commands[m.group('bts')] += bug_nums + except KeyError: + commands[m.group('bts')] = bug_nums + else: + other_lines.append(line) + return (commands, other_lines) + + +def extract_thanks_info(lines, options): + """Return a list of all of the Thanks: entries, and a list of all + of the lines that do not contain Thanks: entries.""" + thanks = [] + other_lines = [] + for line in lines: + if line.startswith('Thanks: '): + thanks.append(line.split(' ', 1)[1].strip()) + else: + other_lines.append(line) + return (thanks, other_lines) + + +def _ispunct(ch): + return not ch.isalnum() and not ch.isspace() + + +def terminate_first_line_if_needed(lines): + """Terminate the first line of lines with a '.' if multi-line.""" + # Don't add a period to empty or one line commit messages. + if len(lines) < 2: + return lines + if lines[0] and _ispunct(lines[0][-1]): + return lines + if lines[1] and (_ispunct(lines[1][0]) or lines[1][0].islower()): + return lines + return [lines[0] + "."] + lines[1:] + + +def format_changelog_entry(commit_info, options, last_commit=False): + """Return a list of lines (without newlines) as the changelog + entry for commit_info (generated by + GitRepository.get_commit_info()). If last_commit is not False, + then this entry is the last one in the series.""" + entry = [commit_info['subject']] + body = commit_info['body'] + (git_dch_cmds, body) = extract_git_dch_cmds(body, options) + if 'ignore' in git_dch_cmds: + return None + (bts_cmds, body) = extract_bts_cmds(body, options) + (thanks, body) = extract_thanks_info(body, options) + body = filter_ignore_rx_matches(body, options) + if options.full and not 'short' in git_dch_cmds: + # Add all non-blank body lines. + entry.extend([line for line in body if line.strip()]) + for bts in bts_cmds: + print bts_cmds + entry[-1] += '(%s: %s) ' % (bts, ', '.join(bts_cmds[bts])) + if thanks: + # Last wins for now (match old behavior). + entry[-1] += '- thanks to %s' % thanks[-1] + if options.idlen: + entry[0] = '[%s] ' % commitid[0:options.idlen] + entry[0] + entry = terminate_first_line_if_needed(entry) + if not last_commit: + entry += '' + return entry @@ -282,6 +282,22 @@ class GitRepository(object): raise GitRepositoryError, "Error getting subject of commit %s" % commit return out[0].strip() + def get_commit_info(self, commit): + """Given a commit name, return a dictionary of its components, + including id, author, email, subject, and body.""" + self.__check_path() + out, ret = self.__git_getoutput('log', + ['--pretty=format:%an%n%ae%n%s%n%b%n', + '-n1', commit]) + if ret: + raise GitRepositoryError, "Unable to retrieve log entry for %s" \ + % commit + return {'id' : commit, + 'author' : out[0].strip(), + 'email' : out[1].strip(), + 'subject' : out[2].rstrip(), + 'body' : [line.rstrip() for line in out[3:]]} + def find_tag(self, commit, pattern=None): "find the closest tag to a branch's head" args = [ '--abbrev=0' ] @@ -25,6 +25,7 @@ import sys import shutil import subprocess import gbp.command_wrappers as gbpc +import gbp.dch as dch import gbp.log from gbp.git import (GitRepositoryError, GitRepository, build_tag, tag_to_version) from gbp.config import GbpOptionParser, GbpOptionGroup @@ -32,10 +33,9 @@ from gbp.errors import GbpError from gbp.deb import parse_changelog, NoChangelogError, is_native, compare_versions from gbp.command_wrappers import (Command, CommandExecFailed) +user_customizations = {} snapshot_re = re.compile("\s*\*\* SNAPSHOT build @(?P<commit>[a-z0-9]+)\s+\*\*") -author_re = re.compile('Author: (?P<author>.*) <(?P<email>.*)>') -bug_r = r'(?:bug|issue)?\#?\s?\d+' -bug_re = re.compile(bug_r, re.I) + def system(cmd): try: @@ -44,29 +44,7 @@ def system(cmd): raise GbpError -def escape_commit(msg): - return msg.replace('"','\\\"').replace("$","\$").replace("`","\`") - -def ispunct(ch): - return not ch.isalnum() and not ch.isspace() - -def punctuate_commit(msg): - """Terminate the first line of a commit message with a '.' if it's a - multiline commit""" - lines = msg.split('\n', 1) - if len(lines) <= 1: - return msg - first, rest = lines - # Don't add a dot on one line commit messages - if not rest: - return msg - if first and ispunct(first[-1]): - return msg - if ispunct(rest[0]) or rest[0].islower(): - return msg - return "%s.\n%s" % (first, rest) - -def spawn_dch(msg='', author=None, email=None, newversion=False, version=None, +def spawn_dch(msg=[], author=None, email=None, newversion=False, version=None, release=False, distribution=None, dch_options=''): """ Spawn dch @@ -94,9 +72,6 @@ def spawn_dch(msg='', author=None, email=None, newversion=False, version=None, versionopt = "--release --no-force-save-on-release" msg = None - if msg: - msg = punctuate_commit(msg) - if author and email: env = """DEBFULLNAME="%s" DEBEMAIL="%s" """ % (author, email) @@ -104,9 +79,22 @@ def spawn_dch(msg='', author=None, email=None, newversion=False, version=None, distopt = "--distribution=%s" % distribution cmd = '%(env)s dch --no-auto-nmu %(distopt)s %(versionopt)s %(dch_options)s ' % locals() - if type(msg) == type(''): - cmd += '-- "%s"' % escape_commit(msg) + if msg: + cmd += '-- "[[[insert-git-dch-commit-message-here]]]"' + else: + cmd += '-- ""' system(cmd) + if msg: + old_cl = open("debian/changelog", "r") + new_cl = open("debian/changelog.bak", "w") + for line in old_cl: + if line == " * [[[insert-git-dch-commit-message-here]]]\n": + print >> new_cl, " * " + msg[0] + for line in msg[1:]: + print >> new_cl, " " + line + else: + print >> new_cl, line, + os.rename("debian/changelog.bak", "debian/changelog") def add_changelog_entry(msg, author, email, dch_options): @@ -242,69 +230,16 @@ def do_snapshot(changelog, repo, next_snapshot): return snapshot, commit -def get_author(commit): - """get the author from a commit message""" - for line in commit: - m = author_re.match(line) - if m: - return m.group('author'), m.group('email') - - -def parse_commit(repo, commitid, options): - """parse a commit and return message and author""" - msg = '' - thanks = '' - closes = '' - git_dch = '' - bugs = {} - bts_closes = re.compile(r'(?P<bts>%s):\s+%s' % (options.meta_closes, bug_r), re.I) - - if options.ignore_regex: # Ignore r'' since it matches everything - ignore_re = re.compile(options.ignore_regex) - else: - ignore_re = None - - commit = repo.show(commitid) - author, email = get_author(commit) - if not author: - raise GbpError, "can't parse author of commit %s" % commit - for line in commit: - if line.startswith(' '): # commit body - line = line[4:] - m = bts_closes.match(line) - if m: - bug_nums = [ bug.strip() for bug in bug_re.findall(line, re.I) ] - try: - bugs[m.group('bts')] += bug_nums - except KeyError: - bugs[m.group('bts')] = bug_nums - elif line.startswith('Thanks: '): - thanks = line.split(' ', 1)[1].strip() - elif line.startswith('Git-Dch: '): - git_dch = line.split(' ', 1)[1].strip() - else: # normal commit message - if msg and not options.full: - continue - if ignore_re and ignore_re.match(line): - continue - if line.strip(): # don't add all whitespace lines - msg += line - # start of diff output: - elif line.startswith('diff '): - break - if options.meta: - if git_dch == 'Ignore': - return None - if git_dch == 'Short': - msg = msg.split('\n')[0] - for bts in bugs: - closes += '(%s: %s) ' % (bts, ', '.join(bugs[bts])) - if thanks: - thanks = '- thanks to %s' % thanks - msg += closes + thanks - if options.idlen: - msg = "[%s] " % commitid[0:options.idlen] + msg - return msg, (author, email) +def parse_commit(repo, commitid, opts, last_commit=False): + """parse a commit and return message, author, and author email""" + commit_info = repo.get_commit_info(commitid) + author = commit_info['author'] + email = commit_info['email'] + format_entry = user_customizations.get('format_changelog_entry') + if not format_entry: + format_entry = dch.format_changelog_entry + entry = format_entry(commit_info, opts, last_commit=last_commit) + return entry, (author, email) def guess_snapshot_commit(cp, repo, options): @@ -347,10 +282,12 @@ def main(argv): version_group = GbpOptionGroup(parser, "release & version number options", "what version number and release to use") commit_group = GbpOptionGroup(parser, "commit message formatting", "howto format the changelog entries") naming_group = GbpOptionGroup(parser, "branch and tag naming", "branch names and tag formats") + custom_group = GbpOptionGroup(parser, "customization", "options for customization") parser.add_option_group(range_group) parser.add_option_group(version_group) parser.add_option_group(commit_group) parser.add_option_group(naming_group) + parser.add_option_group(custom_group) parser.add_boolean_config_file_option(option_name = "ignore-branch", dest="ignore_branch") naming_group.add_config_file_option(option_name="debian-branch", dest="debian_branch") @@ -390,6 +327,13 @@ def main(argv): help="Ignore commit lines matching regex, default is '%(ignore-regex)s'") commit_group.add_boolean_config_file_option(option_name="multimaint-merge", dest="multimaint_merge") + help_msg = 'Load Python code from CUSTOMIZATION_FILE. At the moment,' \ + + ' the only useful thing the code can do is define a custom' \ + + ' format_changelog_entry() function.' + custom_group.add_config_file_option(option_name="customizations", + dest="customization_file", + help=help_msg) + (options, args) = parser.parse_args(argv[1:]) gbp.log.setup(options.color, options.verbose) @@ -404,6 +348,10 @@ def main(argv): else: dch_options = "--nomultimaint-merge" + if options.customization_file: + execfile(options.customization_file, + user_customizations, + user_customizations) try: try: repo = GitRepository('.') @@ -458,8 +406,11 @@ def main(argv): else: add_section = False + i = 0 for c in commits: - parsed = parse_commit(repo, c, options) + i += 1 + parsed = parse_commit(repo, c, options, + last_commit = i == len(commits)) if not parsed: # Some commits can be ignored continue @@ -490,7 +441,7 @@ def main(argv): if add_section: # If we end up here, then there were no commits to include, # so we put a dummy message in the new section. - add_changelog_section(distribution="UNRELEASED", msg="UNRELEASED", + add_changelog_section(distribution="UNRELEASED", msg=["UNRELEASED"], version=version_change, dch_options=dch_options, repo=repo, |