#!/usr/bin/python -u
# vim: set fileencoding=utf-8 :
#
# (C) 2006-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
#
"""run commands to build a debian package out of a git repository"""

import ConfigParser
import errno
import os, os.path
import pipes
import sys
import time
import gbp.deb as du
from gbp.git import (GitRepositoryError, GitRepository, build_tag)
from gbp.command_wrappers import (GitTag, Command, RunAtCommand, CommandExecFailed,
                                  PristineTar, RemoveTree, GitAdd, PristineTar)
from gbp.config import (GbpOptionParser, GbpOptionGroup)
from gbp.errors import GbpError
from glob import glob

# when we want to reference the index in a treeish context we call it:
index_name = "INDEX"
# when we want to reference the working copy in treeish context we call it:
wc_name = "WC"
# index file name used to export working copy
wc_index = ".git/gbp_index"

def git_archive_pipe(prefix, pipe, output, treeish):
    """run the git_archive pipe"""
    pipe.prepend('git archive --format=tar --prefix=%s/ %s' % (prefix, treeish), '.-')
    try:
        ret = pipe.copy('', output)
        if ret:
            print >>sys.stderr, "Error creating %s: %d" % (output, ret)
            return False
    except OSError, err:
        print >>sys.stderr, "Error creating %s: %s" % (output, err[0])
        return False
    except:
        print >>sys.stderr, "Error creating %s" % (output,)
        return False
    return True


def git_archive(cp, output_dir, treeish, comp_type, comp_level):
    "create a compressed orig tarball in output_dir using git_archive"

    try:
        comp_opts = du.compressor_opts[comp_type][0]
    except KeyError:
        raise GbpError, "Unsupported compression type '%s'" % comp_type

    output = os.path.join(output_dir, du.orig_file(cp, comp_type))
    prefix = "%s-%s" % (cp['Source'], cp['Upstream-Version'])

    pipe = pipes.Template()
    pipe.append('%s -c -%s %s' % (comp_type, comp_level, comp_opts),  '--')
    return git_archive_pipe(prefix, pipe, output, treeish)


def dump_tree(export_dir, treeish):
    "dump a tree to output_dir"
    output_dir = os.path.dirname(export_dir)
    prefix = os.path.basename(export_dir)
    pipe = pipes.Template()
    pipe.append('tar -C %s -xf -' % output_dir,  '-.')
    return git_archive_pipe(prefix, pipe, '', treeish)


def move_old_export(target):
    """move a build tree away if it exists"""
    try:
        os.mkdir(target)
    except OSError, (e, msg):
        if e == errno.EEXIST:
            os.rename(target, "%s.obsolete.%s" % (target, time.time()))


def prepare_output_dir(dir):
    output_dir = dir
    if not dir:
        output_dir = '..'
    output_dir = os.path.abspath(output_dir)

    try:
        os.mkdir(output_dir)
    except OSError, (e, msg):
        if e != errno.EEXIST:
            raise GbpError, "Cannot create output dir %s" % output_dir
    return output_dir

def pristine_tar_build_orig(repo, cp, output_dir, options):
    """
    build orig using pristine-tar
    @return: True: orig.tar.gz build, False: noop
    """
    if options.pristine_tar:
        pt = PristineTar()
        if not repo.has_branch(pt.branch):
            print >>sys.stderr, 'Pristine-tar branch "%s" not found' % pt.branch
        pt.checkout(os.path.join(output_dir, du.orig_file(cp, options.comp_type)))
        return True
    else:
        return False


def git_archive_build_orig(repo, cp, output_dir, options):
    """build orig using git-archive"""
    # --upstream-branch was given on the command line, so use this:
    if options.upstream_branch != GbpOptionParser.defaults['upstream-branch']:
        upstream_tree = options.upstream_branch
    else:
        upstream_tree = build_tag(options.upstream_tag, cp['Upstream-Version'])
        # fall back to the upstream-branch tip if the tag doesn't exist
        if not repo.has_treeish(upstream_tree):
            upstream_tree = GbpOptionParser.defaults['upstream-branch']
    print "%s does not exist, creating from '%s'" % (du.orig_file(cp,
                                                     options.comp_type),
                                                     upstream_tree)
    if not repo.has_treeish(upstream_tree):
        raise GbpError # git-ls-tree printed an error message already
    if options.verbose:
        print "Building upstream tarball with compression '%s -%s'" % (options.comp_type,
                                                                       options.comp_level)
    if not git_archive(cp, output_dir, upstream_tree,
                       options.comp_type, options.comp_level):
        raise GbpError, "Cannot create upstream tarball at '%s'" % output_dir


def write_wc(repo):
    """write out the current working copy as a treeish object"""
    tree = None
    os.putenv("GIT_INDEX_FILE", wc_index)
    GitAdd()(['-f', '.'])
    tree = repo.write_tree()
    os.unsetenv("GIT_INDEX_FILE")
    return tree

def drop_index(repo):
    """drop our custom index"""
    if os.path.exists(wc_index):
        os.unlink(wc_index)

def extract_orig(orig_tarball, dest_dir):
    """extract orig tarball to export dir before exporting from git"""
    print "Extracting %s to '%s'" % (os.path.basename(orig_tarball), dest_dir)

    move_old_export(dest_dir)
    du.unpack_orig(orig_tarball, dest_dir, '')

    # Check if tarball extracts into a single folder or not:
    tar_topdir = du.tar_toplevel(dest_dir)
    if tar_topdir != dest_dir:
        # If it extracts a single folder, move all of its contents to dest_dir:
        r = glob("%s/*" % tar_topdir)
        r.extend(glob("%s/.*" % tar_topdir)) # include hidden files and folders
        for f in r:
            os.rename(f, os.path.join(dest_dir, os.path.basename(f)))

        # Remove that single folder:
        os.rmdir(tar_topdir)


def guess_comp_type(repo, comp_type, srcpkg, upstream_version):
    """Guess compression type"""

    if comp_type != 'auto':
        try:
            dummy = du.compressor_opts[comp_type]
        except KeyError:
            print >>sys.stderr, "Unknown compression type - guessing."
            comp_type = 'auto'
    else:
        if not repo.has_branch(PristineTar.branch):
            comp_type = 'gzip'
        else:
            regex = 'pristine-tar .* %s_%s\.orig.tar\.' % (srcpkg, upstream_version)
            commits = repo.grep_log(regex, PristineTar.branch)
            if commits:
                commit = commits[0]
            else:
                commit = PristineTar.branch
            tarball = repo.get_subject(commit)
            comp_type = du.get_compression(tarball)
            if not comp_type:
                comp_type = 'gzip'
                print >>sys.stderr, "Unknown compression type of %s, assuming %s" % (tarball, comp_type)
    return comp_type


def setup_pbuilder(options):
    """setup everything to use git-pbuilder"""
    if options.use_pbuilder:
        options.builder = 'git-pbuilder'
        options.cleaner = '/bin/true'
        os.environ['DIST'] = options.pbuilder_dist
        if options.pbuilder_arch:
            os.environ['ARCH'] = options.pbuilder_arch


def main(argv):
    changelog = 'debian/changelog'
    retval = 0
    prefix = "git-"

    args = [ arg for arg in argv[1:] if arg.find('--%s' % prefix) == 0 ]
    dpkg_args = [ arg for arg in argv[1:] if arg.find('--%s' % prefix) == -1 ]

    # We handle these although they don't have a --git- prefix
    for arg in [ "--help", "-h", "--version" ]:
        if arg in dpkg_args:
            args.append(arg)

    try:
        parser = GbpOptionParser(command=os.path.basename(argv[0]), prefix=prefix)
    except ConfigParser.ParsingError, err:
        print >>sys.stderr, err
        return 1

    tag_group = GbpOptionGroup(parser, "tag options", "options related to git tag creation")
    branch_group = GbpOptionGroup(parser, "branch options", "branch layout options")
    cmd_group = GbpOptionGroup(parser, "external command options", "how and when to invoke external commands and hooks")
    orig_group = GbpOptionGroup(parser, "orig tarball options", "options related to the creation of the orig tarball")
    export_group = GbpOptionGroup(parser, "export build-tree options", "alternative build tree related options")
    parser.add_option_group(tag_group)
    parser.add_option_group(orig_group)
    parser.add_option_group(branch_group)
    parser.add_option_group(cmd_group)
    parser.add_option_group(export_group)

    parser.add_boolean_config_file_option(option_name = "ignore-new", dest="ignore_new")
    parser.add_option("--git-verbose", action="store_true", dest="verbose", default=False,
                      help="verbose command execution")
    tag_group.add_option("--git-tag", action="store_true", dest="tag", default=False,
                      help="create a tag after a successful build")
    tag_group.add_option("--git-tag-only", action="store_true", dest="tag_only", default=False,
                      help="don't build, only tag and run the posttag hook")
    tag_group.add_option("--git-retag", action="store_true", dest="retag", default=False,
                      help="don't fail if the tag already exists")
    tag_group.add_boolean_config_file_option(option_name="sign-tags", dest="sign_tags")
    tag_group.add_config_file_option(option_name="keyid", dest="keyid")
    tag_group.add_config_file_option(option_name="debian-tag", dest="debian_tag")
    tag_group.add_config_file_option(option_name="upstream-tag", dest="upstream_tag")
    orig_group.add_boolean_config_file_option(option_name="pristine-tar", dest="pristine_tar")
    orig_group.add_config_file_option(option_name="force-create", dest="force_create",
                      help="force creation of orig.tar.gz", action="store_true")
    orig_group.add_config_file_option(option_name="no-create-orig", dest="no_create_orig",
                      help="don't create orig.tar.gz", action="store_true")
    orig_group.add_config_file_option(option_name="tarball-dir", dest="tarball_dir",
                      help="location to look for external tarballs")
    orig_group.add_config_file_option(option_name="compression", dest="comp_type",
                      help="Compression type, default is '%(compression)s'")
    orig_group.add_config_file_option(option_name="compression-level", dest="comp_level",
                      help="Compression level, default is '%(compression-level)s'")
    branch_group.add_config_file_option(option_name="upstream-branch", dest="upstream_branch")
    branch_group.add_config_file_option(option_name="debian-branch", dest="debian_branch")
    branch_group.add_boolean_config_file_option(option_name = "ignore-branch", dest="ignore_branch")
    cmd_group.add_config_file_option(option_name="builder", dest="builder",
                      help="command to build the Debian package, default is '%(builder)s'")
    cmd_group.add_config_file_option(option_name="cleaner", dest="cleaner",
                      help="command to clean the working copy, default is '%(cleaner)s'")
    cmd_group.add_config_file_option(option_name="prebuild", dest="prebuild",
                      help="command to run before a build, default is '%(prebuild)s'")
    cmd_group.add_config_file_option(option_name="postbuild", dest="postbuild",
                      help="hook run after a successful build, default is '%(postbuild)s'")
    cmd_group.add_config_file_option(option_name="posttag", dest="posttag",
                      help="hook run after a successful tag operation, default is '%(posttag)s'")
    cmd_group.add_boolean_config_file_option(option_name="pbuilder", dest="use_pbuilder")
    cmd_group.add_config_file_option(option_name="dist", dest="pbuilder_dist")
    cmd_group.add_config_file_option(option_name="arch", dest="pbuilder_arch")
    export_group.add_config_file_option(option_name="export-dir", dest="export_dir",
                      help="before building the package export the source into EXPORT_DIR, default is '%(export-dir)s'")
    export_group.add_config_file_option("export", dest="export",
                      help="export treeish object TREEISH, default is '%(export)s'", metavar="TREEISH")
    export_group.add_option("--git-dont-purge", action="store_false", dest="purge", default=True,
                      help="retain exported package build directory")
    export_group.add_boolean_config_file_option(option_name="overlay", dest="overlay")
    (options, args) = parser.parse_args(args)

    if options.verbose:
        Command.verbose = True

    if options.retag:
        if not options.tag and not options.tag_only:
            print >>sys.stderr, "'--%sretag' needs either '--%stag' or '--%stag-only'" % (prefix, prefix, prefix)
            return 1

    if options.overlay and not options.export_dir:
        parser.error("Overlay must be used with --git-export-dir")

    try:
        repo = GitRepository(os.path.curdir)
    except GitRepositoryError:
        print >>sys.stderr, "%s is not a git repository" % (os.path.abspath('.'))
        return 1
    else:
        repo_dir = os.path.abspath(os.path.curdir)

    try:
        branch = repo.get_branch()
        Command(options.cleaner, shell=True)()
        if not options.ignore_new:
            (ret, out) = repo.is_clean()
            if not ret:
                print >>sys.stderr, "You have uncommitted changes in your source tree:"
                print >>sys.stderr, out
                raise GbpError, "Use --git-ignore-new to ignore."

        if not options.ignore_new and not options.ignore_branch:
            if branch != options.debian_branch:
                print >>sys.stderr, "You are not on branch '%s' but on '%s'" % (options.debian_branch, branch)
                raise GbpError, "Use --git-ignore-branch to ignore or --git-debian-branch to set the branch name."

        try:
            cp = du.parse_changelog(changelog)
            version = cp['Version']
            version_no_epoch = cp['NoEpoch-Version']
            if du.is_native(cp):
                major = cp['Debian-Version']
            else:
                major = cp['Upstream-Version']
        except du.NoChangelogError:
            raise GbpError, "'%s' does not exist, not a debian package" % changelog
        except du.ParseChangeLogError, err:
            raise GbpError, "Error parsing Changelog: %s" % err
        except KeyError:
            raise GbpError, "Can't parse version from changelog"

        if not options.tag_only:
            output_dir = prepare_output_dir(options.export_dir)
            if options.tarball_dir:
                tarball_dir = options.tarball_dir
            else:
                tarball_dir = output_dir

            # Get/build the orig.tar.gz if necessary:
            if not du.is_native(cp):
                options.comp_type = guess_comp_type(repo, options.comp_type, cp['Source'], major)
                orig_file = du.orig_file(cp, options.comp_type)

                # look in tarball_dir first, if found force a symlink to it
                if options.tarball_dir:
                    print "Looking for orig tarball '%s' at '%s'" % (orig_file, tarball_dir)
                    if not du.symlink_orig(cp, options.comp_type, tarball_dir, output_dir, force=True):
                        print "Orig tarball '%s' not found at '%s'" % (orig_file, tarball_dir)
                    else:
                        print "Orig tarball '%s' found at '%s'" % (orig_file, tarball_dir)
                # build an orig unless the user forbids it, always build (and overwrite pre-existing) if user forces it
                if options.force_create or (not options.no_create_orig and not du.has_orig(cp, options.comp_type, output_dir)):
                    if not pristine_tar_build_orig(repo, cp, output_dir, options):
                        git_archive_build_orig(repo, cp, output_dir, options)

            # Export to another build dir if requested:
            if options.export_dir:
                # write a tree of the index if necessary:
                if options.export == index_name:
                    tree = repo.write_tree()
                elif options.export == wc_name:
                    tree = write_wc(repo)
                else:
                    tree = options.export
                if not repo.has_treeish(tree):
                    raise GbpError # git-ls-tree printed an error message already
                tmp_dir = os.path.join(output_dir, "%s-tmp" % cp['Source'])

                # Extract orig tarball if git-overlay option is selected:
                if options.overlay:
                    if du.is_native(cp):
                        raise GbpError, "Cannot overlay Debian native package"
                    extract_orig(os.path.join(output_dir, du.orig_file(cp, options.comp_type)), tmp_dir)

                print "Exporting '%s' to '%s'" % (options.export, tmp_dir)
                dump_tree(tmp_dir, tree)
                cp = du.parse_changelog(os.path.join(tmp_dir, 'debian', 'changelog'))
                export_dir = os.path.join(output_dir, "%s-%s" % (cp['Source'], major))
                print "Moving '%s' to '%s'" % (tmp_dir, export_dir)
                move_old_export(export_dir)
                os.rename(tmp_dir, export_dir)

            if options.export_dir:
                build_dir = export_dir
            else:
                build_dir = repo_dir

            if options.prebuild:
                RunAtCommand(options.prebuild, shell=True,
                             extra_env={'GBP_GIT_DIR': repo.base_dir(),
                                        'GBP_BUILD_DIR': build_dir})(dir=build_dir)

            setup_pbuilder(options)
            # Finally build the package:
            RunAtCommand(options.builder, dpkg_args, shell=True, 
                         extra_env={'GBP_BUILD_DIR': build_dir})(dir=build_dir)
            if options.postbuild:
                arch = du.get_arch()
                changes = os.path.abspath("%s/../%s_%s_%s.changes" %
                                          (build_dir, cp['Source'], version_no_epoch, arch))
                Command(options.postbuild, shell=True,
                        extra_env={'GBP_CHANGES_FILE': changes,
                                   'GBP_BUILD_DIR': build_dir})()
        if options.tag or options.tag_only:
            print "Tagging %s" % version
            tag = build_tag(options.debian_tag, version)
            if options.retag and repo.has_tag(tag):
                repo.remove_tag(tag)
            GitTag(options.sign_tags, options.keyid)(tag,
                   msg="Debian release %s" % version)
            if options.posttag:
                sha = repo.rev_parse("%s^{}" % tag)
                Command(options.posttag, shell=True,
                        extra_env={'GBP_TAG': tag,
                                   'GBP_BRANCH': branch,
                                   'GBP_SHA1': sha})()
    except CommandExecFailed:
        retval = 1
    except GbpError, err:
        if len(err.__str__()):
            print >>sys.stderr, err
        retval = 1
    finally:
        drop_index(repo)

    if not options.tag_only:
        if options.export_dir and options.purge and not retval:
            RemoveTree(export_dir)()

    return retval

if __name__ == '__main__':
    sys.exit(main(sys.argv))

# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
