#!/usr/bin/python # vim: set fileencoding=utf-8 : # # (C) 2007,2008 Guido Guenther # 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 changelogs""" import sys import re import os.path import shutil import subprocess import gbp.command_wrappers as gbpc from gbp.git_utils import (GitRepositoryError, GitRepository, build_tag) from gbp.config import GbpOptionParser, GbpOptionGroup from gbp.errors import GbpError from gbp.deb_utils import parse_changelog from gbp.command_wrappers import (Command, CommandExecFailed) snapshot_re = re.compile("\s*\*\* SNAPSHOT build @(?P[a-z0-9]+)\s+\*\*") author_re = re.compile('Author: (?P.*) <(?P.*)>') def system(cmd): try: Command(cmd, shell=True)() except CommandExecFailed: raise GbpError def escape_commit(msg): return msg.replace('"','\\\"').replace("$","\$").replace("`","\`") def add_changelog_entry(msg, author, email): "add aa single changelog entry" cmd = """DEBFULLNAME="%s" DEBEMAIL="%s" dch --no-auto-nmu --append -- "%s" """ % (author, email, escape_commit(msg)) system(cmd) def add_changelog_section(msg, distribution): "add a new changelog section" cmd = "dch --no-auto-nmu --distribution=%s -i %s" % (distribution, msg) system(cmd) def fixup_trailer(): """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""" cmd = "dch \"\"" system(cmd) def head_commit(): """get the full sha1 of the last commit on HEAD""" commit = subprocess.Popen([ 'git-log', 'HEAD^..' ], stdout=subprocess.PIPE).stdout sha = commit.readline().split()[-1] return sha def snapshot_version(version): """ get the current release and snapshot version Format is ~.gbp """ 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, sha='unknown'): """Mangle changelog to either add or remove snapshot markers""" try: tmp = '%s.%s' % (changelog, str(snapshot)) cw = file(tmp, '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 line = '' if snapshot: print >>cw, " ** SNAPSHOT build @%s **\n" % sha if line: print >>cw, line.rstrip() shutil.copyfileobj(cr, cw) cw.close() cr.close() os.unlink(changelog) os.rename(tmp, changelog) except OSError, e: raise GbpError, "Error mangling changelog %s" % e def do_release(changelog, cp): "remove the snapshot header and set the distribution" (release, snapshot) = snapshot_version(cp['Version']) if snapshot: cp['MangledVersion'] = release mangle_changelog(changelog, cp, 0) cmd = "dch --release" system(cmd) def do_snapshot(changelog, next_snapshot): """ Add new snapshot banner to most recent changelog section. The next snapshot number is calculated by eval()'ing next_snapshot """ commit = head_commit() cp = parse_changelog(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, snapshot, commit) 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 = '' bugs = [] 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:] if line.startswith('Closes: '): bugs += [ line.split(' ', 1)[1].strip() ] elif line.startswith('Thanks: '): thanks = line.split(' ', 1)[1].strip() else: # normal commit message if options.short and msg: continue elif line.strip(): # don't add all whitespace lines msg += line # start of diff output: elif line.startswith('diff '): break if options.meta: if bugs: closes = '(Closes: %s)' % ', '.join(bugs) 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 shortlog_to_dch(repo, commits, options): """convert the changes in git shortlog format to debian changelog format""" author = 'Unknown' for commit in commits: msg, (author, email) = parse_commit(repo, commit, options) add_changelog_entry(msg, author, email) def guess_snapshot_commit(cp): """guess the last commit documented in the changelog from the snapshot banner""" sr = re.search(snapshot_re, cp['Changes']) if sr: return sr.group('commit') def main(argv): ret = 0 changelog = 'debian/changelog' until = 'HEAD' found_snapshot_header = False parser = GbpOptionParser(command=os.path.basename(argv[0]), prefix='', usage='%prog [options] paths') 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") parser.add_option_group(range_group) parser.add_option_group(version_group) parser.add_option_group(commit_group) parser.add_config_file_option(option_name="debian-branch", dest='debian_branch', help="branch the debian patch is being developed on, default is '%(debian-branch)s'") parser.add_config_file_option(option_name="upstream-tag", dest="upstream_tag", help="format string for upstream tags, default is '%(upstream-tag)s'") parser.add_config_file_option(option_name="debian-tag", dest="debian_tag", help="format string for debian tags, default is '%(debian-tag)s'") parser.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") 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") commit_group.add_config_file_option(option_name="meta", dest="meta", help="parse meta tags in commit messages, default is '%(meta)s'", action="store_true") commit_group.add_option("--full", action="store_false", dest="short", default=True, help="include the full commit message instead of only the first line") 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") (options, args) = parser.parse_args(argv[1:]) 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") try: if options.verbose: gbpc.Command.verbose = True 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: print >>sys.stderr, "You are not on branch '%s' but on '%s'" % (options.debian_branch, branch) raise GbpError, "Use --debian-branch to set the branch to pick changes from" cp = parse_changelog(changelog) if options.since: since = options.since else: since = '' if options.auto: since = guess_snapshot_commit(cp) if since: print "Continuing from commit '%s'" % since found_snapshot_header = True else: print "Couldn't get snapshot header, using version info" if not since: since = build_tag(options.debian_tag, cp['Version']) if args: print "Only looking for changes on '%s'" % " ".join(args) commits = repo.commits(since, until, " ".join(args), options.git_log.split(" ")) if commits: if cp['Distribution'] != "UNRELEASED" and not found_snapshot_header: add_changelog_section(distribution="UNRELEASED", msg="UNRELEASED") shortlog_to_dch(repo, commits, options) fixup_trailer() if options.snapshot: (snap, version) = do_snapshot(changelog, options.snapshot_number) print "Changelog has been prepared for snapshot #%d at %s" % (snap, version) else: print "No changes detected from %s to %s." % (since, until) if options.release: do_release(changelog, cp) except (GbpError, GitRepositoryError), err: if len(err.__str__()): print >>sys.stderr, 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\:·: