#!/usr/bin/python -u
# vim: set fileencoding=utf-8 :
#
# (C) 2006, 2007, 2009 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
#
"""Import a new upstream version into a git repository"""

import ConfigParser
import glob
import os
import sys
import re
import subprocess
import tarfile
import time
import tempfile
import gbp.command_wrappers as gbpc
from gbp.deb import (parse_changelog, unpack_orig, repack_orig,
                     NoChangelogError, has_epoch, tar_toplevel,
                     guess_upstream_version, do_uscan,
                     parse_changelog_repo, is_valid_packagename,
                     packagename_msg, is_valid_upstreamversion,
                     upstreamversion_msg)
from gbp.git import (FastImport, GitRepositoryError, GitRepository, build_tag)
from gbp.config import GbpOptionParser, GbpOptionGroup, no_upstream_branch_msg
from gbp.errors import (GbpError, GbpNothingImported)
import gbp.log

# Try to import readline, since that will cause raw_input to get fancy
# line editing and history capabilities. However, if readline is not
# available, raw_input will still work.
try:
    import readline
except ImportError:
    pass

def cleanup_tmp_tree(tree):
    """remove a tree of temporary files"""
    try:
        gbpc.RemoveTree(tree)()
    except gbpc.CommandExecFailed:
        gbp.log.err("Removal of tmptree %s failed." % tree)


def is_link_target(target, link):
    """does symlink link already point to target?"""
    if os.path.exists(link):
            if os.path.samefile(target, link):
                return True
    return False


def symlink_orig(archive, pkg, version):
    """
    create a symlink <pkg>_<version>.orig.tar.<ext> so pristine-tar will see the
    correct basename
    @return: archive path to be used by pristine tar
    """
    if os.path.isdir(archive):
        return None
    ext = os.path.splitext(archive)[1]
    link = "../%s_%s.orig.tar%s" % (pkg, version, ext)
    if os.path.basename(archive) != os.path.basename(link):
        try:
            if not is_link_target(archive, link):
                os.symlink(os.path.abspath(archive), link)
        except OSError, err:
                raise GbpError, "Cannot symlink '%s' to '%s': %s" % (archive, link, err[1])
        return link
    else:
        return archive


def upstream_import_commit_msg(options, version):
    return options.import_msg % dict(version=version)


def fast_import_upstream_tree(repo, tarball, version, options):
    """import the upstream tree to the current branch using git fast-import"""

    try:
        compr = tarball.split('.')[-1]

        if not tarfile.is_tarfile(tarball):
            raise GbpError, "'%s' not a tarball" % tarball 

        tar = tarfile.open(tarball, "r:%s" % compr)

        now = "%d %s" % (time.time(), time.strftime("%z"))
        fastimport = FastImport()
        name, email = repo.get_author_info()
        gbp.log.debug("Starting fastimport of %s" % tarball)
        fastimport.start_commit(options.upstream_branch, name, email, now,
                                upstream_import_commit_msg(options, version))
        fastimport.do_deleteall()

        for item in tar:
            if item.isfile():
                if item.mode & 0100:
                    fastimport.add_executable(item.name, tar.extractfile(item.name), item.size)
                else:
                    fastimport.add_file(item.name, tar.extractfile(item.name), item.size)
            elif item.isdir():
                continue # handled by git transparently
            elif item.issym():
                fastimport.add_symlink(item.name, item.linkname)
            # if tarinfo.isextended() not implemented:
            elif item.type in ( "x", "g", "X" ):
                gbp.log.debug("Skipping %s of type '%s'" % (item.name, item.type))
                continue
            else:
                raise GbpError, "'%s' is not a regular file (%s) - don't use fastimport." % (item.name, item.type)
    except gbpc.CommandExecFailed:
        raise GbpError, "Fastimport of upstream version %s failed." % version
    finally:
        tar.close()
        fastimport.close()

    gbp.log.debug("FastImport done.")


def turn_off_fastimport(options, msg):
    if options.fast_import:
        gbp.log.warn(msg)
        gbp.log.warn("Turning off fastimport.")
        options.fast_import = False


def ask_package_name(default):
    """
    Ask the user for the source package name.
    @default The default package name to suggest to the user.
    """
    while True:
        sourcepackage = raw_input("What will be the source package name? [%s] " % default)
        if not sourcepackage: # No input, use the default.
            sourcepackage = default
        # Valid package name, return it.
        if is_valid_packagename(sourcepackage):
            return sourcepackage

        # Not a valid package name. Print an extra
        # newline before the error to make the output a
        # bit clearer.
        gbp.log.warn("\nNot a valid package name: '%s'.\n%s" % (sourcepackage, packagename_msg))

def ask_package_version(default):
    """
    Ask the user for the upstream package version.
    @default The default package version to suggest to the user.
    """
    while True:
        version = raw_input("What is the upstream version? [%s] " % default)
        if not version: # No input, use the default.
            version = default
        # Valid version, return it.
        if is_valid_upstreamversion(version):
            return version

        # Not a valid upstream version. Print an extra
        # newline before the error to make the output a
        # bit clearer.
        gbp.log.warn("\nNot a valid upstream version: '%s'.\n%s" % (version, upstreamversion_msg))


def parse_args(argv):
    try:
        parser = GbpOptionParser(command=os.path.basename(argv[0]), prefix='',
                                 usage='%prog [options] /path/to/upstream-version.tar.gz | --uscan')
    except ConfigParser.ParsingError, err:
        gbp.log.err(err)
        return None, None

    import_group = GbpOptionGroup(parser, "import options",
                      "pristine-tar and filtering")
    tag_group = GbpOptionGroup(parser, "tag options",
                      "options related to git tag creation")
    branch_group = GbpOptionGroup(parser, "version and branch naming options",
                      "version number and branch layout options")
    cmd_group = GbpOptionGroup(parser, "external command options", "how and when to invoke external commands and hooks")

    for group in [import_group, branch_group, tag_group, cmd_group ]:
        parser.add_option_group(group)

    branch_group.add_option("-u", "--upstream-version", dest="version",
                      help="Upstream Version")
    branch_group.add_config_file_option(option_name="debian-branch",
                      dest="debian_branch")
    branch_group.add_config_file_option(option_name="upstream-branch",
                      dest="upstream_branch")
    branch_group.add_boolean_config_file_option(option_name="merge", dest="merge")

    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="upstream-tag",
                      dest="upstream_tag")

    import_group.add_option("--fast-import", action="store_true",
                      dest="fast_import", default=False,
                      help="use 'git fastimport' (experimental)")
    import_group.add_config_file_option(option_name="filter",
                      dest="filters", action="append")
    import_group.add_boolean_config_file_option(option_name="pristine-tar",
                      dest="pristine_tar")
    import_group.add_boolean_config_file_option(option_name="filter-pristine-tar",
                      dest="filter_pristine_tar")
    import_group.add_config_file_option(option_name="import-msg",
                      dest="import_msg")
    cmd_group.add_config_file_option(option_name="postimport", dest="postimport",
                      help="hook run after a successful import, default is '%(postimport)s'")

    parser.add_boolean_config_file_option(option_name="interactive",
                                          dest='interactive')
    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')

    # Accepted for compatibility
    parser.add_option("--no-dch", dest='no_dch', action="store_true",
                      default=False, help="deprecated - don't use.")
    parser.add_option("--uscan", dest='uscan', action="store_true",
                      default=False, help="use uscan(1) to download the new tarball.")

    (options, args) = parser.parse_args(argv[1:])
    gbp.log.setup(options.color, options.verbose)

    if options.no_dch:
        gbp.log.warn("'--no-dch' passed. This is now the default, please remove this option.")

    if options.filters:
        turn_off_fastimport(options, "Import filters currently not supported with fastimport.")

    return options, args


def main(argv):
    ret = 0
    tmpdir = ''
    pristine_orig = None

    (options, args) = parse_args(argv)

    try:
        if options.uscan: # uscan mode
            if args:
                raise GbpError, "you can't pass both --uscan and a filename."

            gbp.log.info("Launching uscan...")
            try:
                status, tarball = do_uscan()
            except KeyError:
                raise GbpError, "error running uscan - debug by running uscan --verbose"

            if not status and not tarball:
                gbp.log.info("package is up to date, nothing to do.")
                return 0
            elif status and not tarball:
                raise GbpError, "uscan didn't download anything, and no tarball was found in ../"
            elif status and tarball:
                gbp.log.info("using %s" % tarball)
                args.append(tarball)

        # tarball specified
        if len(args) != 1:
            gbp.log.err("More than one tarball specified. Try --help.")
            raise GbpError
        else:
            archive = args[0]

        try:
            repo = GitRepository('.')
        except GitRepositoryError:
            raise GbpError, "%s is not a git repository" % (os.path.abspath('.'))

        # an empty repo has now branches:
        initial_branch = repo.get_branch()
        if initial_branch:
            is_empty = False
        else:
            is_empty = True
            turn_off_fastimport(options, "Fast importing into empty archives not yet supported.")

        if not repo.has_branch(options.upstream_branch) and not is_empty:
            gbp.log.err(no_upstream_branch_msg % options.upstream_branch)
            raise GbpError

        # Guess defaults for the package name and version from the
        # original tarball.
        (guessed_package, guessed_version) = guess_upstream_version(archive) or ('', '')

        # Try to find the source package name
        try:
            cp = parse_changelog(filename='debian/changelog')
            sourcepackage = cp['Source']
        except NoChangelogError:
            try:
                # Check the changelog file from the repository, in case
                # we're not on the debian-branch (but upstream, for
                # example).
                cp = parse_changelog_repo(repo, options.debian_branch, 'debian/changelog')
                sourcepackage = cp['Source']
            except NoChangelogError:
                if options.interactive:
                    sourcepackage = ask_package_name(guessed_package)
                else:
                    sourcepackage = guessed_package

        # Try to find the version.
        if options.version:
            version = options.version
        else:
            if options.interactive:
                version = ask_package_version(guessed_version)
            else:
                version = guessed_version

        (clean, out) = repo.is_clean()
        if not clean and not is_empty:
            gbp.log.err("Repository has uncommitted changes, commit these first: ")
            raise GbpError, out

        if os.path.isdir(archive):
            orig_dir = archive
            turn_off_fastimport(options, "Fastimport only supported for tar achives.")
        else:
            if not options.fast_import:
                tmpdir = tempfile.mkdtemp(dir='../')
                unpack_orig(archive, tmpdir, options.filters)
                gbp.log.debug("Unpacked %s to '%s'" % (archive , tmpdir))
                orig_dir = tar_toplevel(tmpdir)

                # Don't mess up or repo with git metadata from an upstream tarball
                try:
                    if os.path.isdir(os.path.join(orig_dir, '.git/')):
                        raise GbpError, "The orig tarball contains .git metadata - giving up."
                except OSError:
                    pass

                if options.pristine_tar and options.filter_pristine_tar and len(options.filters) > 0:
                    gbp.log.debug("Filter pristine-tar: repacking %s from '%s'" % (archive, tmpdir))
                    archive = os.path.join(
                        os.path.dirname(archive),
                        os.path.basename(archive).replace(".tar", ".gbp.tar")
                        )
                    repack_orig(archive, tmpdir, os.path.basename(orig_dir))
            pristine_orig = symlink_orig(archive, sourcepackage, version)

        try:
            upstream_branch = [ options.upstream_branch, 'master' ][is_empty]
            filter_msg = ["", " (filtering out %s)"
                              % options.filters][len(options.filters) > 0]
            gbp.log.info("Importing '%s' to branch '%s'%s..." % (archive,
                                                                 upstream_branch,
                                                                 filter_msg))
            gbp.log.info("Source package is %s" % sourcepackage)
            gbp.log.info("Upstream version is %s" % version)

            if options.fast_import:
                fast_import_upstream_tree(repo, pristine_orig, version, options)
                commit = options.upstream_branch
            else:
                import_branch = [ options.upstream_branch, None ][is_empty]
                msg = upstream_import_commit_msg(options, version)
                commit = repo.commit_dir(orig_dir, msg=msg, branch=import_branch)
                if not commit:
                    raise GbpError, "Import of upstream version %s failed." % version

            if options.pristine_tar:
                if pristine_orig:
                    gbpc.PristineTar().commit(pristine_orig, 'refs/heads/%s' % upstream_branch)
                else:
                    gbp.log.warn("'%s' not an archive, skipping pristine-tar" % archive)

            tag = build_tag(options.upstream_tag, version)
            gbpc.GitTag(options.sign_tags, options.keyid)(tag,
                                                          msg="Upstream version %s" % version,
                                                          commit=commit)
            if is_empty:
                gbpc.GitBranch()(options.upstream_branch, remote=commit)
                repo.force_head(options.upstream_branch, hard=True)
            elif options.merge:
                gbp.log.info("Merging to '%s'" % options.debian_branch)
                repo.set_branch(options.debian_branch)
                try:
                    gbpc.GitMerge(tag)()
                except gbpc.CommandExecFailed:
                    raise GbpError, """Merge failed, please resolve."""
                if options.postimport:
                    epoch = ''
                    if os.access('debian/changelog', os.R_OK):
                        # No need to check the changelog file from the
                        # repository, since we're certain that we're on
                        # the debian-branch
                        cp = parse_changelog(filename='debian/changelog')
                        if has_epoch(cp):
                            epoch = '%s:' % cp['Epoch']
                    info = { 'version': "%s%s-1" % (epoch, version) }
                    env = { 'GBP_BRANCH': options.debian_branch }
                    gbpc.Command(options.postimport % info, extra_env=env, shell=True)()
        except gbpc.CommandExecFailed:
            raise GbpError, "Import of %s failed" % archive
    except GbpNothingImported, err:
        gbp.log.err(err)
        repo.set_branch(initial_branch)
        ret = 1
    except GbpError, err:
        if len(err.__str__()):
            gbp.log.err(err)
        ret = 1

    if tmpdir:
        cleanup_tmp_tree(tmpdir)

    if not ret:
        gbp.log.info("Succesfully imported version %s of %s" % (version, archive))
    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\:·:
