Introduce gbp-pq-rpm tool
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>
Thu, 12 Jan 2012 13:38:29 +0000 (15:38 +0200)
committerMarkus Lehtonen <markus.lehtonen@linux.intel.com>
Thu, 5 Jun 2014 11:20:04 +0000 (14:20 +0300)
Initial version of gbp-pq-rpm: a tool for managing patch queues for rpm
packages.

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
Signed-off-by: Olev Kartau <olev.kartau@intel.com>
bin/gbp-pq-rpm [new file with mode: 0755]
gbp/config.py
gbp/scripts/pq_rpm.py [new file with mode: 0755]
setup.py
tests/01_test_help.py

diff --git a/bin/gbp-pq-rpm b/bin/gbp-pq-rpm
new file mode 100755 (executable)
index 0000000..528020d
--- /dev/null
@@ -0,0 +1,5 @@
+#! /usr/bin/python -u
+import sys
+from gbp.scripts.pq_rpm import main
+
+sys.exit(main(sys.argv))
index 27fe628..50568a3 100644 (file)
@@ -581,6 +581,7 @@ class GbpOptionParserRpm(GbpOptionParser):
                        'cleaner'                : '',
                        'packaging-dir'          : '',
                        'packaging-tag'          : 'packaging/%(version)s',
+                       'spec-file'              : 'auto',
                      } )
 
     help = dict(GbpOptionParser.help)
@@ -589,6 +590,8 @@ class GbpOptionParserRpm(GbpOptionParser):
                         "subdir where packaging files are stored, default is '%(packaging-dir)s'",
                    '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, 'auto' makes gbp to guess, other values make 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..4287cdc
--- /dev/null
@@ -0,0 +1,386 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2011 Guido Günther <agx@sigxcpu.org>
+# (C) 2012 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 errno
+import os
+import shutil
+import sys
+import tempfile
+import re
+from gbp.config import (GbpOptionParserRpm, GbpOptionGroup)
+from gbp.rpm.git import (GitRepositoryError, RpmGitRepository)
+from gbp.git import GitModifier
+from gbp.command_wrappers import (Command, GitCommand, RunAtCommand,
+                                  CommandExecFailed)
+from gbp.errors import GbpError
+import gbp.log
+from gbp.patch_series import (PatchSeries, Patch)
+from gbp.rpm import (SpecFile, guess_spec)
+from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base,
+                                   parse_gbp_commands, format_patch,
+                                   switch_to_pq_branch, apply_single_patch,
+                                   apply_and_commit_patch, drop_pq)
+
+
+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('%s not a valid tree-ish' % treeish)
+
+    # Generate patches
+    for commit in reversed(repo.get_commits(start, end)):
+        info = repo.get_commit_info(commit)
+        cmds = parse_gbp_commands(info, 'gbp-rpm', ('ignore'), None)[0]
+        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'])
+
+    return patches, commands
+
+
+def rm_patch_files(spec):
+    """
+    Delete the patch files listed in the spec files. Doesn't delete patches
+    marked as not maintained by gbp.
+    """
+    # Remove all old patches from the spec dir
+    for n, p in spec.patches.iteritems():
+        if p['autoupdate']:
+            f = os.path.join(spec.specdir, p['filename'])
+            gbp.log.debug("Removing '%s'" % f)
+            try:
+                os.unlink(f)
+            except OSError, (e, msg):
+                if e != errno.ENOENT:
+                    raise GbpError, "Failed to remove patch: %s" % msg
+                else:
+                    gbp.log.debug("%s does not exist." % f)
+
+
+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)
+    spec.write_spec_file()
+
+
+def export_patches(repo, branch, options):
+    """Export patches from the pq branch into a packaging branch"""
+    if is_pq_branch(branch, options):
+        base = pq_branch_base(branch, options)
+        gbp.log.info("On '%s', switching to '%s'" % (branch, base))
+        branch = base
+        repo.set_branch(branch)
+
+    pq_branch = pq_branch_name(branch, options)
+
+    # Find and parse .spec file
+    try:
+        if options.spec_file != 'auto':
+            specfilename = options.spec_file
+            options.packaging_dir = os.path.dirname(specfilename)
+        else:
+            specfilename = guess_spec(options.packaging_dir,
+                                      True,
+                                      os.path.basename(repo.path) + '.spec')
+        spec = SpecFile(specfilename)
+    except KeyError:
+        raise GbpError, "Can't parse spec"
+
+    # Find upstream version
+    upstream_commit = repo.find_version(options.upstream_tag, spec.version)
+    if not upstream_commit:
+        raise GbpError, ("Couldn't find upstream version %s. Don't know on what base to import." % spec.version)
+
+    update_patch_series(repo, spec, upstream_commit, pq_branch, options)
+
+    GitCommand('status')(['--', spec.specdir])
+
+
+def safe_patches(queue, tmpdir_base):
+    """
+    Safe the current patches in a temporary directory
+    below 'tmpdir_base'
+
+    @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='gbp-pq')
+    safequeue=PatchSeries()
+
+    if len(queue) > 0:
+        gbp.log.debug("Safeing patches '%s' in '%s'" % (os.path.dirname(queue[0].path), tmpdir))
+        for p in queue:
+            dst = os.path.join(tmpdir, os.path.basename(p.path))
+            shutil.copy(p.path, dst)
+            safequeue.append(p)
+            safequeue[-1].path = dst;
+
+    return (tmpdir, 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, branch, 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 branch: branch to base pqtch queue on
+    @param options: command options
+    """
+    tmpdir = None
+
+    if is_pq_branch(branch, options):
+        if options.force:
+            branch = pq_branch_base(branch, options)
+            pq_branch = pq_branch_name(branch, options)
+            repo.checkout(branch)
+        else:
+            gbp.log.err("Already on a patch-queue branch '%s' - doing nothing." % branch)
+            raise GbpError
+    else:
+        pq_branch = pq_branch_name(branch, options)
+
+    if repo.has_branch(pq_branch):
+        if options.force:
+            drop_pq(repo, branch, options)
+        else:
+            raise GbpError, ("Patch queue branch '%s'. already exists. Try 'rebase' instead."
+                             % pq_branch)
+
+    # Find and parse .spec file
+    try:
+        if options.spec_file != 'auto':
+            specfilename = options.spec_file
+            options.packaging_dir = os.path.dirname(specfilename)
+        else:
+            specfilename = guess_spec(options.packaging_dir,
+                                      True,
+                                      os.path.basename(repo.path) + '.spec')
+        spec = SpecFile(specfilename)
+    except KeyError:
+        raise GbpError, "Can't parse spec"
+
+    # Find upstream version
+    commit = repo.find_version(options.upstream_tag, spec.version)
+    if commit:
+        commits=[commit]
+    else:
+        raise GbpError, ("Couldn't find upstream version %s. Don't know on what base to import." % spec.version)
+
+    queue = spec.patchseries()
+    packager = get_packager(spec)
+    # Put patches in a safe place
+    tmpdir, queue = safe_patches(queue, repo.path)
+    for commit in commits:
+        try:
+            gbp.log.info("Trying to apply patches at '%s'" % commit)
+            repo.create_branch(pq_branch, commit)
+        except GitRepositoryError:
+            raise GbpError, ("Cannot create patch-queue branch '%s'." % pq_branch)
+
+        repo.set_branch(pq_branch)
+        for patch in queue:
+            gbp.log.debug("Applying %s" % patch.path)
+            try:
+                apply_and_commit_patch(repo, patch, packager)
+            except (GbpError, GitRepositoryError):
+                repo.set_branch(branch)
+                repo.delete_branch(pq_branch)
+                break
+        else:
+            # All patches applied successfully
+            break
+    else:
+        raise GbpError, "Couldn't apply patches"
+
+    if tmpdir:
+        gbp.log.debug("Remove temporary patch safe '%s'" % tmpdir)
+        shutil.rmtree(tmpdir)
+
+    repo.set_branch(branch)
+
+    return os.path.basename(spec.specfile)
+
+
+def rebase_pq(repo, branch, options):
+    if is_pq_branch(branch, options):
+        base = pq_branch_base(branch, options)
+        gbp.log.info("On '%s', switching to '%s'" % (branch, base))
+        branch = base
+        repo.set_branch(branch)
+
+    # Find and parse .spec file
+    try:
+        if options.spec_file != 'auto':
+            specfilename = options.spec_file
+            options.packaging_dir = os.path.dirname(specfilename)
+        else:
+            specfilename = guess_spec(options.packaging_dir,
+                                      True,
+                                      os.path.basename(repo.path) + '.spec')
+        spec = SpecFile(specfilename)
+    except KeyError:
+        raise GbpError, "Can't parse spec"
+
+    # Find upstream version
+    upstream_commit = repo.find_version(options.upstream_tag, spec.version)
+    if not upstream_commit:
+        raise GbpError, ("Couldn't find upstream version %s. Don't know on what base to import." % spec.version)
+
+    switch_to_pq_branch(repo, branch)
+    GitCommand("rebase")([upstream_commit])
+
+
+def switch_pq(repo, current, options):
+    """Switch to patch-queue branch if on base branch and vice versa"""
+    if is_pq_branch(current, options):
+        base = pq_branch_base(current, options)
+        gbp.log.info("Switching to branch '%s'" % base)
+        repo.checkout(base)
+    else:
+        switch_to_pq_branch(repo, current, options)
+
+
+def main(argv):
+    retval = 0
+
+    try:
+        parser = GbpOptionParserRpm(command=os.path.basename(argv[0]), prefix='',
+                                    usage="%prog [options] action - maintain patches on a patch queue branch\n"
+            "Actions:\n"
+            "  export         Export the patch queue / devel branch associated to the\n"
+            "                 current branch into a patch series in and update the spec file\n"
+            "  import         Create a patch queue / devel branch from spec file\n"
+            "                 and patches in current dir.\n"
+            "  rebase         Switch to patch queue / devel branch associated to the current\n"
+            "                 branch and rebase against upstream.\n"
+            "  drop           Drop (delete) the patch queue /devel branch associated to\n"
+            "                 the current branch.\n"
+            "  apply          Apply a patch\n"
+            "  switch         Switch to patch-queue branch and vice versa")
+    except ConfigParser.ParsingError as err:
+        gbp.log.err('Invalid config file: %s' % err)
+        return 1
+
+    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="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")
+
+    (options, args) = parser.parse_args(argv)
+    gbp.log.setup(options.color, options.verbose)
+
+    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"]:
+        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
+
+    if os.path.abspath('.') != repo.path:
+        gbp.log.warn("Switching to topdir before running commands")
+        os.chdir(repo.path)
+
+    try:
+        current = repo.get_branch()
+        if action == "export":
+            export_patches(repo, current, options)
+        elif action == "import":
+            specfile=import_spec_patches(repo, current, options)
+            current = repo.get_branch()
+            gbp.log.info("Patches listed in '%s' imported on '%s'" %
+                          (specfile, current))
+        elif action == "drop":
+            drop_pq(repo, current, options)
+        elif action == "rebase":
+            rebase_pq(repo, current, options)
+        elif action == "apply":
+            patch = Patch(patchfile)
+            apply_single_patch(repo, current, patch, None, options)
+        elif action == "switch":
+            switch_pq(repo, current, options)
+    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
+
+    return retval
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
+
index fdbb1e5..3902575 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -56,7 +56,8 @@ setup(name = "gbp",
                   'bin/gbp-clone',
                   'bin/gbp-create-remote-repo',
                   'bin/git-pbuilder',
-                  'bin/git-import-srpm'],
+                  'bin/git-import-srpm',
+                  'bin/gbp-pq-rpm'],
       packages = find_packages(exclude=['tests', 'tests.*']),
       data_files = [("/etc/git-buildpackage/", ["gbp.conf"]),],
       setup_requires=['nose>=0.11.1', 'coverage>=2.85', 'nosexcover>=1.0.7'] if \
index 331d7cc..d0266a6 100644 (file)
@@ -27,7 +27,8 @@ class TestHelp(unittest.TestCase):
 
     """Test help output of RPM-specific commands"""
     def testHelpRpm(self):
-        for script in ['import_srpm']:
+        for script in ['import_srpm',
+                       'pq_rpm']:
             module = 'gbp.scripts.%s' % script
             m = __import__(module, globals(), locals(), ['main'], -1)
             self.assertRaises(SystemExit,