pq.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. # vim: set fileencoding=utf-8 :
  2. #
  3. # (C) 2011,2014 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.
  8. #
  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.
  13. #
  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/>
  17. #
  18. """Manage Debian patches on a patch queue branch"""
  19. import errno
  20. import os
  21. import shutil
  22. import sys
  23. import tempfile
  24. import re
  25. from gbp.config import GbpOptionParserDebian
  26. from gbp.deb.source import DebianSource
  27. from gbp.deb.git import DebianGitRepository
  28. from gbp.git import GitRepositoryError
  29. from gbp.command_wrappers import (GitCommand, CommandExecFailed)
  30. from gbp.errors import GbpError
  31. import gbp.log
  32. from gbp.patch_series import (PatchSeries, Patch)
  33. from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base,
  34. parse_gbp_commands, format_patch,
  35. switch_to_pq_branch, apply_single_patch,
  36. apply_and_commit_patch, switch_pq,
  37. drop_pq, get_maintainer_from_control)
  38. from gbp.scripts.common import ExitCodes
  39. from gbp.dch import extract_bts_cmds
  40. PATCH_DIR = "debian/patches/"
  41. SERIES_FILE = os.path.join(PATCH_DIR, "series")
  42. def parse_old_style_topic(commit_info):
  43. """Parse 'gbp-pq-topic:' line(s) from commit info"""
  44. commit = commit_info['id']
  45. topic_regex = 'gbp-pq-topic:\s*(?P<topic>\S.*)'
  46. mangled_body = ''
  47. topic = ''
  48. # Parse and filter commit message body
  49. for line in commit_info['body'].splitlines():
  50. match = re.match(topic_regex, line, flags=re.I)
  51. if match:
  52. topic = match.group('topic')
  53. gbp.log.debug("Topic %s found for %s" % (topic, commit))
  54. gbp.log.warn("Deprecated 'gbp-pq-topic: <topic>' in %s, please "
  55. "use 'Gbp[-Pq]: Topic <topic>' instead" % commit)
  56. continue
  57. mangled_body += line + '\n'
  58. commit_info['body'] = mangled_body
  59. return topic
  60. def generate_patches(repo, start, end, outdir, options):
  61. """
  62. Generate patch files from git
  63. """
  64. gbp.log.info("Generating patches from git (%s..%s)" % (start, end))
  65. patches = []
  66. for treeish in [start, end]:
  67. if not repo.has_treeish(treeish):
  68. raise GbpError('%s not a valid tree-ish' % treeish)
  69. # Generate patches
  70. rev_list = reversed(repo.get_commits(start, end))
  71. for commit in rev_list:
  72. info = repo.get_commit_info(commit)
  73. # Parse 'gbp-pq-topic:'
  74. topic = parse_old_style_topic(info)
  75. cmds = {'topic': topic} if topic else {}
  76. # Parse 'Gbp: ' style commands
  77. (cmds_gbp, info['body']) = parse_gbp_commands(info, 'gbp',
  78. ('ignore'),
  79. ('topic', 'name'),
  80. ('topic', 'name'))
  81. cmds.update(cmds)
  82. # Parse 'Gbp-Pq: ' style commands
  83. (cmds_gbp_pq, info['body']) = parse_gbp_commands(info,
  84. 'gbp-pq',
  85. ('ignore'),
  86. ('topic', 'name'),
  87. ('topic', 'name'))
  88. cmds.update(cmds_gbp_pq)
  89. if 'ignore' not in cmds:
  90. if 'topic' in cmds:
  91. topic = cmds['topic']
  92. name = cmds.get('name', None)
  93. format_patch(outdir, repo, info, patches, options.patch_numbers,
  94. topic=topic, name=name,
  95. renumber=options.renumber,
  96. patch_num_prefix_format=options.patch_num_format)
  97. else:
  98. gbp.log.info('Ignoring commit %s' % info['id'])
  99. return patches
  100. def compare_series(old, new):
  101. """
  102. Compare new pathes to lists of patches already exported
  103. >>> compare_series(['a', 'b'], ['b', 'c'])
  104. (['c'], ['a'])
  105. >>> compare_series([], [])
  106. ([], [])
  107. """
  108. added = set(new).difference(old)
  109. removed = set(old).difference(new)
  110. return (list(added), list(removed))
  111. def format_series_diff(added, removed, options):
  112. """
  113. Format the patch differences into a suitable commit message
  114. >>> format_series_diff(['a'], ['b'], None)
  115. 'Rediff patches\\n\\nAdded a: <REASON>\\nDropped b: <REASON>\\n'
  116. """
  117. if len(added) == 1 and not removed:
  118. # Single patch added, create a more thorough commit message
  119. patch = Patch(os.path.join('debian', 'patches', added[0]))
  120. msg = patch.subject
  121. bugs, dummy = extract_bts_cmds(patch.long_desc.split('\n'), options)
  122. if bugs:
  123. msg += '\n'
  124. for k, v in bugs.items():
  125. msg += '\n%s: %s' % (k, ', '.join(v))
  126. else:
  127. msg = "Rediff patches\n\n"
  128. for p in added:
  129. msg += 'Added %s: <REASON>\n' % p
  130. for p in removed:
  131. msg += 'Dropped %s: <REASON>\n' % p
  132. return msg
  133. def commit_patches(repo, branch, patches, options, patch_dir):
  134. """
  135. Commit chanages exported from patch queue
  136. """
  137. clean, dummy = repo.is_clean()
  138. if clean:
  139. return ([], [])
  140. vfs = gbp.git.vfs.GitVfs(repo, branch)
  141. try:
  142. with vfs.open('debian/patches/series') as oldseries:
  143. oldpatches = [p.strip() for p in oldseries.readlines()]
  144. except IOError:
  145. # No series file yet
  146. oldpatches = []
  147. newpatches = [p[len(patch_dir):] for p in patches]
  148. # FIXME: handle case were only the contents of the patches changed
  149. added, removed = compare_series(oldpatches, newpatches)
  150. msg = format_series_diff(added, removed, options)
  151. if not repo.is_clean(paths='debian/patches')[0]:
  152. repo.add_files(PATCH_DIR, force=True)
  153. repo.commit_staged(msg=msg)
  154. return added, removed
  155. def find_upstream_commit(repo, branch, upstream_tag):
  156. """
  157. Find commit corresponding upstream version based on changelog
  158. """
  159. vfs = gbp.git.vfs.GitVfs(repo, branch)
  160. cl = DebianSource(vfs).changelog
  161. upstream_commit = repo.find_version(upstream_tag, cl.upstream_version)
  162. if not upstream_commit:
  163. raise GbpError("Couldn't find upstream version %s" %
  164. cl.upstream_version)
  165. return upstream_commit
  166. def pq_on_upstream_tag(pq_from):
  167. """Return True if the patch queue is based on the uptream tag,
  168. False if its based on the debian packaging branch"""
  169. return True if pq_from.upper() == 'TAG' else False
  170. def export_patches(repo, branch, options):
  171. """Export patches from the pq branch into a patch series"""
  172. patch_dir = os.path.join(repo.path, PATCH_DIR)
  173. series_file = os.path.join(repo.path, SERIES_FILE)
  174. if is_pq_branch(branch):
  175. base = pq_branch_base(branch)
  176. gbp.log.info("On '%s', switching to '%s'" % (branch, base))
  177. branch = base
  178. repo.set_branch(branch)
  179. pq_branch = pq_branch_name(branch)
  180. try:
  181. shutil.rmtree(patch_dir)
  182. except OSError as e:
  183. if e.errno != errno.ENOENT:
  184. raise GbpError("Failed to remove patch dir: %s" % e.strerror)
  185. else:
  186. gbp.log.debug("%s does not exist." % patch_dir)
  187. if pq_on_upstream_tag(options.pq_from):
  188. base = find_upstream_commit(repo, branch, options.upstream_tag)
  189. else:
  190. base = branch
  191. patches = generate_patches(repo, base, pq_branch, patch_dir, options)
  192. if patches:
  193. with open(series_file, 'w') as seriesfd:
  194. for patch in patches:
  195. seriesfd.write(os.path.relpath(patch, patch_dir) + '\n')
  196. if options.commit:
  197. added, removed = commit_patches(repo, branch, patches, options, patch_dir)
  198. if added:
  199. what = 'patches' if len(added) > 1 else 'patch'
  200. gbp.log.info("Added %s %s to patch series" % (what, ', '.join(added)))
  201. if removed:
  202. what = 'patches' if len(removed) > 1 else 'patch'
  203. gbp.log.info("Removed %s %s from patch series" % (what, ', '.join(removed)))
  204. else:
  205. GitCommand('status', cwd=repo.path)(['--', PATCH_DIR])
  206. else:
  207. gbp.log.info("No patches on '%s' - nothing to do." % pq_branch)
  208. if options.drop:
  209. drop_pq(repo, branch)
  210. def safe_patches(series, repo):
  211. """
  212. Safe the current patches in a temporary directory
  213. below .git/
  214. @param series: path to series file
  215. @return: tmpdir and path to safed series file
  216. @rtype: tuple
  217. """
  218. src = os.path.dirname(series)
  219. name = os.path.basename(series)
  220. tmpdir = tempfile.mkdtemp(dir=repo.git_dir, prefix='gbp-pq')
  221. patches = os.path.join(tmpdir, 'patches')
  222. series = os.path.join(patches, name)
  223. gbp.log.debug("Safeing patches '%s' in '%s'" % (src, tmpdir))
  224. shutil.copytree(src, patches)
  225. return (tmpdir, series)
  226. def import_quilt_patches(repo, branch, series, tries, force, pq_from,
  227. upstream_tag):
  228. """
  229. apply a series of quilt patches in the series file 'series' to branch
  230. the patch-queue branch for 'branch'
  231. @param repo: git repository to work on
  232. @param branch: branch to base patch queue on
  233. @param series: series file to read patches from
  234. @param tries: try that many times to apply the patches going back one
  235. commit in the branches history after each failure.
  236. @param force: import the patch series even if the branch already exists
  237. @param pq_from: what to use as the starting point for the pq branch.
  238. DEBIAN indicates the current branch, TAG indicates that
  239. the corresponding upstream tag should be used.
  240. @param upstream_tag: upstream tag template to use
  241. """
  242. tmpdir = None
  243. series = os.path.join(repo.path, series)
  244. if is_pq_branch(branch):
  245. if force:
  246. branch = pq_branch_base(branch)
  247. pq_branch = pq_branch_name(branch)
  248. repo.checkout(branch)
  249. else:
  250. gbp.log.err("Already on a patch-queue branch '%s' - doing nothing." % branch)
  251. raise GbpError
  252. else:
  253. pq_branch = pq_branch_name(branch)
  254. if repo.has_branch(pq_branch):
  255. if force:
  256. drop_pq(repo, branch)
  257. else:
  258. raise GbpError("Patch queue branch '%s'. already exists. Try 'rebase' instead."
  259. % pq_branch)
  260. maintainer = get_maintainer_from_control(repo)
  261. if pq_on_upstream_tag(pq_from):
  262. commits = [find_upstream_commit(repo, branch, upstream_tag)]
  263. else: # pq_from == 'DEBIAN'
  264. commits = repo.get_commits(num=tries, first_parent=True)
  265. # If we go back in history we have to safe our pq so we always try to apply
  266. # the latest one
  267. # If we are using the upstream_tag, we always need a copy of the patches
  268. if len(commits) > 1 or pq_on_upstream_tag(pq_from):
  269. if os.path.exists(series):
  270. tmpdir, series = safe_patches(series, repo)
  271. queue = PatchSeries.read_series_file(series)
  272. i = len(commits)
  273. for commit in commits:
  274. if len(commits) > 1:
  275. gbp.log.info("%d %s left" % (i, 'tries' if i > 1 else 'try'))
  276. try:
  277. gbp.log.info("Trying to apply patches at '%s'" % commit)
  278. repo.create_branch(pq_branch, commit)
  279. except GitRepositoryError:
  280. raise GbpError("Cannot create patch-queue branch '%s'." % pq_branch)
  281. repo.set_branch(pq_branch)
  282. for patch in queue:
  283. gbp.log.debug("Applying %s" % patch.path)
  284. try:
  285. name = os.path.basename(patch.path)
  286. apply_and_commit_patch(repo, patch, maintainer, patch.topic, name)
  287. except (GbpError, GitRepositoryError) as e:
  288. gbp.log.err("Failed to apply '%s': %s" % (patch.path, e))
  289. repo.force_head('HEAD', hard=True)
  290. repo.set_branch(branch)
  291. repo.delete_branch(pq_branch)
  292. break
  293. else:
  294. # All patches applied successfully
  295. break
  296. i -= 1
  297. else:
  298. raise GbpError("Couldn't apply patches")
  299. if tmpdir:
  300. gbp.log.debug("Remove temporary patch safe '%s'" % tmpdir)
  301. shutil.rmtree(tmpdir)
  302. return len(queue)
  303. def rebase_pq(repo, branch, pq_from, upstream_tag):
  304. if is_pq_branch(branch):
  305. base = pq_branch_base(branch)
  306. else:
  307. switch_to_pq_branch(repo, branch)
  308. base = branch
  309. if pq_on_upstream_tag(pq_from):
  310. _from = find_upstream_commit(repo, base, upstream_tag)
  311. else:
  312. _from = base
  313. GitCommand("rebase", cwd=repo.path)([_from])
  314. def usage_msg():
  315. return """%prog [options] action - maintain patches on a patch queue branch
  316. Actions:
  317. export export the patch queue associated to the current branch
  318. into a quilt patch series in debian/patches/ and update the
  319. series file.
  320. import create a patch queue branch from quilt patches in debian/patches.
  321. rebase switch to patch queue branch associated to the current
  322. branch and rebase against current branch.
  323. drop drop (delete) the patch queue associated to the current branch.
  324. apply apply a patch
  325. switch switch to patch-queue branch and vice versa"""
  326. def build_parser(name):
  327. try:
  328. parser = GbpOptionParserDebian(command=os.path.basename(name),
  329. usage=usage_msg())
  330. except GbpError as err:
  331. gbp.log.err(err)
  332. return None
  333. parser.add_boolean_config_file_option(option_name="patch-numbers", dest="patch_numbers")
  334. parser.add_config_file_option(option_name="patch-num-format", dest="patch_num_format")
  335. parser.add_boolean_config_file_option(option_name="renumber", dest="renumber")
  336. parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
  337. help="verbose command execution")
  338. parser.add_option("--topic", dest="topic", help="in case of 'apply' topic (subdir) to put patch into")
  339. parser.add_config_file_option(option_name="time-machine", dest="time_machine", type="int")
  340. parser.add_boolean_config_file_option("drop", dest='drop')
  341. parser.add_boolean_config_file_option(option_name="commit", dest="commit")
  342. parser.add_option("--force", dest="force", action="store_true", default=False,
  343. help="in case of import even import if the branch already exists")
  344. parser.add_config_file_option(option_name="color", dest="color", type='tristate')
  345. parser.add_config_file_option(option_name="color-scheme",
  346. dest="color_scheme")
  347. parser.add_config_file_option(option_name="meta-closes", dest="meta_closes")
  348. parser.add_config_file_option(option_name="meta-closes-bugnum", dest="meta_closes_bugnum")
  349. parser.add_config_file_option(option_name="pq-from", dest="pq_from", choices=['DEBIAN', 'TAG'])
  350. parser.add_config_file_option(option_name="upstream-tag", dest="upstream_tag")
  351. return parser
  352. def parse_args(argv):
  353. parser = build_parser(argv[0])
  354. if not parser:
  355. return None, None
  356. return parser.parse_args(argv)
  357. def main(argv):
  358. retval = 0
  359. (options, args) = parse_args(argv)
  360. if not options:
  361. return ExitCodes.parse_error
  362. gbp.log.setup(options.color, options.verbose, options.color_scheme)
  363. if len(args) < 2:
  364. gbp.log.err("No action given.")
  365. return 1
  366. else:
  367. action = args[1]
  368. if args[1] in ["export", "import", "rebase", "drop", "switch"]:
  369. pass
  370. elif args[1] in ["apply"]:
  371. if len(args) != 3:
  372. gbp.log.err("No patch name given.")
  373. return 1
  374. else:
  375. patchfile = args[2]
  376. else:
  377. gbp.log.err("Unknown action '%s'." % args[1])
  378. return 1
  379. try:
  380. repo = DebianGitRepository(os.path.curdir)
  381. except GitRepositoryError:
  382. gbp.log.err("%s is not a git repository" % (os.path.abspath('.')))
  383. return 1
  384. try:
  385. current = repo.get_branch()
  386. if action == "export":
  387. export_patches(repo, current, options)
  388. elif action == "import":
  389. series = SERIES_FILE
  390. tries = options.time_machine if (options.time_machine > 0) else 1
  391. num = import_quilt_patches(repo, current, series, tries,
  392. options.force, options.pq_from,
  393. options.upstream_tag)
  394. current = repo.get_branch()
  395. gbp.log.info("%d patches listed in '%s' imported on '%s'" %
  396. (num, series, current))
  397. elif action == "drop":
  398. drop_pq(repo, current)
  399. elif action == "rebase":
  400. rebase_pq(repo, current, options.pq_from, options.upstream_tag)
  401. elif action == "apply":
  402. patch = Patch(patchfile)
  403. maintainer = get_maintainer_from_control(repo)
  404. apply_single_patch(repo, current, patch, maintainer, options.topic)
  405. elif action == "switch":
  406. switch_pq(repo, current)
  407. except KeyboardInterrupt:
  408. retval = 1
  409. gbp.log.err("Interrupted. Aborting.")
  410. except CommandExecFailed:
  411. retval = 1
  412. except (GbpError, GitRepositoryError) as err:
  413. if str(err):
  414. gbp.log.err(err)
  415. retval = 1
  416. return retval
  417. if __name__ == '__main__':
  418. sys.exit(main(sys.argv))