1 # vim: set fileencoding=utf-8 :
3 # (C) 2011,2014,2017 Guido Günther <agx@sigxcpu.org>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, please see
16 # <http://www.gnu.org/licenses/>
18 """Manage Debian patches on a patch queue branch"""
26 from gbp.config import GbpOptionParserDebian
27 from gbp.deb.source import DebianSource
28 from gbp.deb.git import DebianGitRepository
29 from gbp.git import GitRepositoryError
30 from gbp.command_wrappers import (GitCommand, CommandExecFailed)
31 from gbp.errors import GbpError
33 from gbp.patch_series import (PatchSeries, Patch)
34 from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base,
35 parse_gbp_commands, format_patch,
37 apply_and_commit_patch,
38 drop_pq, get_maintainer_from_control,
40 from gbp.scripts.common import ExitCodes
41 from gbp.dch import extract_bts_cmds
43 PATCH_DIR = "debian/patches/"
44 SERIES_FILE = os.path.join(PATCH_DIR, "series")
47 def parse_old_style_topic(commit_info):
48 """Parse 'gbp-pq-topic:' line(s) from commit info"""
50 commit = commit_info['id']
51 topic_regex = r'gbp-pq-topic:\s*(?P<topic>\S.*)'
54 # Parse and filter commit message body
55 for line in commit_info['body'].splitlines():
56 match = re.match(topic_regex, line, flags=re.I)
58 topic = match.group('topic')
59 gbp.log.debug("Topic %s found for %s" % (topic, commit))
60 gbp.log.warn("Deprecated 'gbp-pq-topic: <topic>' in %s, please "
61 "use 'Gbp[-Pq]: Topic <topic>' instead" % commit)
63 mangled_body += line + '\n'
64 commit_info['body'] = mangled_body
68 def generate_patches(repo, start, end, outdir, options):
70 Generate patch files from git
72 gbp.log.info("Generating patches from git (%s..%s)" % (start, end))
74 for treeish in [start, end]:
75 if not repo.has_treeish(treeish):
76 raise GbpError('%s not a valid tree-ish' % treeish)
79 rev_list = reversed(repo.get_commits(start, end))
80 for commit in rev_list:
81 info = repo.get_commit_info(commit)
82 # Parse 'gbp-pq-topic:'
83 topic = parse_old_style_topic(info)
84 cmds = {'topic': topic} if topic else {}
85 # Parse 'Gbp: ' style commands
86 (cmds_gbp, info['body']) = parse_gbp_commands(info, 'gbp',
91 # Parse 'Gbp-Pq: ' style commands
92 (cmds_gbp_pq, info['body']) = parse_gbp_commands(info,
97 cmds.update(cmds_gbp_pq)
98 if 'ignore' not in cmds:
100 topic = cmds['topic']
101 name = cmds.get('name', None)
102 format_patch(outdir, repo, info, patches, options.abbrev,
103 numbered=options.patch_numbers,
104 topic=topic, name=name,
105 renumber=options.renumber,
106 patch_num_prefix_format=options.patch_num_format)
108 gbp.log.info('Ignoring commit %s' % info['id'])
113 def compare_series(old, new):
115 Compare new pathes to lists of patches already exported
117 >>> compare_series(['# comment', 'a', 'b'], ['b', 'c'])
119 >>> compare_series([], [])
122 added = set(new).difference(old)
123 removed = [l for l in set(old).difference(new) if not l.startswith('#')]
124 return (list(added), removed)
127 def format_series_diff(added, removed, options):
129 Format the patch differences into a suitable commit message
131 >>> format_series_diff(['a'], ['b'], None)
132 'Rediff patches\\n\\nAdd a: <REASON>\\nDrop b: <REASON>\\n'
134 if len(added) == 1 and not removed:
135 # Single patch added, create a more thorough commit message
136 patch = Patch(os.path.join('debian', 'patches', added[0]))
138 bugs, dummy = extract_bts_cmds(patch.long_desc.split('\n'), options)
141 for k, v in bugs.items():
142 msg += '\n%s: %s' % (k, ', '.join(v))
144 msg = "Rediff patches\n\n"
146 msg += 'Add %s: <REASON>\n' % p
148 msg += 'Drop %s: <REASON>\n' % p
152 def commit_patches(repo, branch, patches, options, patch_dir):
154 Commit chanages exported from patch queue
156 clean, dummy = repo.is_clean()
160 vfs = gbp.git.vfs.GitVfs(repo, branch)
162 with vfs.open('debian/patches/series') as oldseries:
163 oldpatches = [p.strip() for p in oldseries.readlines()]
167 newpatches = [p[len(patch_dir):] for p in patches]
169 # FIXME: handle case were only the contents of the patches changed
170 added, removed = compare_series(oldpatches, newpatches)
171 msg = format_series_diff(added, removed, options)
173 if not repo.is_clean(paths='debian/patches')[0]:
174 repo.add_files(PATCH_DIR, force=True)
175 repo.commit_staged(msg=msg)
176 return added, removed
179 def find_upstream_commit(repo, branch, upstream_tag):
181 Find commit corresponding to upstream version based on changelog
183 vfs = gbp.git.vfs.GitVfs(repo, pq_branch_base(branch))
184 cl = DebianSource(vfs).changelog
185 upstream_commit = repo.find_version(upstream_tag, cl.upstream_version)
186 if not upstream_commit:
187 raise GbpError("Couldn't find upstream version %s" %
189 return upstream_commit
192 def pq_on_upstream_tag(pq_from):
193 """Return True if the patch queue is based on the uptream tag,
194 False if its based on the debian packaging branch"""
195 return True if pq_from.upper() == 'TAG' else False
198 def export_patches(repo, branch, options):
199 """Export patches from the pq branch into a patch series"""
200 patch_dir = os.path.join(repo.path, PATCH_DIR)
201 series_file = os.path.join(repo.path, SERIES_FILE)
202 if is_pq_branch(branch):
203 base = pq_branch_base(branch)
204 gbp.log.info("On '%s', switching to '%s'" % (branch, base))
206 repo.set_branch(branch)
208 pq_branch = pq_branch_name(branch)
210 shutil.rmtree(patch_dir)
212 if e.errno != errno.ENOENT:
213 raise GbpError("Failed to remove patch dir: %s" % e.strerror)
215 gbp.log.debug("%s does not exist." % patch_dir)
217 if pq_on_upstream_tag(options.pq_from):
218 base = find_upstream_commit(repo, branch, options.upstream_tag)
222 patches = generate_patches(repo, base, pq_branch, patch_dir, options)
225 with open(series_file, 'w') as seriesfd:
226 for patch in patches:
227 seriesfd.write(os.path.relpath(patch, patch_dir) + '\n')
229 gbp.log.info("No patches on '%s' - nothing to export." % pq_branch)
232 added, removed = commit_patches(repo, branch, patches, options, patch_dir)
234 what = 'patches' if len(added) > 1 else 'patch'
235 gbp.log.info("Added %s %s to patch series" % (what, ', '.join(added)))
237 what = 'patches' if len(removed) > 1 else 'patch'
238 gbp.log.info("Removed %s %s from patch series" % (what, ', '.join(removed)))
240 gbp.log.info("Updated existing patches.")
243 drop_pq(repo, branch)
246 def safe_patches(series, repo):
248 Safe the current patches in a temporary directory
251 @param series: path to series file
252 @return: tmpdir and path to safed series file
256 src = os.path.dirname(series)
257 name = os.path.basename(series)
259 tmpdir = tempfile.mkdtemp(dir=repo.git_dir, prefix='gbp-pq')
260 patches = os.path.join(tmpdir, 'patches')
261 series = os.path.join(patches, name)
263 gbp.log.debug("Safeing patches '%s' in '%s'" % (src, tmpdir))
264 shutil.copytree(src, patches)
266 return (tmpdir, series)
269 def import_quilt_patches(repo, branch, series, tries, force, pq_from,
272 apply a series of quilt patches in the series file 'series' to branch
273 the patch-queue branch for 'branch'
275 @param repo: git repository to work on
276 @param branch: branch to base patch queue on
277 @param series: series file to read patches from
278 @param tries: try that many times to apply the patches going back one
279 commit in the branches history after each failure.
280 @param force: import the patch series even if the branch already exists
281 @param pq_from: what to use as the starting point for the pq branch.
282 DEBIAN indicates the current branch, TAG indicates that
283 the corresponding upstream tag should be used.
284 @param upstream_tag: upstream tag template to use
287 series = os.path.join(repo.path, series)
289 if is_pq_branch(branch):
291 branch = pq_branch_base(branch)
292 pq_branch = pq_branch_name(branch)
293 repo.checkout(branch)
295 raise GbpError("Already on a patch-queue branch '%s' - doing nothing." % branch)
297 pq_branch = pq_branch_name(branch)
299 if repo.has_branch(pq_branch):
301 drop_pq(repo, branch)
303 raise GbpError("Patch queue branch '%s'. already exists. Try 'rebase' or 'switch' instead."
306 maintainer = get_maintainer_from_control(repo)
307 if pq_on_upstream_tag(pq_from):
308 commits = [find_upstream_commit(repo, branch, upstream_tag)]
309 else: # pq_from == 'DEBIAN'
310 commits = repo.get_commits(num=tries, first_parent=True)
311 # If we go back in history we have to safe our pq so we always try to apply
313 # If we are using the upstream_tag, we always need a copy of the patches
314 if len(commits) > 1 or pq_on_upstream_tag(pq_from):
315 if os.path.exists(series):
316 tmpdir, series = safe_patches(series, repo)
318 queue = PatchSeries.read_series_file(series)
321 for commit in commits:
323 gbp.log.info("%d %s left" % (i, 'tries' if i > 1 else 'try'))
325 gbp.log.info("Trying to apply patches at '%s'" % commit)
326 repo.create_branch(pq_branch, commit)
327 except GitRepositoryError:
328 raise GbpError("Cannot create patch-queue branch '%s'." % pq_branch)
330 repo.set_branch(pq_branch)
332 gbp.log.debug("Applying %s" % patch.path)
334 name = os.path.basename(patch.path)
335 apply_and_commit_patch(repo, patch, maintainer, patch.topic, name)
336 except (GbpError, GitRepositoryError) as e:
337 gbp.log.err("Failed to apply '%s': %s" % (patch.path, e))
338 repo.force_head('HEAD', hard=True)
339 repo.set_branch(branch)
340 repo.delete_branch(pq_branch)
343 # All patches applied successfully
347 raise GbpError("Couldn't apply patches")
350 gbp.log.debug("Remove temporary patch safe '%s'" % tmpdir)
351 shutil.rmtree(tmpdir)
356 def rebase_pq(repo, branch, options):
357 maybe_import_pq(repo, branch, options)
358 # Make sure we're on the pq branch
359 switch_to_pq_branch(repo, branch)
360 if pq_on_upstream_tag(options.pq_from):
361 base = find_upstream_commit(repo, branch, options.upstream_tag)
363 base = pq_branch_base(repo.branch)
365 GitCommand("rebase", cwd=repo.path)([base])
368 def import_pq(repo, branch, options):
369 """Import quilt patches onto pq branch"""
371 tries = options.time_machine if (options.time_machine > 0) else 1
372 num = import_quilt_patches(repo, branch, series, tries,
373 options.force, options.pq_from,
374 options.upstream_tag)
375 gbp.log.info("%d patches listed in '%s' imported on '%s'" %
376 (num, series, repo.get_branch()))
379 def maybe_import_pq(repo, branch, options):
380 """Import quilt patches onto pq branch if pq branch does not exist yet"""
381 if not repo.has_branch(pq_branch_name(branch)):
382 gbp.log.info("No pq branch found, importing patches")
383 import_pq(repo, branch, options)
388 def switch_pq(repo, branch, options):
389 """Switch to patch-queue branch if on base branch and vice versa"""
390 if is_pq_branch(branch):
391 base = pq_branch_base(branch)
392 gbp.log.info("Switching to %s" % base)
395 maybe_import_pq(repo, branch, options)
396 switch_to_pq_branch(repo, branch)
400 return """%prog [options] action - maintain patches on a patch queue branch
402 export export the patch queue associated to the current branch
403 into a quilt patch series in debian/patches/ and update the
405 import create a patch queue branch from quilt patches in debian/patches.
406 rebase switch to patch queue branch associated to the current
407 branch and rebase against current branch.
408 drop drop (delete) the patch queue associated to the current branch.
410 switch switch to patch-queue branch and vice versa"""
413 def build_parser(name):
415 parser = GbpOptionParserDebian(command=os.path.basename(name),
417 except GbpError as err:
421 parser.add_boolean_config_file_option(option_name="patch-numbers", dest="patch_numbers")
422 parser.add_config_file_option(option_name="patch-num-format", dest="patch_num_format")
423 parser.add_boolean_config_file_option(option_name="renumber", dest="renumber")
424 parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
425 help="verbose command execution")
426 parser.add_option("--topic", dest="topic", help="in case of 'apply' topic (subdir) to put patch into")
427 parser.add_config_file_option(option_name="time-machine", dest="time_machine", type="int")
428 parser.add_boolean_config_file_option("drop", dest='drop')
429 parser.add_boolean_config_file_option(option_name="commit", dest="commit")
430 parser.add_config_file_option(option_name="abbrev", dest="abbrev", type="int")
431 parser.add_option("--force", dest="force", action="store_true", default=False,
432 help="in case of import even import if the branch already exists")
433 parser.add_config_file_option(option_name="color", dest="color", type='tristate')
434 parser.add_config_file_option(option_name="color-scheme",
436 parser.add_config_file_option(option_name="meta-closes", dest="meta_closes")
437 parser.add_config_file_option(option_name="meta-closes-bugnum", dest="meta_closes_bugnum")
438 parser.add_config_file_option(option_name="pq-from", dest="pq_from", choices=['DEBIAN', 'TAG'])
439 parser.add_config_file_option(option_name="upstream-tag", dest="upstream_tag")
443 def parse_args(argv):
444 parser = build_parser(argv[0])
447 return parser.parse_args(argv)
453 (options, args) = parse_args(argv)
455 return ExitCodes.parse_error
457 gbp.log.setup(options.color, options.verbose, options.color_scheme)
460 gbp.log.err("No action given.")
465 if args[1] in ["export", "import", "rebase", "drop", "switch"]:
467 elif args[1] in ["apply"]:
469 gbp.log.err("No patch name given.")
474 gbp.log.err("Unknown action '%s'." % args[1])
478 repo = DebianGitRepository(os.path.curdir)
479 except GitRepositoryError:
480 gbp.log.err("%s is not a git repository" % (os.path.abspath('.')))
484 current = repo.get_branch()
485 if action == "export":
486 export_patches(repo, current, options)
487 elif action == "import":
488 import_pq(repo, current, options)
489 elif action == "drop":
490 drop_pq(repo, current)
491 elif action == "rebase":
492 rebase_pq(repo, current, options)
493 elif action == "apply":
494 patch = Patch(patchfile)
495 maintainer = get_maintainer_from_control(repo)
496 apply_single_patch(repo, current, patch, maintainer, options.topic)
497 elif action == "switch":
498 switch_pq(repo, current, options)
499 except KeyboardInterrupt:
501 gbp.log.err("Interrupted. Aborting.")
502 except CommandExecFailed:
504 except (GbpError, GitRepositoryError) as err:
512 if __name__ == '__main__':
513 sys.exit(main(sys.argv))