#!/usr/bin/python -u # vim: set fileencoding=utf-8 : # # (C) 2011 Guido Günther # 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 errno import re import os import shutil import subprocess import sys from gbp.config import (GbpOptionParser, GbpOptionGroup) from gbp.git import (GitRepositoryError, GitRepository) from gbp.command_wrappers import (Command, GitCommand, RunAtCommand, CommandExecFailed) from gbp.errors import GbpError import gbp.log from gbp.pq import PatchQueue PQ_BRANCH_PREFIX = "patch-queue/" PATCH_DIR = "debian/patches/" SERIES_FILE = os.path.join(PATCH_DIR,"series") def is_pq_branch(branch): """ is branch a patch-queue branch? >>> is_pq_branch("foo") False >>> is_pq_branch("patch-queue/foo") True """ return [False, True][branch.startswith(PQ_BRANCH_PREFIX)] def pq_branch_name(branch): """ get the patch queue branch corresponding to branch >>> pq_branch_name("patch-queue/master") >>> pq_branch_name("foo") 'patch-queue/foo' """ if not is_pq_branch(branch): return PQ_BRANCH_PREFIX + branch def pq_branch_base(pq_branch): """ get the branch corresponding to the given patch queue branch >>> pq_branch_base("patch-queue/master") 'master' >>> pq_branch_base("foo") """ if is_pq_branch(pq_branch): return pq_branch[len(PQ_BRANCH_PREFIX):] def write_patch(patch, options): """Write the patch exported by 'git-format-patch' to it's final location (as specified in the commit)""" oldname = patch[len(PATCH_DIR):] newname = oldname tmpname = patch + ".gbp" old = file(patch, 'r') tmp = file(tmpname, 'w') in_patch = False topic = None # Skip first line (from ) old.readline() for line in old: if in_patch: if line.startswith('-- '): # Found final signature, we're done: tmp.write(line) break else: if line.lower().startswith("gbp-pq-topic: "): topic = line.split(" ",1)[1].strip() gbp.log.debug("Topic %s found for %s" % (topic, patch)) continue elif (line.startswith("diff --git a/") or line.startswith("---")): in_patch = True tmp.write(line) tmp.close() old.close() if not options.patch_numbers: patch_re = re.compile("[0-9]+-(?P.+)") m = patch_re.match(oldname) if m: newname = m.group('name') if topic: topicdir = os.path.join(PATCH_DIR, topic) else: topicdir = PATCH_DIR if not os.path.isdir(topicdir): os.makedirs(topicdir, 0755) os.unlink(patch) dstname = os.path.join(topicdir, newname) gbp.log.debug("Moving %s to %s" % (tmpname, dstname)) shutil.move(tmpname, dstname) return dstname def export_patches(repo, branch, options): """Export patches from the pq branch into a patch series""" if is_pq_branch(branch): base = pq_branch_base(branch) gbp.log.info("On '%s', switching to '%s'" % (branch, base)) branch = base repo.set_branch(branch) pq_branch = pq_branch_name(branch) try: shutil.rmtree(PATCH_DIR) except OSError, (e, msg): if e != errno.ENOENT: raise GbpError, "Failed to remove patch dir: %s" % msg else: gbp.log.debug("%s does not exist." % PATCH_DIR) patches = repo.format_patches(branch, pq_branch, PATCH_DIR) if patches: f = file(SERIES_FILE, 'w') gbp.log.info("Regenerating patch queue in '%s'." % PATCH_DIR) for patch in patches: filename = write_patch(patch, options) f.write(filename[len(PATCH_DIR):] + '\n') f.close() GitCommand('status')(['--', PATCH_DIR]) else: gbp.log.info("No patches on '%s' - nothing to do." % pq_branch) def get_maintainer_from_control(): """Get the maintainer from the control file""" cmd = 'sed -n -e \"s/Maintainer: \\+\\(.*\\)/\\1/p\" debian/control' maintainer = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout.readlines()[0].strip() m = re.match('(?P.*[^ ]) *<(?P.*)>', maintainer) if m: return m.group('name'), m.group('email') else: return None, None def import_quilt_patches(repo, branch, series): """apply a series of quilt patches in the series file 'series' to branch the patch-queue branch for 'branch'""" if is_pq_branch(branch): gbp.log.err("Already on a patch-queue branch '%s' - doing nothing." % branch) raise GbpError else: pq_branch = pq_branch_name(branch) if repo.has_branch(pq_branch): raise GbpError, ("Patch queue branch '%s'. already exists. Try 'rebase' instead." try: repo.create_branch(pq_branch) except CommandExecFailed: raise GbpError, ("Cannot create patch-queue branch '%s'." % pq_branch) repo.set_branch(pq_branch) queue = PatchQueue.read_series_file(series) for patch in queue: gbp.log.debug("Applying %s" % patch.path) apply_and_commit_patch(repo, patch.path, patch.topic) def get_mailinfo(patch): """Read patch information into a structured form""" info = {} body = os.path.join('.git', 'gbp_patchinfo') pipe = subprocess.Popen("git mailinfo %s /dev/null < %s" % (body, patch), shell=True, stdout=subprocess.PIPE).stdout for line in pipe: if ':' in line: rfc_header, value = line.split(" ",1) header = rfc_header[:-1].lower() info[header] = value.strip() try: f = file(body) commit_msg = "".join([ line for line in f ]) f.close() os.unlink(body) except IOError, msg: raise GbpError, "Failed to read patch header of '%s': %s" % (patch, msg) return info, commit_msg def switch_to_pq_branch(repo, branch): """Switch to patch-queue branch if not already there, create it if it doesn't exist yet""" if is_pq_branch (branch): return pq_branch = pq_branch_name(branch) if not repo.has_branch(pq_branch): try: repo.create_branch(pq_branch) except CommandExecFailed: raise GbpError, ("Cannot create patch-queue branch '%s'. Try 'rebase' instead." % pq_branch) gbp.log.info("Switching to '%s'" % pq_branch) repo.set_branch(pq_branch) def apply_single_patch(repo, branch, patch, topic=None): switch_to_pq_branch(repo, branch) apply_and_commit_patch(repo, patch, topic) def apply_and_commit_patch(repo, patch, topic=None): """apply a single patch 'patch', add topic 'topic' and commit it""" header, body = get_mailinfo(patch) if not header.has_key('subject'): header['subject'] = os.path.basename(patch) if header.has_key('author') and header.has_key('email'): header['name'] = header['author'] else: name, email = get_maintainer_from_control() if name: gbp.log.warn("Patch '%s' has no authorship information, using '%s <%s>'" % (patch, name, email)) header['name'] = name header['email'] = email else: gbp.log.warn("Patch %s has no authorship information") head = repo.rev_parse('HEAD') repo.apply_patch(patch) tree = repo.write_tree() msg = "%s\n\n%s" % (header['subject'], body) if topic: msg += "\nGbp-Pq-Topic: %s" % topic commit = repo.commit_tree(tree, msg, [head], author=header) repo.update_ref('HEAD', commit, msg="gbp-pq import %s" % patch) def drop_pq(repo, branch): if is_pq_branch(branch): gbp.log.err("On a patch-queue branch, can't drop it.") raise GbpError else: pq_branch = pq_branch_name(branch) if repo.has_branch(pq_branch): repo.delete_branch(pq_branch) gbp.log.info("Dropped branch '%s'." % pq_branch) else: gbp.log.info("No patch queue branch found - doing nothing.") def rebase_pq(repo, branch): switch_to_pq_branch(repo, branch) GitCommand("rebase")([branch]) def main(argv): retval = 0 parser = GbpOptionParser(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 associated to the current branch\n" " into a quilt patch series in debian/patches/ and update the\n" " series file.\n" " import create a patch queue branch from quilt patches in debian/patches.\n" " rebase switch to patch queue branch associated to the current\n" " branch and rebase against current branch.\n" " drop drop (delete) the patch queue associated to the current branch.\n" " apply apply a patch\n") 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("--topic", dest="topic", help="in case of 'apply' topic (subdir) to put patch into") parser.add_config_file_option(option_name="color", dest="color", type='tristate') (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"]: pass elif args[1] in ["apply"]: if len(args) != 3: gbp.log.err("No patch name given.") return 1 else: patch = args[2] else: gbp.log.err("Unknown action '%s'." % args[1]) return 1 try: repo = GitRepository(os.path.curdir) except GitRepositoryError: gbp.log.err("%s is not a git repository" % (os.path.abspath('.'))) return 1 try: current = repo.get_branch() if action == "export": export_patches(repo, current, options) elif action == "import": series = SERIES_FILE import_quilt_patches(repo, current, series) current = repo.get_branch() gbp.log.info("Patches listed in '%s' imported on '%s'" % (series, current)) elif action == "drop": drop_pq(repo, current) elif action == "rebase": rebase_pq(repo, current) elif action == "apply": apply_single_patch(repo, current, patch, options.topic) except CommandExecFailed: 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))