diff options
-rw-r--r-- | gbp/scripts/common/pq.py | 213 | ||||
-rwxr-xr-x | gbp/scripts/pq.py | 40 | ||||
-rw-r--r-- | tests/13_test_gbp_pq.py | 15 |
3 files changed, 106 insertions, 162 deletions
diff --git a/gbp/scripts/common/pq.py b/gbp/scripts/common/pq.py index 7e6e210b..7bd855e6 100644 --- a/gbp/scripts/common/pq.py +++ b/gbp/scripts/common/pq.py @@ -20,8 +20,9 @@ import re import os -import shutil import subprocess +import textwrap + from gbp.git import GitRepositoryError, GitModifier from gbp.errors import GbpError import gbp.log @@ -65,146 +66,76 @@ def pq_branch_base(pq_branch): return pq_branch[len(PQ_BRANCH_PREFIX):] -def patch_read_header(src): - """ - Read a patches header and split it into single lines. We - assume the header ends at the first line starting with - "diff ..." - """ - header = [] - - for line in src: - if line.startswith('diff '): - break - else: - header.append(line) - else: - raise GbpError("Failed to find patch header in %s" % src.name) - return header - - -def patch_header_parse_topic(header): - """ - Parse the topic from the patch header removing the corresponding - line. This mangles the header in place. - - @param header: patch header - @type header: C{list} of C{str} - - >>> h = ['foo', 'gbp-pq-topic: bar'] - >>> patch_header_parse_topic(h) - 'bar' - >>> h - ['foo'] - """ - topic = None - index = -1 - - for line in header: - if line.lower().startswith("gbp-pq-topic: "): - index = header.index(line) - break - if index != -1: - topic = header[index].split(" ", 1)[1].strip() - del header[index] - return topic - - -def patch_header_mangle_newline(header): - """ - Look for the diff stat separator and remove - trailing new lines before it. This mangles - the header in place. - - @param header: patch header - @type header: C{list} of C{str} - - >>> h = ['foo bar\\n', '\\n', 'bar', '\\n', '\\n', '\\n', '---\\n', '\\n'] - >>> patch_header_mangle_newline(h) - >>> h - ['foo bar\\n', '\\n', 'bar', '\\n', '---\\n', '\\n'] - """ - while True: - try: - index = header.index('---\n') - except ValueError: - return - try: - # Remove trailing newlines until we have at - # at most one left - if header[index-1] == header[index-2] == '\n': - del header[index-2] - else: - return - except IndexError: - return - - -def patch_write_header(srcname, dstname): - """ - Write out the patch header doing any necessary processing such - as detecting and removing a given topic, dropping trailing - new lines and skipping the first line containing the sha1. - """ - topic = None - - with open(srcname) as src: - header = patch_read_header(src) - header_len = len(''.join(header)) - - topic = patch_header_parse_topic(header) - patch_header_mangle_newline(header) - - with open(dstname, 'w') as dst: - dst.write(''.join(header[1:])) - - return (header_len, topic) - - -def patch_write_content(srcname, dstname, header_len): - """ - Write out the patch body skipping the header - """ - with open(srcname) as src: - src.seek(header_len, 0) - with open(dstname, 'a') as dst: - dst.write(src.read()) - - -def write_patch(patch, patch_dir, options): - """Write the patch exported by 'git-format-patch' to it's final location - (as specified in the commit)""" - oldname = os.path.basename(patch) - tmpname = patch + ".gbp" - topic = None - - header_len, topic = patch_write_header(patch, tmpname) - patch_write_content(patch, tmpname, header_len) - - if options.patch_numbers: - newname = oldname - else: - patch_re = re.compile("[0-9]+-(?P<name>.+)") - m = patch_re.match(oldname) - if m: - newname = m.group('name') - else: - raise GbpError("Can't get patch name from '%s'" % oldname) - - if topic: - dstdir = os.path.join(patch_dir, topic) - else: - dstdir = patch_dir - - if not os.path.isdir(dstdir): - os.makedirs(dstdir, 0755) - - os.unlink(patch) - dstname = os.path.join(dstdir, newname) - gbp.log.debug("Moving %s to %s" % (tmpname, dstname)) - shutil.move(tmpname, dstname) - - return dstname +def write_patch_file(filename, commit_info, diff): + """Write patch file""" + if not diff: + gbp.log.debug("I won't generate empty diff %s" % filename) + return None + try: + with open(filename, 'w') as patch: + name = commit_info['author']['name'] + email = commit_info['author']['email'] + # Put name in quotes if special characters found + if re.search("[,.@()\[\]\\\:;]", name): + name = '"%s"' % name + patch.write('From: %s <%s>\n' % (name, email)) + date = commit_info['author'].datetime + patch.write('Date: %s\n' % + date.strftime('%a, %-d %b %Y %H:%M:%S %z')) + subj_lines = textwrap.wrap('Subject: ' + commit_info['subject'], + 77, subsequent_indent=' ', + break_long_words=False, + break_on_hyphens=False) + patch.write('\n'.join(subj_lines) + '\n\n') + patch.writelines(commit_info['body']) + patch.write('---\n') + patch.write(diff) + except IOError as err: + raise GbpError('Unable to create patch file: %s' % err) + return filename + + +def format_patch(outdir, repo, commit_info, series, numbered=True, + topic_regex=None): + """Create patch of a single commit""" + commit = commit_info['id'] + + # Parse and filter commit message body + topic = "" + mangled_body = "" + for line in commit_info['body'].splitlines(): + if topic_regex: + match = re.match(topic_regex, line, flags=re.I) + if match: + topic = match.group('topic') + gbp.log.debug("Topic %s found for %s" % (topic, commit)) + continue + mangled_body += line + '\n' + commit_info['body'] = mangled_body + + # Determine filename and path + outdir = os.path.join(outdir, topic) + if not os.path.exists(outdir): + os.makedirs(outdir) + num_prefix = '%04d-' % (len(series) + 1) + suffix = '.patch' + base_maxlen = 63 - len(num_prefix) - len(suffix) + base = commit_info['patchname'][:base_maxlen] + filename = (num_prefix if numbered else '') + base + suffix + filepath = os.path.join(outdir, filename) + # Make sure that we don't overwrite existing patches in the series + if filepath in series: + presuffix = '-%d' % len(series) + base = base[:base_maxlen-len(presuffix)] + presuffix + filename = (num_prefix if numbered else '') + base + suffix + filepath = os.path.join(outdir, filename) + + # Finally, create the patch + diff = repo.diff('%s^!' % commit, stat=80, summary=True, text=True) + patch = write_patch_file(filepath, commit_info, diff) + if patch: + series.append(patch) + return patch def get_maintainer_from_control(repo): diff --git a/gbp/scripts/pq.py b/gbp/scripts/pq.py index 59be3832..c0f09469 100755 --- a/gbp/scripts/pq.py +++ b/gbp/scripts/pq.py @@ -22,6 +22,7 @@ import os import shutil import sys import tempfile +import re from gbp.config import GbpOptionParserDebian from gbp.git import (GitRepositoryError, GitRepository) from gbp.command_wrappers import (GitCommand, CommandExecFailed) @@ -29,7 +30,7 @@ from gbp.errors import GbpError import gbp.log from gbp.patch_series import (PatchSeries, Patch) from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base, - write_patch, switch_to_pq_branch, + format_patch, switch_to_pq_branch, apply_single_patch, apply_and_commit_patch, drop_pq, get_maintainer_from_control) @@ -37,6 +38,27 @@ PATCH_DIR = "debian/patches/" SERIES_FILE = os.path.join(PATCH_DIR,"series") +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 = [] + for treeish in [start, end]: + if not repo.has_treeish(treeish): + raise GbpError('%s not a valid tree-ish' % treeish) + + # Generate patches + rev_list = reversed(repo.get_commits(start, end)) + topic_regex = 'gbp-pq-topic:\s*(?P<topic>\S.*)' + for commit in rev_list: + info = repo.get_commit_info(commit) + format_patch(outdir, repo, info, patches, options.patch_numbers, + topic_regex=topic_regex) + + return patches + + def export_patches(repo, branch, options): """Export patches from the pq branch into a patch series""" if is_pq_branch(branch): @@ -54,18 +76,12 @@ def export_patches(repo, branch, options): else: gbp.log.debug("%s does not exist." % PATCH_DIR) - patches = repo.format_patches(branch, - pq_branch, PATCH_DIR, - signature=False, - symmetric=False) - if patches: - f = open(SERIES_FILE, 'w') - gbp.log.info("Regenerating patch queue in '%s'." % PATCH_DIR) - for patch in patches: - filename = write_patch(patch, PATCH_DIR, options) - f.write(filename[len(PATCH_DIR):] + '\n') + patches = generate_patches(repo, branch, pq_branch, PATCH_DIR, options) - f.close() + if patches: + with open(SERIES_FILE, 'w') as seriesfd: + for patch in patches: + seriesfd.write(os.path.relpath(patch, PATCH_DIR) + '\n') GitCommand('status')(['--', PATCH_DIR]) else: gbp.log.info("No patches on '%s' - nothing to do." % pq_branch) diff --git a/tests/13_test_gbp_pq.py b/tests/13_test_gbp_pq.py index cebea253..753143d3 100644 --- a/tests/13_test_gbp_pq.py +++ b/tests/13_test_gbp_pq.py @@ -21,6 +21,7 @@ import os import logging import unittest +from gbp.scripts.pq import generate_patches import gbp.scripts.common.pq as pq import gbp.patch_series import tests.testutils as testutils @@ -103,8 +104,8 @@ class TestWritePatch(testutils.DebianGitTestRepo): def tearDown(self): context.teardown() - def test_write_patch(self): - """Test moving a patch to it's final location""" + def test_generate_patches(self): + """Test generation of patches""" class opts: patch_numbers = False @@ -115,13 +116,9 @@ class TestWritePatch(testutils.DebianGitTestRepo): # Write it out as patch and check it's existence d = context.new_tmpdir(__name__) - patchfile = self.repo.format_patches('HEAD^', 'HEAD', str(d))[0] - expected = os.path.join(str(d), '0001-added-foo.patch') - self.assertEqual(expected, patchfile) - pq.write_patch(patchfile, self.repo.path, opts) - expected = os.path.join(self.repo.path, - 'gbptest', - 'added-foo.patch') + patchfile = generate_patches(self.repo, 'HEAD^', 'HEAD', str(d), + opts)[0] + expected = os.path.join(str(d), 'gbptest', 'added-foo.patch') self.assertTrue(os.path.exists(expected)) logging.debug(file(expected).read()) |