Skip to content
Snippets Groups Projects
Select Git revision
  • 91dca36407d18adb5b7b0c934ca1aedc93c40446
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/windowsReleaseTest
  • release/releaseTest
  • release/releaseWindowsTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 4.0.0
  • 2.2.0
  • 2.1.0
  • 2.0.1
  • 2.0.0
  • 1.4.1
  • 1.4.0
  • 1.3.0
  • 1.2.0
  • 1.1.0
31 results

git-dch

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    git-dch 14.94 KiB
    #!/usr/bin/python -u
    # vim: set fileencoding=utf-8 :
    #
    # (C) 2007,2008 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 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<commit>[a-z0-9]+)\s+\*\*")
    author_re = re.compile('Author: (?P<author>.*) <(?P<email>.*)>')
    bug_r = r'(?:bug)?\#?\s?\d+'
    bug_re = re.compile(bug_r, re.I)
    
    def system(cmd):
        try:
            Command(cmd, shell=True)()
        except CommandExecFailed:
            raise GbpError
    
    
    def escape_commit(msg):
        return msg.replace('"','\\\"').replace("$","\$").replace("`","\`")
    
    
    def spawn_dch(msg='', author=None, email=None, newversion=False, version=None, release=False, distribution=None):
        distopt = ""
        versionopt = ""
        env = ""
    
        if newversion:
            if version:
                versionopt = '--newversion=%s' % version
            else:
                versionopt = '-i'
        elif release:
            versionopt = "--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 ' % locals()
        if type(msg) == type(''):
            cmd += '"%s"' % escape_commit(msg)
        system(cmd)
    
    
    def add_changelog_entry(msg, author, email):
        "add aa single changelog entry"
        spawn_dch(msg=msg, author=author, email=email)
    
    
    def add_changelog_section(msg, distribution, author=None, email=None, version=None):
        "add a new changelog section"
        spawn_dch(msg=msg, newversion= True, version=version, author=author, email=email, distribution=distribution)
    
    
    def fixup_trailer(repo, git_author=False):
        """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 = None
        if git_author:
            try: author = repo.get_config('user.name')
            except KeyError: pass
    
            try: email = repo.get_config('user.email')
            except KeyError: pass
    
        spawn_dch(msg='', author=author, email=email)
    
    
    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 <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, cp):
        "remove the snapshot header and set the distribution"
        (release, snapshot) = snapshot_version(cp['Version'])
        if snapshot:
            cp['MangledVersion'] = release
            mangle_changelog(changelog, cp)
        # <julien.bonjean@savoirfairelinux.com>
        # prevent doing a release
        # spawn_dch(release=True)
    
    
    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)
    
        # <julien.bonjean@savoirfairelinux.com>
        # clean version before generate snapshot
        version=cp['Version']
        try:
            (release, suffix) = version.rsplit('~', 1)
        except:
            pass
        try:
            (snapshot, commit)  = suffix.split('.', 1) 
    	stripped = str(int(snapshot))
        except:
    	version=release
        commit = head_commit()
    
        (release, snapshot) = snapshot_version(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 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 = {}
        bts_closes = re.compile(r'(?P<bts>%s):\s+%s' % (options.meta_closes, bug_r), re.I)
    
        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()
                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:
            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 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
        first_commit = None
    
        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")
        naming_group = GbpOptionGroup(parser, "branch and tag naming", "branch names and tag formats")
        parser.add_option_group(range_group)
        parser.add_option_group(version_group)
        parser.add_option_group(commit_group)
        parser.add_option_group(naming_group)
    
        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")
        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_config_file_option(option_name="git-author", dest="git_author", action="store_true")
        version_group.add_config_file_option(option_name="no-git-author", dest="git_author", action="store_false")
        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_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_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 find 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(" "))
    
            # add a new changelog section if:
            if 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.new_version or not found_snapshot_header:
                # the user wants to force a new version or switch to snapshot mode
                add_section = True
            else:
                add_section = False
    
            if add_section:
                if commits:
                    first_commit = commits[0]
                    commits = commits[1:]
                    commit_msg, (commit_author, commit_email) = parse_commit(repo, first_commit, options)
                else:
                    commit_msg = "UNRELEASED"
                    commit_author = None
                    commit_email = None
                add_changelog_section(distribution="UNRELEASED", msg=commit_msg,
                                      version=options.new_version, author=commit_author,
                                      email=commit_email)
    
            if commits:
                shortlog_to_dch(repo, commits, options)
                fixup_trailer(repo, git_author=options.git_author)
            elif not first_commit:
                print "No changes detected from %s to %s." % (since, until)
    
            if options.release:
                do_release(changelog, cp)
            elif options.snapshot:
                (snap, version) = do_snapshot(changelog, options.snapshot_number)
                print "Changelog has been prepared for snapshot #%d at %s" % (snap, version)
    
        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\:·: