pq-rpm: implement 'convert' subcommand
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>
Mon, 10 Feb 2014 08:49:29 +0000 (10:49 +0200)
committerMarkus Lehtonen <markus.lehtonen@linux.intel.com>
Fri, 14 Nov 2014 12:47:20 +0000 (14:47 +0200)
The new subcommand is intended to be a one-time-callable command for
converting a package to use the "orphan-packaging" development model and
git layout (where packaging files are maintained in a separate branch
and code development in another branch).

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
gbp/scripts/pq_rpm.py
tests/component/rpm/test_pq_rpm.py

index 64a4c7e..6c07c72 100755 (executable)
@@ -56,7 +56,11 @@ rebase         Switch to patch queue / devel branch associated to the current
 drop           Drop (delete) the patch queue /devel branch associated to
                the current branch.
 apply          Apply a patch
-switch         Switch to patch-queue branch and vice versa."""
+switch         Switch to patch-queue branch and vice versa.
+convert        [experimental] Convert package from single-branch development
+               model (packaging and source code changes in the same branch)
+               into the orphan-packaging plus patch-queue / development branch
+               development model."""
 
 def compress_patches(patches, compress_size=0):
     """
@@ -74,6 +78,15 @@ def compress_patches(patches, compress_size=0):
         ret_patches.append(os.path.basename(patch) + suffix)
     return ret_patches
 
+def is_ancestor(repo, parent, child):
+    """Check if commit is ancestor of another"""
+    parent_sha1 = repo.rev_parse("%s^0" % parent)
+    child_sha1 = repo.rev_parse("%s^0" % child)
+    try:
+        merge_base = repo.get_merge_base(parent_sha1, child_sha1)
+    except GitRepositoryError:
+        merge_base = None
+    return merge_base == parent_sha1
 
 def generate_patches(repo, start, squash, end, outdir, options):
     """
@@ -97,11 +110,8 @@ def generate_patches(repo, start, squash, end, outdir, options):
         end_commit_sha1 = repo.rev_parse("%s^0" % end_commit)
 
     start_sha1 = repo.rev_parse("%s^0" % start)
-    try:
-        merge_base = repo.get_merge_base(start_sha1, end_commit_sha1)
-    except GitRepositoryError:
-        merge_base = None
-    if merge_base != start_sha1:
+
+    if not is_ancestor(repo, start_sha1, end_commit_sha1):
         raise GbpError("Start commit '%s' not an ancestor of end commit "
                        "'%s'" % (start, end_commit))
     # Squash commits, if requested
@@ -201,6 +211,7 @@ def update_patch_series(repo, spec, start, end, options):
                                          spec.specdir, options)
     spec.update_patches(patches, commands)
     spec.write_spec_file()
+    return patches
 
 
 def parse_spec(options, repo, treeish=None):
@@ -315,7 +326,7 @@ def get_packager(spec):
             return GitModifier(match.group('name'), match.group('email'))
     return GitModifier()
 
-def import_extra_files(repo, commitish, files):
+def import_extra_files(repo, commitish, files, patch_ignore=True):
     """Import branch-specific gbp.conf files to current branch"""
     found = {}
     for fname in files:
@@ -337,10 +348,12 @@ def import_extra_files(repo, commitish, files):
         files = found.keys()
         gbp.log.debug('Adding/commiting %s' % files)
         repo.add_files(files, force=True)
-        commit_msg = ('Auto-import packaging file(s) from branch %s:\n'
-                      '    %s\n\nGbp: Ignore\nGbp-Rpm: Ignore' % (commitish,
-                      '    '.join(files)))
+        commit_msg = ("Auto-import file(s) from branch '%s':\n    %s\n" %
+                      (commitish, '    '.join(files)))
+        if patch_ignore:
+            commit_msg += "\nGbp: Ignore\nGbp-Rpm: Ignore"
         repo.commit_files(files, msg=commit_msg)
+    return found.keys()
 
 def import_spec_patches(repo, options):
     """
@@ -477,6 +490,68 @@ def apply_single_patch(repo, patchfile, options):
     patch = Patch(patchfile)
     apply_and_commit_patch(repo, patch, fallback_author=None)
 
+def convert_package(repo, options):
+    """Convert package to orphan-packaging model"""
+    old_packaging = repo.get_branch()
+    # Check if we're on pq branch, already
+    err_msg_base = "Seems you're already using orphan-packaging model - "
+    if is_pq_branch(old_packaging, options):
+        raise GbpError(err_msg_base + "you're on patch-queue branch")
+    # Check if a pq branch already exists
+    spec = parse_spec(options, repo, treeish=old_packaging)
+    pq_branch = pq_branch_name(old_packaging, options, spec.version)
+    if repo.has_branch(pq_branch):
+        pq_branch = pq_branch_name(old_packaging, options, spec.version)
+        raise GbpError(err_msg_base + "pq branch %s already exists" % pq_branch)
+    # Check that the current branch is based on upstream
+    upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag)
+    if not is_ancestor(repo, upstream_commit, old_packaging):
+        raise GbpError(err_msg_base + "%s is not based on upstream version %s" %
+                       (old_packaging, spec.upstreamversion))
+    # Check new branch
+    new_branch = old_packaging + "-orphan"
+    if repo.has_branch(new_branch):
+        if not options.force:
+            raise GbpError("Branch '%s' already exists!" % new_branch)
+        else:
+            gbp.log.info("Dropping branch '%s'" % new_branch)
+            repo.delete_branch(new_branch)
+
+    # Dump and commit packaging files
+    gbp.log.info("Importing packaging files from branch '%s' to '%s'" %
+                 (old_packaging, new_branch))
+    packaging_tree = '%s:%s' % (old_packaging, options.packaging_dir)
+    packaging_tmp = tempfile.mkdtemp(prefix='pack_', dir=options.tmp_dir)
+    dump_tree(repo, packaging_tmp, packaging_tree, with_submodules=False,
+              recursive=False)
+
+    msg = "Auto-import packaging files from branch '%s'" % old_packaging
+    repo.commit_dir(packaging_tmp, msg, new_branch, create_missing_branch=True)
+    repo.set_branch(new_branch)
+
+    # Generate patches
+    spec = SpecFile(spec.specfile)
+    patches = update_patch_series(repo, spec, upstream_commit, old_packaging,
+                                  options)
+
+    # Commit paches and spec
+    if patches:
+        gbp.log.info("Committing patches and modified spec file to git")
+        repo.add_files('.', untracked=False)
+        repo.add_files(patches)
+        msg = "Auto-generated patches from branch '%s'" % old_packaging
+        repo.commit_staged(msg=msg)
+
+    # Copy extra files
+    import_extra_files(repo, old_packaging, options.import_files,
+                       patch_ignore=False)
+
+    gbp.log.info("Package successfully converted to orphan-packaging.")
+    gbp.log.info("You're now on the new '%s' packaging branch (the old "
+            "packaging branch '%s' was left intact)." %
+            (new_branch, old_packaging))
+    gbp.log.info("Please check all files and test building the package!")
+
 
 def opt_split_cb(option, opt_str, value, parser):
     """Split option string into a list"""
@@ -544,7 +619,7 @@ def main(argv):
     else:
         action = args[1]
 
-    if args[1] in ["export", "import", "rebase", "drop", "switch"]:
+    if args[1] in ["export", "import", "rebase", "drop", "switch", "convert"]:
         pass
     elif args[1] in ["apply"]:
         if len(args) != 3:
@@ -582,6 +657,8 @@ def main(argv):
             apply_single_patch(repo, patchfile, options)
         elif action == "switch":
             switch_pq(repo, options)
+        elif action == "convert":
+            convert_package(repo, options)
     except CommandExecFailed:
         retval = 1
     except GitRepositoryError as err:
index 19b04d9..69b760f 100644 (file)
@@ -252,6 +252,45 @@ class TestPqRpm(RpmRepoTestBase):
         self._check_repo_state(repo, 'development/master', branches,
                                upstr_files + ['mydir/myfile.txt'])
 
+    def test_convert(self):
+        """Basic test for convert action"""
+        repo = self.init_test_repo('gbp-test2')
+        branches = repo.get_local_branches() + ['master-orphan']
+        files = ['bar.tar.gz', 'foo.txt', 'gbp-test2.spec',
+                 'gbp-test2-alt.spec', 'my.patch', '0001-My-addition.patch']
+
+        # First should fail because 'master-orphan' branch already exists
+        eq_(mock_pq(['convert']), 1)
+        self._check_log(-1, "gbp:error: Branch 'master-orphan' already exists")
+
+        # Re-try with force
+        eq_(mock_pq(['convert', '--import-files=', '--force']), 0)
+        self._check_repo_state(repo, 'master-orphan', branches, files)
+
+    def test_convert_fail(self):
+        """Tests for convert action error cases"""
+        repo = self.init_test_repo('gbp-test')
+        branches = repo.get_local_branches()
+
+        # Already on orphan packaging branch
+        eq_(mock_pq(['convert']), 1)
+        self._check_repo_state(repo, 'master', branches)
+        self._check_log(-1, ".*is not based on upstream version")
+
+        # Create a pq branch and try from there
+        eq_(mock_pq(['import']), 0)
+        eq_(mock_pq(['convert']), 1)
+        self._check_repo_state(repo, 'development/master',
+                               branches + ['development/master'])
+        self._check_log(-1, ".*you're on patch-queue branch")
+
+        # Switch back to orphan packaging branch and try again
+        eq_(mock_pq(['switch']), 0)
+        eq_(mock_pq(['convert']), 1)
+        self._check_repo_state(repo, 'master',
+                               branches + ['development/master'])
+        self._check_log(-1, r".*pq branch \S+ already exists")
+
     def test_option_patch_numbers(self):
         """Test the --patch-numbers cmdline option"""
         repo = self.init_test_repo('gbp-test')