diff options
author | Guido Günther <agx@sigxcpu.org> | 2011-10-30 20:03:33 +0100 |
---|---|---|
committer | Guido Günther <agx@sigxcpu.org> | 2011-11-01 18:12:48 +0100 |
commit | e28ea0740a7b4eb2ef4c1bd3079d77a40c6072b8 (patch) | |
tree | 779145e71a996c471a1d1a6b1acde1bc90deecc4 /gbp/scripts/dch.py | |
parent | abf90abcba15beb51196cf503f35695acdcd91c1 (diff) | |
download | git-buildpackage-e28ea0740a7b4eb2ef4c1bd3079d77a40c6072b8.tar.gz git-buildpackage-e28ea0740a7b4eb2ef4c1bd3079d77a40c6072b8.tar.bz2 git-buildpackage-e28ea0740a7b4eb2ef4c1bd3079d77a40c6072b8.zip |
Get rid of the symlink
by moving the commands to gbp/scripts/
Diffstat (limited to 'gbp/scripts/dch.py')
-rw-r--r-- | gbp/scripts/dch.py | 500 |
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\:·: |