summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRob Browning <rlb@defaultvalue.org>2010-12-06 08:30:22 +0100
committerGuido Günther <agx@sigxcpu.org>2010-12-06 09:17:04 +0100
commit3640569c90a3326aa8b882fa72d63ed881785de4 (patch)
treede2c96284fadad8e87b4c258202b68a526d7b905
parente8757040c20697c7cce1180127e257f9f17ab713 (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.py1
-rw-r--r--gbp/dch.py116
-rw-r--r--gbp/git.py16
-rwxr-xr-xgit-dch143
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
diff --git a/gbp/git.py b/gbp/git.py
index 3e929ca6..ff0ea79a 100644
--- a/gbp/git.py
+++ b/gbp/git.py
@@ -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' ]
diff --git a/git-dch b/git-dch
index 1688a0af..4ec8a413 100755
--- a/git-dch
+++ b/git-dch
@@ -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,