summaryrefslogtreecommitdiff
path: root/gbp/scripts
diff options
context:
space:
mode:
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>2012-01-12 15:38:29 +0200
committerGuido Günther <agx@sigxcpu.org>2014-12-29 15:52:18 +0100
commit60479af1733edfd990cd36ca9d04f7fe70abd5fe (patch)
tree1baf76838aee5a1982b90f4aee961c7b59258066 /gbp/scripts
parent813d01d2b4c208bdd6b7b01d5e661800dd743fd9 (diff)
Introduce gbp-pq-rpm
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>
Diffstat (limited to 'gbp/scripts')
-rwxr-xr-xgbp/scripts/pq_rpm.py464
1 files changed, 464 insertions, 0 deletions
diff --git a/gbp/scripts/pq_rpm.py b/gbp/scripts/pq_rpm.py
new file mode 100755
index 0000000..3d1c4bc
--- /dev/null
+++ b/gbp/scripts/pq_rpm.py
@@ -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))
+