#!/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 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<commit>[a-z0-9]+)\s+\*\*")
author_re = re.compile('Author: (?P<author>.*) <(?P<email>.*)>')

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():
    """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"""
    spawn_dch(msg='')


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)
    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)
    (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 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+#[0-9]+' % options.meta_closes)
    bts_bug = re.compile(r'#[0-9]+')

    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:
                nums = bts_bug.findall(line)
                try:
                    bugs[m.group('bts')] += nums
                except KeyError:
                    bugs[m.group('bts')] = 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',
                      help="branch the debian patch is being developed on, default is '%(debian-branch)s'")
    naming_group.add_config_file_option(option_name="upstream-tag", dest="upstream_tag",
                      help="format string for upstream tags, default is '%(upstream-tag)s'")
    naming_group.add_config_file_option(option_name="debian-tag", dest="debian_tag",
                      help="format string for debian tags, default is '%(debian-tag)s'")
    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")
    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()
        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\:·:
