pq_rpm.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. # vim: set fileencoding=utf-8 :
  2. #
  3. # (C) 2011 Guido Günther <agx@sigxcpu.org>
  4. # (C) 2012-2014 Intel Corporation <markus.lehtonen@linux.intel.com>
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, please see
  17. # <http://www.gnu.org/licenses/>
  18. #
  19. """manage patches in a patch queue"""
  20. from six.moves import configparser
  21. import bz2
  22. import errno
  23. import gzip
  24. import os
  25. import re
  26. import sys
  27. import gbp.log
  28. from gbp.tmpfile import init_tmpdir, del_tmpdir, tempfile
  29. from gbp.config import GbpOptionParserRpm
  30. from gbp.rpm.git import GitRepositoryError, RpmGitRepository
  31. from gbp.git.modifier import GitModifier
  32. from gbp.command_wrappers import GitCommand, CommandExecFailed
  33. from gbp.errors import GbpError
  34. from gbp.patch_series import PatchSeries, Patch
  35. from gbp.pkg import parse_archive_filename
  36. from gbp.rpm import (SpecFile, NoSpecError, guess_spec, guess_spec_repo,
  37. spec_from_repo)
  38. from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base,
  39. parse_gbp_commands, format_patch, format_diff,
  40. switch_to_pq_branch, apply_single_patch, apply_and_commit_patch,
  41. drop_pq, switch_pq)
  42. from gbp.scripts.common.buildpackage import dump_tree
  43. def is_ancestor(repo, parent, child):
  44. """Check if commit is ancestor of another"""
  45. parent_sha1 = repo.rev_parse("%s^0" % parent)
  46. child_sha1 = repo.rev_parse("%s^0" % child)
  47. try:
  48. merge_base = repo.get_merge_base(parent_sha1, child_sha1)
  49. except GitRepositoryError:
  50. merge_base = None
  51. return merge_base == parent_sha1
  52. def generate_patches(repo, start, end, outdir, options):
  53. """
  54. Generate patch files from git
  55. """
  56. gbp.log.info("Generating patches from git (%s..%s)" % (start, end))
  57. patches = []
  58. commands = {}
  59. for treeish in [start, end]:
  60. if not repo.has_treeish(treeish):
  61. raise GbpError('Invalid treeish object %s' % treeish)
  62. start_sha1 = repo.rev_parse("%s^0" % start)
  63. try:
  64. end_commit = end
  65. except GitRepositoryError:
  66. # In case of plain tree-ish objects, assume current branch head is the
  67. # last commit
  68. end_commit = "HEAD"
  69. end_commit_sha1 = repo.rev_parse("%s^0" % end_commit)
  70. start_sha1 = repo.rev_parse("%s^0" % start)
  71. if not is_ancestor(repo, start_sha1, end_commit_sha1):
  72. raise GbpError("Start commit '%s' not an ancestor of end commit "
  73. "'%s'" % (start, end_commit))
  74. # Check for merge commits, squash if merges found
  75. merges = repo.get_commits(start, end_commit, options=['--merges'])
  76. if merges:
  77. # Shorten SHA1s
  78. start_sha1 = repo.rev_parse(start, short=7)
  79. merge_sha1 = repo.rev_parse(merges[0], short=7)
  80. patch_fn = format_diff(outdir, None, repo, start_sha1, merge_sha1)
  81. if patch_fn:
  82. gbp.log.info("Merge commits found! Diff between %s..%s written "
  83. "into one monolithic diff" % (start_sha1, merge_sha1))
  84. patches.append(patch_fn)
  85. start = merge_sha1
  86. # Generate patches
  87. for commit in reversed(repo.get_commits(start, end_commit)):
  88. info = repo.get_commit_info(commit)
  89. (cmds, info['body']) = parse_gbp_commands(info,
  90. 'gbp-rpm',
  91. ('ignore'),
  92. ('if', 'ifarch'))
  93. if not 'ignore' in cmds:
  94. patch_fn = format_patch(outdir, repo, info, patches,
  95. options.patch_numbers)
  96. if patch_fn:
  97. commands[os.path.basename(patch_fn)] = cmds
  98. else:
  99. gbp.log.info('Ignoring commit %s' % info['id'])
  100. # Generate diff to the tree-ish object
  101. if end_commit != end:
  102. gbp.log.info("Generating diff file %s..%s" % (end_commit, end))
  103. patch_fn = format_diff(outdir, None, repo, end_commit, end,
  104. options.patch_export_ignore_path)
  105. if patch_fn:
  106. patches.append(patch_fn)
  107. return patches, commands
  108. def rm_patch_files(spec):
  109. """
  110. Delete the patch files listed in the spec file. Doesn't delete patches
  111. marked as not maintained by gbp.
  112. """
  113. # Remove all old patches from the spec dir
  114. for patch in spec.patchseries(unapplied=True):
  115. gbp.log.debug("Removing '%s'" % patch.path)
  116. try:
  117. os.unlink(patch.path)
  118. except OSError as err:
  119. if err.errno != errno.ENOENT:
  120. raise GbpError("Failed to remove patch: %s" % err)
  121. else:
  122. gbp.log.debug("Patch %s does not exist." % patch.path)
  123. def update_patch_series(repo, spec, start, end, options):
  124. """
  125. Export patches to packaging directory and update spec file accordingly.
  126. """
  127. # Unlink old patch files and generate new patches
  128. rm_patch_files(spec)
  129. patches, commands = generate_patches(repo, start, end,
  130. spec.specdir, options)
  131. spec.update_patches(patches, commands)
  132. spec.write_spec_file()
  133. return patches
  134. def parse_spec(options, repo, treeish=None):
  135. """
  136. Find and parse spec file.
  137. If treeish is given, try to find the spec file from that. Otherwise, search
  138. for the spec file in the working copy.
  139. """
  140. try:
  141. if options.spec_file:
  142. if not treeish:
  143. spec = SpecFile(options.spec_file)
  144. else:
  145. spec = spec_from_repo(repo, treeish, options.spec_file)
  146. else:
  147. preferred_name = os.path.basename(repo.path) + '.spec'
  148. if not treeish:
  149. spec = guess_spec(options.packaging_dir, True, preferred_name)
  150. else:
  151. spec = guess_spec_repo(repo, treeish, options.packaging_dir,
  152. True, preferred_name)
  153. except NoSpecError as err:
  154. raise GbpError("Can't parse spec: %s" % err)
  155. relpath = spec.specpath if treeish else os.path.relpath(spec.specpath,
  156. repo.path)
  157. options.packaging_dir = os.path.dirname(relpath)
  158. gbp.log.debug("Using '%s' from '%s'" % (relpath, treeish or 'working copy'))
  159. return spec
  160. def find_upstream_commit(repo, spec, upstream_tag):
  161. """Find commit corresponding upstream version"""
  162. tag_str_fields = {'upstreamversion': spec.upstreamversion,
  163. 'version': spec.upstreamversion}
  164. upstream_commit = repo.find_version(upstream_tag, tag_str_fields)
  165. if not upstream_commit:
  166. raise GbpError("Couldn't find upstream version %s" %
  167. spec.upstreamversion)
  168. return upstream_commit
  169. def export_patches(repo, options):
  170. """Export patches from the pq branch into a packaging branch"""
  171. current = repo.get_branch()
  172. if is_pq_branch(current):
  173. base = pq_branch_base(current)
  174. gbp.log.info("On branch '%s', switching to '%s'" % (current, base))
  175. repo.set_branch(base)
  176. pq_branch = current
  177. else:
  178. pq_branch = pq_branch_name(current)
  179. spec = parse_spec(options, repo)
  180. upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
  181. export_treeish = pq_branch
  182. update_patch_series(repo, spec, upstream_commit, export_treeish, options)
  183. GitCommand('status')(['--', spec.specdir])
  184. def safe_patches(queue):
  185. """
  186. Safe the current patches in a temporary directory
  187. @param queue: an existing patch queue
  188. @return: safed queue (with patches in tmpdir)
  189. @rtype: tuple
  190. """
  191. tmpdir = tempfile.mkdtemp(prefix='patchimport_')
  192. safequeue = PatchSeries()
  193. if len(queue) > 0:
  194. gbp.log.debug("Safeing patches '%s' in '%s'" %
  195. (os.path.dirname(queue[0].path), tmpdir))
  196. for patch in queue:
  197. base, _archive_fmt, comp = parse_archive_filename(patch.path)
  198. uncompressors = {'gzip': gzip.open, 'bzip2': bz2.BZ2File}
  199. if comp in uncompressors:
  200. gbp.log.debug("Uncompressing '%s'" % os.path.basename(patch.path))
  201. src = uncompressors[comp](patch.path, 'r')
  202. dst_name = os.path.join(tmpdir, os.path.basename(base))
  203. elif comp:
  204. raise GbpError("Unsupported patch compression '%s', giving up"
  205. % comp)
  206. else:
  207. src = open(patch.path, 'r')
  208. dst_name = os.path.join(tmpdir, os.path.basename(patch.path))
  209. dst = open(dst_name, 'w')
  210. dst.writelines(src)
  211. src.close()
  212. dst.close()
  213. safequeue.append(patch)
  214. safequeue[-1].path = dst_name
  215. return safequeue
  216. def get_packager(spec):
  217. """Get packager information from spec"""
  218. if spec.packager:
  219. match = re.match(r'(?P<name>.*[^ ])\s*<(?P<email>\S*)>',
  220. spec.packager.strip())
  221. if match:
  222. return GitModifier(match.group('name'), match.group('email'))
  223. return GitModifier()
  224. def import_spec_patches(repo, options):
  225. """
  226. apply a series of patches in a spec/packaging dir to branch
  227. the patch-queue branch for 'branch'
  228. @param repo: git repository to work on
  229. @param options: command options
  230. """
  231. current = repo.get_branch()
  232. # Get spec and related information
  233. if is_pq_branch(current):
  234. base = pq_branch_base(current)
  235. if options.force:
  236. spec = parse_spec(options, repo, base)
  237. spec_treeish = base
  238. else:
  239. raise GbpError("Already on a patch-queue branch '%s' - doing "
  240. "nothing." % current)
  241. else:
  242. spec = parse_spec(options, repo)
  243. spec_treeish = None
  244. base = current
  245. upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
  246. packager = get_packager(spec)
  247. pq_branch = pq_branch_name(base)
  248. # Create pq-branch
  249. if repo.has_branch(pq_branch) and not options.force:
  250. raise GbpError("Patch-queue branch '%s' already exists. "
  251. "Try 'switch' instead." % pq_branch)
  252. try:
  253. if repo.get_branch() == pq_branch:
  254. repo.force_head(upstream_commit, hard=True)
  255. else:
  256. repo.create_branch(pq_branch, upstream_commit, force=True)
  257. except GitRepositoryError as err:
  258. raise GbpError("Cannot create patch-queue branch '%s': %s" %
  259. (pq_branch, err))
  260. # Put patches in a safe place
  261. if spec_treeish:
  262. packaging_tmp = tempfile.mkdtemp(prefix='dump_')
  263. packaging_tree = '%s:%s' % (spec_treeish, options.packaging_dir)
  264. dump_tree(repo, packaging_tmp, packaging_tree, with_submodules=False,
  265. recursive=False)
  266. spec.specdir = packaging_tmp
  267. in_queue = spec.patchseries()
  268. queue = safe_patches(in_queue)
  269. # Do import
  270. try:
  271. gbp.log.info("Switching to branch '%s'" % pq_branch)
  272. repo.set_branch(pq_branch)
  273. if not queue:
  274. return
  275. gbp.log.info("Trying to apply patches from branch '%s' onto '%s'" %
  276. (base, upstream_commit))
  277. for patch in queue:
  278. gbp.log.debug("Applying %s" % patch.path)
  279. apply_and_commit_patch(repo, patch, packager)
  280. except (GbpError, GitRepositoryError) as err:
  281. repo.set_branch(base)
  282. repo.delete_branch(pq_branch)
  283. raise GbpError('Import failed: %s' % err)
  284. gbp.log.info("Patches listed in '%s' imported on '%s'" % (spec.specfile,
  285. pq_branch))
  286. def rebase_pq(repo, options):
  287. """Rebase pq branch on the correct upstream version (from spec file)."""
  288. current = repo.get_branch()
  289. if is_pq_branch(current):
  290. base = pq_branch_base(current)
  291. spec = parse_spec(options, repo, base)
  292. else:
  293. base = current
  294. spec = parse_spec(options, repo)
  295. upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
  296. switch_to_pq_branch(repo, base)
  297. GitCommand("rebase")([upstream_commit])
  298. def build_parser(name):
  299. """Construct command line parser"""
  300. try:
  301. parser = GbpOptionParserRpm(command=os.path.basename(name),
  302. prefix='', usage=
  303. """%prog [options] action - maintain patches on a patch queue branch
  304. tions:
  305. export Export the patch queue / devel branch associated to the
  306. current branch into a patch series in and update the spec file
  307. import Create a patch queue / devel branch from spec file
  308. and patches in current dir.
  309. rebase Switch to patch queue / devel branch associated to the current
  310. branch and rebase against upstream.
  311. drop Drop (delete) the patch queue /devel branch associated to
  312. the current branch.
  313. apply Apply a patch
  314. switch Switch to patch-queue branch and vice versa.""")
  315. except configparser.ParsingError as err:
  316. gbp.log.err('Invalid config file: %s' % err)
  317. return None
  318. parser.add_boolean_config_file_option(option_name="patch-numbers",
  319. dest="patch_numbers")
  320. parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
  321. default=False, help="Verbose command execution")
  322. parser.add_option("--force", dest="force", action="store_true",
  323. default=False,
  324. help="In case of import even import if the branch already exists")
  325. parser.add_config_file_option(option_name="color", dest="color",
  326. type='tristate')
  327. parser.add_config_file_option(option_name="color-scheme",
  328. dest="color_scheme")
  329. parser.add_config_file_option(option_name="tmp-dir", dest="tmp_dir")
  330. parser.add_config_file_option(option_name="upstream-tag",
  331. dest="upstream_tag")
  332. parser.add_config_file_option(option_name="spec-file", dest="spec_file")
  333. parser.add_config_file_option(option_name="packaging-dir",
  334. dest="packaging_dir")
  335. return parser
  336. def parse_args(argv):
  337. """Parse command line arguments"""
  338. parser = build_parser(argv[0])
  339. if not parser:
  340. return None, None
  341. return parser.parse_args(argv)
  342. def main(argv):
  343. """Main function for the gbp pq-rpm command"""
  344. retval = 0
  345. (options, args) = parse_args(argv)
  346. if not options:
  347. return 1
  348. gbp.log.setup(options.color, options.verbose, options.color_scheme)
  349. if len(args) < 2:
  350. gbp.log.err("No action given.")
  351. return 1
  352. else:
  353. action = args[1]
  354. if args[1] in ["export", "import", "rebase", "drop", "switch", "convert"]:
  355. pass
  356. elif args[1] in ["apply"]:
  357. if len(args) != 3:
  358. gbp.log.err("No patch name given.")
  359. return 1
  360. else:
  361. patchfile = args[2]
  362. else:
  363. gbp.log.err("Unknown action '%s'." % args[1])
  364. return 1
  365. try:
  366. repo = RpmGitRepository(os.path.curdir)
  367. except GitRepositoryError:
  368. gbp.log.err("%s is not a git repository" % (os.path.abspath('.')))
  369. return 1
  370. try:
  371. # Create base temporary directory for this run
  372. init_tmpdir(options.tmp_dir, prefix='pq-rpm_')
  373. current = repo.get_branch()
  374. if action == "export":
  375. export_patches(repo, options)
  376. elif action == "import":
  377. import_spec_patches(repo, options)
  378. elif action == "drop":
  379. drop_pq(repo, current)
  380. elif action == "rebase":
  381. rebase_pq(repo, options)
  382. elif action == "apply":
  383. patch = Patch(patchfile)
  384. apply_single_patch(repo, current, patch, fallback_author=None)
  385. elif action == "switch":
  386. switch_pq(repo, current)
  387. except CommandExecFailed:
  388. retval = 1
  389. except GitRepositoryError as err:
  390. gbp.log.err("Git command failed: %s" % err)
  391. retval = 1
  392. except GbpError as err:
  393. if str(err):
  394. gbp.log.err(err)
  395. retval = 1
  396. finally:
  397. del_tmpdir()
  398. return retval
  399. if __name__ == '__main__':
  400. sys.exit(main(sys.argv))