diff options
Diffstat (limited to 'gbp/scripts/pq.py')
-rw-r--r-- | gbp/scripts/pq.py | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/gbp/scripts/pq.py b/gbp/scripts/pq.py new file mode 100644 index 0000000..2ad7b25 --- /dev/null +++ b/gbp/scripts/pq.py @@ -0,0 +1,427 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2011 Guido Günther <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 +# +"""manage patches in a patch queue""" + +import errno +import re +import os +import shutil +import subprocess +import sys +import tempfile +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 <sha1>) + old.readline() + for line in old: + if in_patch: + if line == '-- \n': + # 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<name>.+)") + 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<name>.*[^ ]) *<(?P<email>.*)>', maintainer) + if m: + return m.group('name'), m.group('email') + else: + return None, None + + +def safe_patches(series): + """ + Safe the current patches in a temporary directory + below .git/ + + @param series: path to series file + @return: tmpdir and path to safed series file + @rtype: tuple + """ + + src = os.path.dirname(series) + name = os.path.basename(series) + + tmpdir = tempfile.mkdtemp(dir='.git/', prefix='gbp-pq') + patches = os.path.join(tmpdir, 'patches') + series = os.path.join(patches, name) + + gbp.log.debug("Safeing patches '%s' in '%s'" % (src, tmpdir)) + shutil.copytree(src, patches) + + return (tmpdir, series) + + +def import_quilt_patches(repo, branch, series, tries): + """ + apply a series of quilt patches in the series file 'series' to branch + the patch-queue branch for 'branch' + + @param repo: git repository to work on + @param branch: branch to base pqtch queue on + @param series; series file to read patches from + @param tries: try that many times to apply the patches going back one + commit in the branches history after each failure. + """ + tmpdir = None + + 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." + % pq_branch) + + commits = repo.get_commits(options=['-%d' % tries], first_parent=True) + # If we go back in history we have to safe our pq so we always try to apply + # the latest one + if len(commits) > 1: + tmpdir, series = safe_patches(series) + + queue = PatchQueue.read_series_file(series) + for commit in commits: + try: + gbp.log.info("Trying to apply patches at '%s'" % commit) + repo.create_branch(pq_branch, commit) + except CommandExecFailed: + 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.path, patch.topic) + except (GbpError, GitRepositoryError, CommandExecFailed): + 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) + + +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 we don't find a subject use the patch's name + if not header.has_key('subject'): + header['subject'] = os.path.basename(patch) + # Strip of .diff or .patch from patch name + base, ext = header['subject'].rsplit('.', 1) + if ext in [ 'diff', 'patch' ]: + header['subject'] = base + + 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") + + 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, [repo.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="time-machine", dest="time_machine", type="int") + 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 + tries = options.time_machine if (options.time_machine > 0) else 1 + import_quilt_patches(repo, current, series, tries) + 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)) + |