summaryrefslogtreecommitdiff
path: root/gbp/scripts/pq.py
diff options
context:
space:
mode:
Diffstat (limited to 'gbp/scripts/pq.py')
-rw-r--r--gbp/scripts/pq.py427
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))
+