aboutsummaryrefslogtreecommitdiffhomepage
path: root/gbp/scripts/dch.py
diff options
context:
space:
mode:
Diffstat (limited to 'gbp/scripts/dch.py')
-rw-r--r--gbp/scripts/dch.py500
1 files changed, 500 insertions, 0 deletions
diff --git a/gbp/scripts/dch.py b/gbp/scripts/dch.py
new file mode 100644
index 00000000..a82c1bdc
--- /dev/null
+++ b/gbp/scripts/dch.py
@@ -0,0 +1,500 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2007, 2008, 2009, 2010 Guido Guenther <agx@sigxcpu.org>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+"""Generate Debian changelog entries from git commit messages"""
+
+import ConfigParser
+import os.path
+import re
+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
+from gbp.errors import GbpError
+from gbp.deb import parse_changelog, NoChangelogError, is_native, compare_versions
+
+user_customizations = {}
+snapshot_re = re.compile("\s*\*\* SNAPSHOT build @(?P<commit>[a-z0-9]+)\s+\*\*")
+
+
+def system(cmd):
+ try:
+ gbpc.Command(cmd, shell=True)()
+ except gbpc.CommandExecFailed:
+ raise GbpError
+
+
+def spawn_dch(msg=[], author=None, email=None, newversion=False, version=None,
+ release=False, distribution=None, dch_options=''):
+ """
+ Spawn dch
+ param author: committers name
+ param email: committers email
+ param newversion: start a new version
+ version: the verion to use
+ release: finalize changelog for releaze
+ distribution: distribution to use
+ dch_options: options passed verbatim to dch
+ """
+ distopt = ""
+ versionopt = ""
+ env = ""
+
+ if newversion:
+ if version:
+ try:
+ versionopt = version['increment']
+ except KeyError:
+ versionopt = '--newversion=%s' % version['version']
+ else:
+ versionopt = '-i'
+ elif release:
+ versionopt = "--release --no-force-save-on-release"
+ msg = None
+
+ if author and email:
+ env = """DEBFULLNAME="%s" DEBEMAIL="%s" """ % (author, email)
+
+ if distribution:
+ distopt = "--distribution=%s" % distribution
+
+ cmd = '%(env)s dch --no-auto-nmu %(distopt)s %(versionopt)s %(dch_options)s ' % locals()
+ 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):
+ "add a single changelog entry"
+ spawn_dch(msg=msg, author=author, email=email, dch_options=dch_options)
+
+
+def add_changelog_section(msg, distribution, repo, options, cp,
+ author=None, email=None, version=None, dch_options=''):
+ "add a new changelog section"
+ # If no version(change) was specified guess the new version based on the
+ # latest upstream version on the upstream branch
+ if not version and not is_native(cp):
+ pattern = options.upstream_tag.replace('%(version)s', '*')
+ try:
+ tag = repo.find_tag('HEAD', pattern=pattern)
+ upstream = tag_to_version(tag, options.upstream_tag)
+ if upstream:
+ gbp.log.debug("Found %s." % upstream)
+ new_version = "%s-1" % upstream
+ if compare_versions(upstream, cp['Version']) > 0:
+ version['version'] = new_version
+ except GitRepositoryError:
+ gbp.log.debug("No tag found matching pattern %s." % pattern)
+ spawn_dch(msg=msg, newversion=True, version=version, author=author, email=email,
+ distribution=distribution, dch_options=dch_options)
+
+
+def get_author_email(repo, use_git_config):
+ """Get author and email from git configuration"""
+ author = email = None
+
+ if use_git_config:
+ try: author = repo.get_config('user.name')
+ except KeyError: pass
+
+ try: email = repo.get_config('user.email')
+ except KeyError: pass
+ return author, email
+
+
+def fixup_trailer(repo, git_author, dch_options):
+ """fixup the changelog trailer's comitter and email address - it might
+ otherwise point to the last git committer instead of the person creating
+ the changelog"""
+ author, email = get_author_email(repo, git_author)
+ spawn_dch(msg='', author=author, email=email, dch_options=dch_options)
+
+
+def snapshot_version(version):
+ """
+ get the current release and snapshot version
+ Format is <debian-version>~<release>.gbp<short-commit-id>
+ """
+ try:
+ (release, suffix) = version.rsplit('~', 1)
+ (snapshot, commit) = suffix.split('.', 1)
+ if not commit.startswith('gbp'):
+ raise ValueError
+ else:
+ snapshot = int(snapshot)
+ except ValueError: # not a snapshot release
+ release = version
+ snapshot = 0
+ return release, snapshot
+
+
+def mangle_changelog(changelog, cp, snapshot=''):
+ """
+ Mangle changelog to either add or remove snapshot markers
+
+ @param snapshot: SHA1 if snapshot header should be added/maintained, empty if it should be removed
+ @type snapshot: str
+ """
+ try:
+ tmpfile = '%s.%s' % (changelog, snapshot)
+ cw = file(tmpfile, 'w')
+ cr = file(changelog, 'r')
+
+ cr.readline() # skip version and empty line
+ cr.readline()
+ print >>cw, "%(Source)s (%(MangledVersion)s) %(Distribution)s; urgency=%(urgency)s\n" % cp
+
+ line = cr.readline()
+ if snapshot_re.match(line):
+ cr.readline() # consume the empty line after the snapshot header
+ line = ''
+
+ if snapshot:
+ print >>cw, " ** SNAPSHOT build @%s **\n" % snapshot
+
+ if line:
+ print >>cw, line.rstrip()
+ shutil.copyfileobj(cr, cw)
+ cw.close()
+ cr.close()
+ os.unlink(changelog)
+ os.rename(tmpfile, changelog)
+ except OSError, e:
+ raise GbpError, "Error mangling changelog %s" % e
+
+
+def do_release(changelog, repo, cp, git_author, dch_options):
+ "remove the snapshot header and set the distribution"
+ author, email = get_author_email(repo, git_author)
+ (release, snapshot) = snapshot_version(cp['Version'])
+ if snapshot:
+ cp['MangledVersion'] = release
+ mangle_changelog(changelog, cp)
+ spawn_dch(release=True, author=author, email=email, dch_options=dch_options)
+
+
+def do_snapshot(changelog, repo, next_snapshot):
+ """
+ Add new snapshot banner to most recent changelog section. The next snapshot
+ number is calculated by eval()'ing next_snapshot
+ """
+ commit = repo.head
+
+ cp = parse_changelog(filename=changelog)
+ (release, snapshot) = snapshot_version(cp['Version'])
+ snapshot = int(eval(next_snapshot))
+
+ suffix = "%d.gbp%s" % (snapshot, "".join(commit[0:6]))
+ cp['MangledVersion'] = "%s~%s" % (release, suffix)
+
+ mangle_changelog(changelog, cp, commit)
+ return snapshot, commit
+
+
+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):
+ """
+ guess the last commit documented in the changelog from the snapshot banner
+ or the last point the changelog was touched.
+ """
+ sr = re.search(snapshot_re, cp['Changes'])
+ if sr:
+ return sr.group('commit')
+ # If the current topmost changelog entry has already been tagged rely on
+ # the version information only. The upper level relies then on the version
+ # info anyway:
+ if repo.find_version(options.debian_tag, cp['Version']):
+ return None
+ # If we didn't find a snapshot header we look at the point the changelog
+ # was last touched.
+ last = repo.get_commits(paths="debian/changelog", options=["-1"])
+ if last:
+ gbp.log.info("Changelog last touched at '%s'" % last[0])
+ return last[0]
+ return None
+
+
+def get_customizations(customization_file):
+ if customization_file:
+ execfile(customization_file,
+ user_customizations,
+ user_customizations)
+
+
+def process_options(options, parser):
+ if options.snapshot and options.release:
+ parser.error("'--snapshot' and '--release' are incompatible options")
+
+ if options.since and options.auto:
+ parser.error("'--since' and '--auto' are incompatible options")
+
+ if options.multimaint_merge:
+ dch_options = "--multimaint-merge"
+ else:
+ dch_options = "--nomultimaint-merge"
+
+ if options.multimaint:
+ dch_options += " --multimaint"
+ else:
+ dch_options += " --nomultimaint"
+
+ get_customizations(options.customization_file)
+ return dch_options
+
+
+def process_editor_option(options):
+ # Determine Editor and check if we need it
+ states = ['always']
+
+ if options.snapshot:
+ states.append("snapshot")
+ elif options.release:
+ states.append("release")
+
+ if options.spawn_editor in states:
+ return "sensible-editor"
+ else:
+ return None
+
+
+def main(argv):
+ ret = 0
+ changelog = 'debian/changelog'
+ until = 'HEAD'
+ found_snapshot_header = False
+ version_change = {}
+
+ try:
+ parser = GbpOptionParser(command=os.path.basename(argv[0]), prefix='',
+ usage='%prog [options] paths')
+ except ConfigParser.ParsingError, err:
+ gbp.log.errror(err)
+ return 1
+ range_group = GbpOptionGroup(parser, "commit range options", "which commits to add to the changelog")
+ 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")
+ naming_group.add_config_file_option(option_name="upstream-tag", dest="upstream_tag")
+ naming_group.add_config_file_option(option_name="debian-tag", dest="debian_tag")
+ naming_group.add_config_file_option(option_name="snapshot-number", dest="snapshot_number",
+ help="expression to determine the next snapshot number, default is '%(snapshot-number)s'")
+ parser.add_config_file_option(option_name="git-log", dest="git_log",
+ help="options to pass to git-log, default is '%(git-log)s'")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
+ help="verbose command execution")
+ parser.add_config_file_option(option_name="color", dest="color", type='tristate')
+ range_group.add_option("-s", "--since", dest="since", help="commit to start from (e.g. HEAD^^^, debian/0.4.3)")
+ range_group.add_option("-a", "--auto", action="store_true", dest="auto", default=False,
+ help="autocomplete changelog from last snapshot or tag")
+ version_group.add_option("-R", "--release", action="store_true", dest="release", default=False,
+ help="mark as release")
+ version_group.add_option("-S", "--snapshot", action="store_true", dest="snapshot", default=False,
+ help="mark as snapshot build")
+ version_group.add_option("-N", "--new-version", dest="new_version",
+ help="use this as base for the new version number")
+ version_group.add_option("--bpo", dest="bpo", action="store_true", default=False,
+ help="Increment the Debian release number for an upload to lenny-backports, and add a backport upload changelog comment.")
+ version_group.add_option("--nmu", dest="nmu", action="store_true", default=False,
+ help="Increment the Debian release number for a non-maintainer upload")
+ version_group.add_option("--qa", dest="qa", action="store_true", default=False,
+ help="Increment the Debian release number for a Debian QA Team upload, and add a QA upload changelog comment.")
+ version_group.add_boolean_config_file_option(option_name="git-author", dest="git_author")
+ commit_group.add_boolean_config_file_option(option_name="meta", dest="meta")
+ commit_group.add_config_file_option(option_name="meta-closes", dest="meta_closes",
+ help="Meta tags for the bts close commands, default is '%(meta-closes)s'")
+ commit_group.add_boolean_config_file_option(option_name="full", dest="full")
+ commit_group.add_config_file_option(option_name="id-length", dest="idlen",
+ help="include N digits of the commit id in the changelog entry, default is '%(id-length)s'",
+ type="int", metavar="N")
+ commit_group.add_config_file_option(option_name="ignore-regex", dest="ignore_regex",
+ help="Ignore commit lines matching regex, default is '%(ignore-regex)s'")
+ commit_group.add_boolean_config_file_option(option_name="multimaint", dest="multimaint")
+ commit_group.add_boolean_config_file_option(option_name="multimaint-merge", dest="multimaint_merge")
+ commit_group.add_config_file_option(option_name="spawn-editor", dest="spawn_editor")
+
+ 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)
+ dch_options = process_options(options, parser)
+ editor_cmd = process_editor_option(options)
+
+ try:
+ try:
+ repo = GitRepository('.')
+ except GitRepositoryError:
+ raise GbpError, "%s is not a git repository" % (os.path.abspath('.'))
+
+ branch = repo.get_branch()
+ if options.debian_branch != branch and not options.ignore_branch:
+ gbp.log.err("You are not on branch '%s' but on '%s'" % (options.debian_branch, branch))
+ raise GbpError, "Use --ignore-branch to ignore or --debian-branch to set the branch name."
+
+ cp = parse_changelog(filename=changelog)
+
+ if options.since:
+ since = options.since
+ else:
+ since = ''
+ if options.auto:
+ since = guess_snapshot_commit(cp, repo, options)
+ if since:
+ gbp.log.info("Continuing from commit '%s'" % since)
+ found_snapshot_header = True
+ else:
+ gbp.log.info("Couldn't find snapshot header, using version info")
+ if not since:
+ since = repo.find_version(options.debian_tag, cp['Version'])
+ if not since:
+ raise GbpError, "Version %s not found" % cp['Version']
+
+ if args:
+ gbp.log.info("Only looking for changes on '%s'" % " ".join(args))
+ commits = repo.get_commits(since=since, until=until,
+ paths=" ".join(args),
+ options=options.git_log.split(" "))
+ commits.reverse()
+
+ # add a new changelog section if:
+ if options.new_version or options.bpo or options.nmu or options.qa:
+ if options.bpo:
+ version_change['increment'] = '--bpo'
+ elif options.nmu:
+ version_change['increment'] = '--nmu'
+ elif options.qa:
+ version_change['increment'] = '--qa'
+ else:
+ version_change['version'] = options.new_version
+ # the user wants to force a new version
+ add_section = True
+ elif cp['Distribution'] != "UNRELEASED" and not found_snapshot_header and commits:
+ # the last version was a release and we have pending commits
+ add_section = True
+ elif options.snapshot and not found_snapshot_header:
+ # the user want to switch to snapshot mode
+ add_section = True
+ else:
+ add_section = False
+
+ i = 0
+ for c in commits:
+ i += 1
+ parsed = parse_commit(repo, c, options,
+ last_commit = i == len(commits))
+ commit_msg, (commit_author, commit_email) = parsed
+ if not commit_msg:
+ # Some commits can be ignored
+ continue
+
+ if add_section:
+ # Add a section containing just this message (we can't
+ # add an empty section with dch)
+ add_changelog_section(distribution="UNRELEASED", msg=commit_msg,
+ version=version_change,
+ author=commit_author,
+ email=commit_email,
+ dch_options=dch_options,
+ repo=repo,
+ options=options,
+ cp=cp)
+ # Adding a section only needs to happen once.
+ add_section = False
+ else:
+ add_changelog_entry(commit_msg, commit_author, commit_email, dch_options)
+
+
+ # Show a message if there were no commits (not even ignored
+ # commits).
+ if not commits:
+ gbp.log.info("No changes detected from %s to %s." % (since, until))
+
+ 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"],
+ version=version_change,
+ dch_options=dch_options,
+ repo=repo,
+ options=options,
+ cp=cp)
+
+ fixup_trailer(repo, git_author=options.git_author,
+ dch_options=dch_options)
+
+ if options.release:
+ do_release(changelog, repo, cp, git_author=options.git_author,
+ dch_options=dch_options)
+ elif options.snapshot:
+ (snap, version) = do_snapshot(changelog, repo, options.snapshot_number)
+ gbp.log.info("Changelog has been prepared for snapshot #%d at %s" % (snap, version))
+
+ if editor_cmd:
+ gbpc.Command(editor_cmd, ["debian/changelog"])()
+
+ except (GbpError, GitRepositoryError, NoChangelogError), err:
+ if len(err.__str__()):
+ gbp.log.err(err)
+ ret = 1
+ return ret
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
+
+# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: