Introduce gbp-pq-rpm
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>
Thu, 12 Jan 2012 13:38:29 +0000 (15:38 +0200)
committerGuido Günther <agx@sigxcpu.org>
Mon, 29 Dec 2014 14:52:18 +0000 (15:52 +0100)
Initial version of gbp-pq-rpm - a tool for managing patch queues for rpm
packages. The functionality more or less corresponds to that of the
(Debian) gbp-pq. The only major difference probably being (in addition
to the obvious of working with .spec files instead of debian/) is that
patches are always imported on top of the upstream version, not on top
of the packaging branch (which might not even contain any source code).

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
Signed-off-by: Olev Kartau <olev.kartau@intel.com>
debian/git-buildpackage-rpm.install
gbp/config.py
gbp/scripts/pq_rpm.py [new file with mode: 0755]
tests/component/rpm/__init__.py
tests/component/rpm/data
tests/component/rpm/test_pq_rpm.py [new file with mode: 0644]

index 67c030974027fbff9e15b189ffd0fc0333cfaf2a..2568b4c92e4be0e7b62d2d72155dda3297d498f3 100644 (file)
@@ -1,2 +1,3 @@
 usr/lib/python2.?/dist-packages/gbp/rpm/
 usr/lib/python2.7/dist-packages/gbp/scripts/import_srpm.py
+usr/lib/python2.7/dist-packages/gbp/scripts/pq_rpm.py
index 8225c16a6611febcd7324fbc2f9e6b01ca41936e..9469f0b82224056490059da424cc7a61952e5d1d 100644 (file)
@@ -569,6 +569,7 @@ class GbpOptionParserRpm(GbpOptionParser):
             'packaging-branch'          : 'master',
             'packaging-dir'             : '',
             'packaging-tag'             : 'packaging/%(version)s',
+            'spec-file'                 : '',
                     })
 
     help = dict(GbpOptionParser.help)
@@ -588,6 +589,9 @@ class GbpOptionParserRpm(GbpOptionParser):
             'packaging-tag':
                 "Format string for packaging tags, RPM counterpart of the "
                 "'debian-tag' option, default is '%(packaging-tag)s'",
+            'spec-file':
+                "Spec file to use, causes the packaging-dir option to be "
+                "ignored, default is '%(spec-file)s'",
                  })
 
 # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
diff --git a/gbp/scripts/pq_rpm.py b/gbp/scripts/pq_rpm.py
new file mode 100755 (executable)
index 0000000..3d1c4bc
--- /dev/null
@@ -0,0 +1,464 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2011 Guido Günther <agx@sigxcpu.org>
+# (C) 2012-2014 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    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
+#
+"""manage patches in a patch queue"""
+
+import ConfigParser
+import bz2
+import errno
+import gzip
+import os
+import re
+import shutil
+import sys
+
+import gbp.log
+import gbp.tmpfile as tempfile
+from gbp.config import GbpOptionParserRpm
+from gbp.rpm.git import GitRepositoryError, RpmGitRepository
+from gbp.git.modifier import GitModifier
+from gbp.command_wrappers import GitCommand, CommandExecFailed
+from gbp.errors import GbpError
+from gbp.patch_series import PatchSeries, Patch
+from gbp.pkg import parse_archive_filename
+from gbp.rpm import (SpecFile, NoSpecError, guess_spec, guess_spec_repo,
+                     spec_from_repo)
+from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base,
+            parse_gbp_commands, format_patch, format_diff,
+            switch_to_pq_branch, apply_single_patch, apply_and_commit_patch,
+            drop_pq, switch_pq)
+from gbp.scripts.common.buildpackage import dump_tree
+
+
+def is_ancestor(repo, parent, child):
+    """Check if commit is ancestor of another"""
+    parent_sha1 = repo.rev_parse("%s^0" % parent)
+    child_sha1 = repo.rev_parse("%s^0" % child)
+    try:
+        merge_base = repo.get_merge_base(parent_sha1, child_sha1)
+    except GitRepositoryError:
+        merge_base = None
+    return merge_base == parent_sha1
+
+def generate_patches(repo, start, end, outdir, options):
+    """
+    Generate patch files from git
+    """
+    gbp.log.info("Generating patches from git (%s..%s)" % (start, end))
+    patches = []
+    commands = {}
+    for treeish in [start, end]:
+        if not repo.has_treeish(treeish):
+            raise GbpError('Invalid treeish object %s' % treeish)
+
+    start_sha1 = repo.rev_parse("%s^0" % start)
+    try:
+        end_commit = end
+    except GitRepositoryError:
+        # In case of plain tree-ish objects, assume current branch head is the
+        # last commit
+        end_commit = "HEAD"
+    end_commit_sha1 = repo.rev_parse("%s^0" % end_commit)
+
+    start_sha1 = repo.rev_parse("%s^0" % start)
+
+    if not is_ancestor(repo, start_sha1, end_commit_sha1):
+        raise GbpError("Start commit '%s' not an ancestor of end commit "
+                       "'%s'" % (start, end_commit))
+    # Check for merge commits, squash if merges found
+    merges = repo.get_commits(start, end_commit, options=['--merges'])
+    if merges:
+        # Shorten SHA1s
+        start_sha1 = repo.rev_parse(start, short=7)
+        merge_sha1 = repo.rev_parse(merges[0], short=7)
+        patch_fn = format_diff(outdir, None, repo, start_sha1, merge_sha1)
+        if patch_fn:
+            gbp.log.info("Merge commits found! Diff between %s..%s written "
+                         "into one monolithic diff" % (start_sha1, merge_sha1))
+            patches.append(patch_fn)
+            start = merge_sha1
+
+    # Generate patches
+    for commit in reversed(repo.get_commits(start, end_commit)):
+        info = repo.get_commit_info(commit)
+        cmds = parse_gbp_commands(info, 'gbp-rpm', ('ignore'),
+                                  ('if', 'ifarch'))
+        if not 'ignore' in cmds:
+            patch_fn = format_patch(outdir, repo, info, patches,
+                                    options.patch_numbers)
+            if patch_fn:
+                commands[os.path.basename(patch_fn)] = cmds
+        else:
+            gbp.log.info('Ignoring commit %s' % info['id'])
+
+    # Generate diff to the tree-ish object
+    if end_commit != end:
+        gbp.log.info("Generating diff file %s..%s" % (end_commit, end))
+        patch_fn = format_diff(outdir, None, repo, end_commit, end,
+                               options.patch_export_ignore_path)
+        if patch_fn:
+            patches.append(patch_fn)
+
+    return patches, commands
+
+
+def rm_patch_files(spec):
+    """
+    Delete the patch files listed in the spec file. Doesn't delete patches
+    marked as not maintained by gbp.
+    """
+    # Remove all old patches from the spec dir
+    for patch in spec.patchseries(unapplied=True):
+        gbp.log.debug("Removing '%s'" % patch.path)
+        try:
+            os.unlink(patch.path)
+        except OSError as err:
+            if err.errno != errno.ENOENT:
+                raise GbpError("Failed to remove patch: %s" % err)
+            else:
+                gbp.log.debug("Patch %s does not exist." % patch.path)
+
+
+def update_patch_series(repo, spec, start, end, options):
+    """
+    Export patches to packaging directory and update spec file accordingly.
+    """
+    # Unlink old patch files and generate new patches
+    rm_patch_files(spec)
+
+    patches, commands = generate_patches(repo, start, end,
+                                         spec.specdir, options)
+    spec.update_patches(patches, commands)
+    spec.write_spec_file()
+    return patches
+
+
+def parse_spec(options, repo, treeish=None):
+    """
+    Find and parse spec file.
+
+    If treeish is given, try to find the spec file from that. Otherwise, search
+    for the spec file in the working copy.
+    """
+    try:
+        if options.spec_file:
+            options.packaging_dir = os.path.dirname(options.spec_file)
+            if not treeish:
+                spec = SpecFile(options.spec_file)
+            else:
+                spec = spec_from_repo(repo, treeish, options.spec_file)
+        else:
+            preferred_name = os.path.basename(repo.path) + '.spec'
+            if not treeish:
+                spec = guess_spec(options.packaging_dir, True, preferred_name)
+            else:
+                spec = guess_spec_repo(repo, treeish, options.packaging_dir,
+                                       True, preferred_name)
+    except NoSpecError as err:
+        raise GbpError("Can't parse spec: %s" % err)
+    relpath = spec.specpath if treeish else os.path.relpath(spec.specpath,
+                                                            repo.path)
+    gbp.log.debug("Using '%s' from '%s'" % (relpath, treeish or 'working copy'))
+    return spec
+
+
+def find_upstream_commit(repo, spec, upstream_tag):
+    """Find commit corresponding upstream version"""
+    tag_str_fields = {'upstreamversion': spec.upstreamversion,
+                      'version': spec.upstreamversion}
+    upstream_commit = repo.find_version(upstream_tag, tag_str_fields)
+    if not upstream_commit:
+        raise GbpError("Couldn't find upstream version %s" %
+                       spec.upstreamversion)
+    return upstream_commit
+
+
+def export_patches(repo, options):
+    """Export patches from the pq branch into a packaging branch"""
+    current = repo.get_branch()
+    if is_pq_branch(current):
+        base = pq_branch_base(current)
+        gbp.log.info("On branch '%s', switching to '%s'" % (current, base))
+        repo.set_branch(base)
+        pq_branch = current
+    else:
+        pq_branch = pq_branch_name(current)
+    spec = parse_spec(options, repo)
+    upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
+    export_treeish = pq_branch
+
+    update_patch_series(repo, spec, upstream_commit, export_treeish, options)
+
+    GitCommand('status')(['--', spec.specdir])
+
+
+def safe_patches(queue, tmpdir_base):
+    """
+    Safe the current patches in a temporary directory
+    below 'tmpdir_base'. Also, uncompress compressed patches here.
+
+    @param queue: an existing patch queue
+    @param tmpdir_base: base under which to create tmpdir
+    @return: tmpdir and a safed queue (with patches in tmpdir)
+    @rtype: tuple
+    """
+
+    tmpdir = tempfile.mkdtemp(dir=tmpdir_base, prefix='patchimport_')
+    safequeue = PatchSeries()
+
+    if len(queue) > 0:
+        gbp.log.debug("Safeing patches '%s' in '%s'" %
+                        (os.path.dirname(queue[0].path), tmpdir))
+    for patch in queue:
+        base, _archive_fmt, comp = parse_archive_filename(patch.path)
+        uncompressors = {'gzip': gzip.open, 'bzip2': bz2.BZ2File}
+        if comp in uncompressors:
+            gbp.log.debug("Uncompressing '%s'" % os.path.basename(patch.path))
+            src = uncompressors[comp](patch.path, 'r')
+            dst_name = os.path.join(tmpdir, os.path.basename(base))
+        elif comp:
+            raise GbpError("Unsupported patch compression '%s', giving up"
+                           % comp)
+        else:
+            src = open(patch.path, 'r')
+            dst_name = os.path.join(tmpdir, os.path.basename(patch.path))
+
+        dst = open(dst_name, 'w')
+        dst.writelines(src)
+        src.close()
+        dst.close()
+
+        safequeue.append(patch)
+        safequeue[-1].path = dst_name
+
+    return safequeue
+
+
+def get_packager(spec):
+    """Get packager information from spec"""
+    if spec.packager:
+        match = re.match(r'(?P<name>.*[^ ])\s*<(?P<email>\S*)>',
+                         spec.packager.strip())
+        if match:
+            return GitModifier(match.group('name'), match.group('email'))
+    return GitModifier()
+
+
+def import_spec_patches(repo, options):
+    """
+    apply a series of patches in a spec/packaging dir to branch
+    the patch-queue branch for 'branch'
+
+    @param repo: git repository to work on
+    @param options: command options
+    """
+    current = repo.get_branch()
+    # Get spec and related information
+    if is_pq_branch(current):
+        base = pq_branch_base(current)
+        if options.force:
+            spec = parse_spec(options, repo, base)
+            spec_treeish = base
+        else:
+            raise GbpError("Already on a patch-queue branch '%s' - doing "
+                           "nothing." % current)
+    else:
+        spec = parse_spec(options, repo)
+        spec_treeish = None
+        base = current
+    upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
+    packager = get_packager(spec)
+    pq_branch = pq_branch_name(base)
+
+    # Create pq-branch
+    if repo.has_branch(pq_branch) and not options.force:
+        raise GbpError("Patch-queue branch '%s' already exists. "
+                       "Try 'switch' instead." % pq_branch)
+    try:
+        if repo.get_branch() == pq_branch:
+            repo.force_head(upstream_commit, hard=True)
+        else:
+            repo.create_branch(pq_branch, upstream_commit, force=True)
+    except GitRepositoryError as err:
+        raise GbpError("Cannot create patch-queue branch '%s': %s" %
+                        (pq_branch, err))
+
+    # Put patches in a safe place
+    if spec_treeish:
+        packaging_tmp = tempfile.mkdtemp(prefix='dump_', dir=options.tmp_dir)
+        packaging_tree = '%s:%s' % (spec_treeish, options.packaging_dir)
+        dump_tree(repo, packaging_tmp, packaging_tree, with_submodules=False,
+                  recursive=False)
+        spec.specdir = packaging_tmp
+    in_queue = spec.patchseries()
+    queue = safe_patches(in_queue, options.tmp_dir)
+    # Do import
+    try:
+        gbp.log.info("Switching to branch '%s'" % pq_branch)
+        repo.set_branch(pq_branch)
+
+        if not queue:
+            return
+        gbp.log.info("Trying to apply patches from branch '%s' onto '%s'" %
+                        (base, upstream_commit))
+        for patch in queue:
+            gbp.log.debug("Applying %s" % patch.path)
+            apply_and_commit_patch(repo, patch, packager)
+    except (GbpError, GitRepositoryError) as err:
+        repo.set_branch(base)
+        repo.delete_branch(pq_branch)
+        raise GbpError('Import failed: %s' % err)
+
+    gbp.log.info("Patches listed in '%s' imported on '%s'" % (spec.specfile,
+                                                              pq_branch))
+
+
+def rebase_pq(repo, options):
+    """Rebase pq branch on the correct upstream version (from spec file)."""
+    current = repo.get_branch()
+    if is_pq_branch(current):
+        base = pq_branch_base(current)
+        spec = parse_spec(options, repo, base)
+    else:
+        base = current
+        spec = parse_spec(options, repo)
+    upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
+
+    switch_to_pq_branch(repo, base)
+    GitCommand("rebase")([upstream_commit])
+
+
+def build_parser(name):
+    """Construct command line parser"""
+    try:
+        parser = GbpOptionParserRpm(command=os.path.basename(name),
+                                    prefix='', usage=
+"""%prog [options] action - maintain patches on a patch queue branch
+tions:
+export         Export the patch queue / devel branch associated to the
+               current branch into a patch series in and update the spec file
+import         Create a patch queue / devel branch from spec file
+               and patches in current dir.
+rebase         Switch to patch queue / devel branch associated to the current
+               branch and rebase against upstream.
+drop           Drop (delete) the patch queue /devel branch associated to
+               the current branch.
+apply          Apply a patch
+switch         Switch to patch-queue branch and vice versa.""")
+
+    except ConfigParser.ParsingError as err:
+        gbp.log.err('Invalid config file: %s' % err)
+        return None
+
+    parser.add_boolean_config_file_option(option_name="patch-numbers",
+            dest="patch_numbers")
+    parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+            default=False, help="Verbose command execution")
+    parser.add_option("--force", dest="force", action="store_true",
+            default=False,
+            help="In case of import even import if the branch already exists")
+    parser.add_config_file_option(option_name="color", dest="color",
+            type='tristate')
+    parser.add_config_file_option(option_name="color-scheme",
+            dest="color_scheme")
+    parser.add_config_file_option(option_name="tmp-dir", dest="tmp_dir")
+    parser.add_config_file_option(option_name="upstream-tag",
+            dest="upstream_tag")
+    parser.add_config_file_option(option_name="spec-file", dest="spec_file")
+    parser.add_config_file_option(option_name="packaging-dir",
+            dest="packaging_dir")
+    return parser
+
+
+def parse_args(argv):
+    """Parse command line arguments"""
+    parser = build_parser(argv[0])
+    if not parser:
+        return None, None
+    return parser.parse_args(argv)
+
+
+def main(argv):
+    """Main function for the gbp pq-rpm command"""
+    retval = 0
+
+    (options, args) = parse_args(argv)
+    if not options:
+        return 1
+
+    gbp.log.setup(options.color, options.verbose, options.color_scheme)
+
+    if len(args) < 2:
+        gbp.log.err("No action given.")
+        return 1
+    else:
+        action = args[1]
+
+    if args[1] in ["export", "import", "rebase", "drop", "switch", "convert"]:
+        pass
+    elif args[1] in ["apply"]:
+        if len(args) != 3:
+            gbp.log.err("No patch name given.")
+            return 1
+        else:
+            patchfile = args[2]
+    else:
+        gbp.log.err("Unknown action '%s'." % args[1])
+        return 1
+
+    try:
+        repo = RpmGitRepository(os.path.curdir)
+    except GitRepositoryError:
+        gbp.log.err("%s is not a git repository" % (os.path.abspath('.')))
+        return 1
+
+    try:
+        # Create base temporary directory for this run
+        options.tmp_dir = tempfile.mkdtemp(dir=options.tmp_dir,
+                                           prefix='gbp-pq-rpm_')
+        current = repo.get_branch()
+        if action == "export":
+            export_patches(repo, options)
+        elif action == "import":
+            import_spec_patches(repo, options)
+        elif action == "drop":
+            drop_pq(repo, current)
+        elif action == "rebase":
+            rebase_pq(repo, options)
+        elif action == "apply":
+            patch = Patch(patchfile)
+            apply_single_patch(repo, current, patch, fallback_author=None)
+        elif action == "switch":
+            switch_pq(repo, current)
+    except CommandExecFailed:
+        retval = 1
+    except GitRepositoryError as err:
+        gbp.log.err("Git command failed: %s" % err)
+        retval = 1
+    except GbpError, err:
+        if len(err.__str__()):
+            gbp.log.err(err)
+        retval = 1
+    finally:
+        shutil.rmtree(options.tmp_dir, ignore_errors=True)
+
+    return retval
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
+
index e84fca944476fcc3f44c59b34fdb5322fab043a5..b5be3e7e6d94da6d4f3535dab60f84329392cb24 100644 (file)
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Test module for RPM command line tools of the git-buildpackage suite"""
 
+from nose.tools import nottest
 import os
+import shutil
+from xml.dom import minidom
 
-from tests.component import ComponentTestGitRepository
+from gbp.git import GitRepository, GitRepositoryError
+
+from tests.component import ComponentTestBase, ComponentTestGitRepository
 
 RPM_TEST_DATA_SUBMODULE = os.path.join('tests', 'component', 'rpm', 'data')
 RPM_TEST_DATA_DIR = os.path.abspath(RPM_TEST_DATA_SUBMODULE)
 
+class RepoManifest(object):
+    """Class representing a test repo manifest file"""
+    def __init__(self, filename=None):
+        self._doc = minidom.Document()
+        if filename:
+            self._doc = minidom.parse(filename)
+            if self._doc.firstChild.nodeName != 'gbp-test-manifest':
+                raise Exception('%s is not a test repo manifest' % filename)
+        else:
+            self._doc.appendChild(self._doc.createElement("gbp-test-manifest"))
+
+    def projects_iter(self):
+        """Return an iterator over projects"""
+        for prj_e in self._doc.getElementsByTagName('project'):
+            branches = {}
+            for br_e in prj_e.getElementsByTagName('branch'):
+                rev = br_e.getAttribute('revision')
+                branches[br_e.getAttribute('name')] = rev
+            yield prj_e.getAttribute('name'), branches
+
+
+    def write(self, filename):
+        """Write to file"""
+        with open(filename, 'w') as fileobj:
+            fileobj.write(self._doc.toprettyxml())
+
 def setup():
     """Test Module setup"""
     ComponentTestGitRepository.check_testdata(RPM_TEST_DATA_SUBMODULE)
 
+
+class RpmRepoTestBase(ComponentTestBase):
+    """Baseclass for tests run in a Git repository with packaging data"""
+
+    @classmethod
+    def setup_class(cls):
+        """Initializations only made once per test run"""
+        super(RpmRepoTestBase, cls).setup_class()
+        cls.manifest = RepoManifest(os.path.join(RPM_TEST_DATA_DIR,
+                                                 'test-repo-manifest.xml'))
+        cls.orig_repos = {}
+        for prj, brs in cls.manifest.projects_iter():
+            repo = GitRepository.create(os.path.join(cls._tmproot,
+                                        '%s.repo' % prj))
+            try:
+                repo.add_remote_repo('origin', RPM_TEST_DATA_DIR, fetch=True)
+            except GitRepositoryError:
+                # Workaround for older git working on submodules initialized
+                # with newer git
+                gitfile = os.path.join(RPM_TEST_DATA_DIR, '.git')
+                if os.path.isfile(gitfile):
+                    with open(gitfile) as fobj:
+                        link = fobj.readline().replace('gitdir:', '').strip()
+                    link_dir = os.path.join(RPM_TEST_DATA_DIR, link)
+                    repo.remove_remote_repo('origin')
+                    repo.add_remote_repo('origin', link_dir, fetch=True)
+                else:
+                    raise
+            # Fetch all remote refs of the orig repo, too
+            repo.fetch('origin', tags=True,
+                       refspec='refs/remotes/*:refs/upstream/*')
+            for branch, rev in brs.iteritems():
+                repo.create_branch(branch, rev)
+            repo.force_head('master', hard=True)
+            cls.orig_repos[prj] = repo
+
+    @classmethod
+    @nottest
+    def init_test_repo(cls, pkg_name):
+        """Initialize git repository for testing"""
+        dirname = os.path.basename(cls.orig_repos[pkg_name].path)
+        shutil.copytree(cls.orig_repos[pkg_name].path, dirname)
+        os.chdir(dirname)
+        return GitRepository('.')
+
 # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
index 90bf36d7981fdd1677cf7e734d9e1056a5fced1c..bae44ddc98ae0ed15ae078cb7c2fc597dee48da5 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 90bf36d7981fdd1677cf7e734d9e1056a5fced1c
+Subproject commit bae44ddc98ae0ed15ae078cb7c2fc597dee48da5
diff --git a/tests/component/rpm/test_pq_rpm.py b/tests/component/rpm/test_pq_rpm.py
new file mode 100644 (file)
index 0000000..f0dac8d
--- /dev/null
@@ -0,0 +1,358 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2013 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    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
+"""Tests for the gbp pq-rpm tool"""
+
+import os
+import tempfile
+from nose.tools import assert_raises, eq_, ok_ # pylint: disable=E0611
+
+from gbp.scripts.pq_rpm import main as pq
+from gbp.git import GitRepository
+from gbp.command_wrappers import GitCommand
+
+from tests.component.rpm import RpmRepoTestBase
+
+# Disable "Method could be a function warning"
+# pylint: disable=R0201
+
+
+def mock_pq(args):
+    """Wrapper for pq"""
+    # Call pq-rpm with added arg0
+    return pq(['arg0'] + args)
+
+class TestPqRpm(RpmRepoTestBase):
+    """Basic tests for gbp-pq-rpm"""
+
+    def test_invalid_args(self):
+        """See that pq-rpm fails gracefully when called with invalid args"""
+        GitRepository.create('.')
+        # Test empty args
+        eq_(mock_pq([]), 1)
+        self._check_log(0, 'gbp:error: No action given.')
+        self._clear_log()
+
+        # Test invalid command
+        eq_(mock_pq(['mycommand']), 1)
+        self._check_log(0, "gbp:error: Unknown action 'mycommand'")
+        self._clear_log()
+
+        # Test invalid cmdline options
+        with assert_raises(SystemExit):
+            mock_pq(['--invalid-arg=123'])
+
+    def test_import_outside_repo(self):
+        """Run pq-rpm when not in a git repository"""
+        eq_(mock_pq(['export']), 1)
+        self._check_log(0, 'gbp:error: %s is not a git repository' %
+                              os.path.abspath(os.getcwd()))
+
+    def test_invalid_config_file(self):
+        """Test invalid config file"""
+        # Create dummy invalid config file and run pq-rpm
+        GitRepository.create('.')
+        with open('.gbp.conf', 'w') as conffd:
+            conffd.write('foobar\n')
+        eq_(mock_pq(['foo']), 1)
+        self._check_log(0, 'gbp:error: Invalid config file: File contains no '
+                           'section headers.')
+
+    def test_import_export(self):
+        """Basic test for patch import and export"""
+        repo = self.init_test_repo('gbp-test')
+        branches = repo.get_local_branches() + ['patch-queue/master']
+        # Test import
+        eq_(mock_pq(['import']), 0)
+        files = ['AUTHORS', 'dummy.sh', 'Makefile', 'NEWS', 'README',
+                 'mydir/myfile.txt']
+        branches.append('patch-queue/master')
+        self._check_repo_state(repo, 'patch-queue/master', branches, files)
+        eq_(repo.get_merge_base('upstream', 'patch-queue/master'),
+            repo.rev_parse('upstream'))
+        ok_(len(repo.get_commits('', 'upstream')) <
+            len(repo.get_commits('', 'patch-queue/master')))
+
+        # Test export
+        eq_(mock_pq(['export', '--upstream-tag',
+                     'srcdata/gbp-test/upstream/%(version)s']), 0)
+        files = ['.gbp.conf', '.gitignore', 'bar.tar.gz', 'foo.txt',
+                 'gbp-test.spec', '0001-my-gz.patch', '0002-my-bzip2.patch',
+                 '0003-my2.patch', 'my.patch']
+        self._check_repo_state(repo, 'master', branches, files)
+        eq_(repo.status()[' M'], ['gbp-test.spec'])
+
+        # Another export after removing some patches
+        os.unlink('0001-my-gz.patch')
+        eq_(mock_pq(['export']), 0)
+        self._check_repo_state(repo, 'master', branches, files)
+
+    def test_import_export2(self):
+        """Another test for import and export"""
+        repo = self.init_test_repo('gbp-test2')
+        branches = repo.get_local_branches() + ['patch-queue/master-orphan']
+        repo.set_branch('master-orphan')
+        # Import
+        eq_(mock_pq(['import']), 0)
+        files = ['dummy.sh', 'Makefile', 'README', 'mydir/myfile.txt']
+        self._check_repo_state(repo, 'patch-queue/master-orphan', branches,
+                               files)
+
+        # Test export
+        eq_(mock_pq(['export', '--upstream-tag',
+                     'srcdata/gbp-test2/upstream/%(version)s', '--spec-file',
+                     'packaging/gbp-test2.spec']), 0)
+        self._check_repo_state(repo, 'master-orphan', branches)
+        eq_(repo.status()[' M'], ['packaging/gbp-test2.spec'])
+
+    def test_rebase(self):
+        """Basic test for rebase action"""
+        repo = self.init_test_repo('gbp-test')
+        repo.rename_branch('pq/master', 'patch-queue/master')
+        repo.set_branch('patch-queue/master')
+        branches = repo.get_local_branches()
+        # Make development branch out-of-sync
+        GitCommand("rebase")(['--onto', 'upstream^', 'upstream'])
+        # Sanity check for our git rebase...
+        ok_(repo.get_merge_base('patch-queue/master', 'upstream') !=
+            repo.rev_parse('upstream'))
+
+        # Do rebase
+        eq_(mock_pq(['rebase']), 0)
+        self._check_repo_state(repo, 'patch-queue/master', branches)
+        ok_(repo.get_merge_base('patch-queue/master', 'upstream') ==
+            repo.rev_parse('upstream'))
+
+        # Get to out-of-sync, again, and try rebase from master branch
+        GitCommand("rebase")(['--onto', 'upstream^', 'upstream'])
+        eq_(mock_pq(['switch']), 0)
+        eq_(mock_pq(['rebase']), 0)
+        self._check_repo_state(repo, 'patch-queue/master', branches)
+        ok_(repo.get_merge_base('patch-queue/master', 'upstream') ==
+            repo.rev_parse('upstream'))
+
+    def test_switch(self):
+        """Basic test for switch action"""
+        repo = self.init_test_repo('gbp-test')
+        pkg_files = repo.list_files()
+        branches = repo.get_local_branches() + ['patch-queue/master']
+        # Switch to non-existent pq-branch should create one
+        eq_(mock_pq(['switch']), 0)
+        self._check_repo_state(repo, 'patch-queue/master', branches)
+
+        # Switch to base branch and back to pq
+        eq_(mock_pq(['switch']), 0)
+        self._check_repo_state(repo, 'master', branches)
+        eq_(mock_pq(['switch']), 0)
+        self._check_repo_state(repo, 'patch-queue/master', branches)
+
+    def test_switch_drop(self):
+        """Basic test for drop action"""
+        repo = self.init_test_repo('gbp-test')
+        repo.rename_branch('pq/master', 'patch-queue/master')
+        repo.set_branch('patch-queue/master')
+        branches = repo.get_local_branches()
+
+        # Drop pq should fail when on pq branch
+        eq_(mock_pq(['drop']), 1)
+        self._check_log(-1, "gbp:error: On a patch-queue branch, can't drop it")
+        self._check_repo_state(repo, 'patch-queue/master', branches)
+
+        # Switch to master
+        eq_(mock_pq(['switch']), 0)
+        self._check_repo_state(repo, 'master', branches)
+
+        # Drop should succeed when on master branch
+        eq_(mock_pq(['drop']), 0)
+        branches.remove('patch-queue/master')
+        self._check_repo_state(repo, 'master', branches)
+
+    def test_force_import(self):
+        """Test force import"""
+        repo = self.init_test_repo('gbp-test')
+        pkg_files = repo.list_files()
+        repo.rename_branch('pq/master', 'patch-queue/master')
+        repo.set_branch('patch-queue/master')
+        branches = repo.get_local_branches()
+        pq_files = repo.list_files()
+
+        # Re-import should fail
+        eq_(mock_pq(['import']), 1)
+        self._check_log(0, "gbp:error: Already on a patch-queue branch")
+        self._check_repo_state(repo, 'patch-queue/master', branches, pq_files)
+
+        # Mangle pq branch and try force import on top of that
+        repo.force_head('master', hard=True)
+        self._check_repo_state(repo, 'patch-queue/master', branches, pkg_files)
+        eq_(mock_pq(['import', '--force']), 0)
+        # .gbp.conf won't get imported by pq
+        pq_files.remove('.gbp.conf')
+        self._check_repo_state(repo, 'patch-queue/master', branches, pq_files)
+
+        # Switch back to master
+        eq_(mock_pq(['switch']), 0)
+        self._check_repo_state(repo, 'master', branches, pkg_files)
+
+        # Import should fail
+        eq_(mock_pq(['import']), 1)
+        self._check_log(-1, "gbp:error: Patch-queue branch .* already exists")
+        self._check_repo_state(repo, 'master', branches, pkg_files)
+
+        # Force import should succeed
+        eq_(mock_pq(['import', '--force']), 0)
+        self._check_repo_state(repo, 'patch-queue/master', branches, pq_files)
+
+    def test_apply(self):
+        """Basic test for apply action"""
+        repo = self.init_test_repo('gbp-test')
+        upstr_files = ['dummy.sh', 'Makefile', 'README']
+        branches = repo.get_local_branches() + ['patch-queue/master']
+
+        # No patch given
+        eq_(mock_pq(['apply']), 1)
+        self._check_log(-1, "gbp:error: No patch name given.")
+
+        # Create a pristine pq-branch
+        repo.create_branch('patch-queue/master', 'upstream')
+
+        # Apply patch
+        with tempfile.NamedTemporaryFile() as tmp_patch:
+            tmp_patch.write(repo.show('master:%s' % 'my.patch'))
+            tmp_patch.file.flush()
+            eq_(mock_pq(['apply', tmp_patch.name]), 0)
+            self._check_repo_state(repo, 'patch-queue/master', branches,
+                                   upstr_files)
+
+        # Apply another patch, now when already on pq branch
+        with tempfile.NamedTemporaryFile() as tmp_patch:
+            tmp_patch.write(repo.show('master:%s' % 'my2.patch'))
+            tmp_patch.file.flush()
+            eq_(mock_pq(['apply', tmp_patch.name]), 0)
+        self._check_repo_state(repo, 'patch-queue/master', branches,
+                               upstr_files + ['mydir/myfile.txt'])
+
+    def test_option_patch_numbers(self):
+        """Test the --patch-numbers cmdline option"""
+        repo = self.init_test_repo('gbp-test')
+        repo.rename_branch('pq/master', 'patch-queue/master')
+        branches = repo.get_local_branches()
+        # Export
+        eq_(mock_pq(['export', '--no-patch-numbers']), 0)
+        files = ['.gbp.conf', '.gitignore', 'bar.tar.gz', 'foo.txt',
+                 'gbp-test.spec', 'my-gz.patch', 'my-bzip2.patch', 'my2.patch',
+                 'my.patch']
+        self._check_repo_state(repo, 'master', branches, files)
+
+    def test_option_tmp_dir(self):
+        """Test the --tmp-dir cmdline option"""
+        self.init_test_repo('gbp-test')
+        eq_(mock_pq(['import', '--tmp-dir=foo/bar']), 0)
+        # Check that the tmp dir basedir was created
+        ok_(os.path.isdir('foo/bar'))
+
+    def test_option_upstream_tag(self):
+        """Test the --upstream-tag cmdline option"""
+        repo = self.init_test_repo('gbp-test')
+
+        # Non-existent upstream-tag -> failure
+        eq_(mock_pq(['import', '--upstream-tag=foobar/%(upstreamversion)s']), 1)
+        self._check_log(-1, "gbp:error: Couldn't find upstream version")
+
+        # Create tag -> import should succeed
+        repo.create_tag('foobar/1.1', msg="test tag", commit='upstream')
+        eq_(mock_pq(['import', '--upstream-tag=foobar/%(upstreamversion)s']), 0)
+
+    def test_option_spec_file(self):
+        """Test --spec-file commandline option"""
+        self.init_test_repo('gbp-test')
+
+        # Non-existent spec file should lead to failure
+        eq_(mock_pq(['import', '--spec-file=foo.spec']), 1)
+        self._check_log(-1, "gbp:error: Can't parse spec: Unable to read spec")
+        # Correct spec file
+        eq_(mock_pq(['import', '--spec-file=gbp-test.spec']), 0)
+
+        # Force import on top to test parsing spec from another branch
+        eq_(mock_pq(['import', '--spec-file=gbp-test.spec', '--force',
+                     '--upstream-tag',
+                     'srcdata/gbp-test/upstream/%(version)s']), 0)
+
+        # Test with export, too
+        eq_(mock_pq(['export', '--spec-file=foo.spec']), 1)
+        self._check_log(-1, "gbp:error: Can't parse spec: Unable to read spec")
+        eq_(mock_pq(['export', '--spec-file=gbp-test.spec']), 0)
+
+    def test_option_packaging_dir(self):
+        """Test --packaging-dir command line option"""
+        self.init_test_repo('gbp-test')
+
+        # Wrong packaging dir should lead to failure
+        eq_(mock_pq(['import', '--packaging-dir=foo']), 1)
+        self._check_log(-1, "gbp:error: Can't parse spec: No spec file found")
+        # Use correct packaging dir
+        eq_(mock_pq(['import', '--packaging-dir=.']), 0)
+
+        # Test with export, --spec-file option should override packaging dir
+        eq_(mock_pq(['export', '--packaging-dir=foo', '--upstream-tag',
+                     'srcdata/gbp-test/upstream/%(version)s',
+                     '--spec-file=gbp-test.spec']), 0)
+
+    def test_export_with_merges(self):
+        """Test exporting pq-branch with merge commits"""
+        repo = self.init_test_repo('gbp-test')
+        repo.rename_branch('pq/master', 'patch-queue/master')
+        repo.set_branch('patch-queue/master')
+        branches = repo.get_local_branches()
+
+        # Create a merge commit in pq-branch
+        patches = repo.format_patches('HEAD^', 'HEAD', '.')
+        repo.force_head('HEAD^', hard=True)
+        repo.commit_dir('.', 'Merge with master', 'patch-queue/master',
+                        ['master'])
+        merge_rev = repo.rev_parse('HEAD', short=7)
+        eq_(mock_pq(['apply', patches[0]]), 0)
+        upstr_rev = repo.rev_parse('upstream', short=7)
+        os.unlink(patches[0])
+
+        # Export should create diff up to the merge point and one "normal" patch
+        eq_(mock_pq(['export']), 0)
+        files = ['.gbp.conf', '.gitignore', 'bar.tar.gz', 'foo.txt',
+                 'gbp-test.spec', 'my.patch',
+                  '%s-to-%s.diff' % (upstr_rev, merge_rev), '0002-my2.patch']
+        self._check_repo_state(repo, 'master', branches, files)
+
+    def test_import_unapplicable_patch(self):
+        """Test import when a patch does not apply"""
+        repo = self.init_test_repo('gbp-test')
+        branches = repo.get_local_branches()
+        # Mangle patch
+        with open('my2.patch', 'w') as patch_file:
+            patch_file.write('-this-does\n+not-apply\n')
+        eq_(mock_pq(['import']), 1)
+        self._check_log(-2, "("
+                             "Aborting|"
+                             "Please, commit your changes or stash them|"
+                             "gbp:error: Import failed.* You have local changes"
+                            ")")
+        self._check_repo_state(repo, 'master', branches)
+
+        # Now commit the changes to the patch and try again
+        repo.add_files(['my2.patch'], force=True)
+        repo.commit_files(['my2.patch'], msg="Mangle patch")
+        eq_(mock_pq(['import']), 1)
+        self._check_log(-2, "gbp:error: Import failed: Error running git apply")
+        self._check_repo_state(repo, 'master', branches)
+