Hello, On Fri, 2014-11-28 at 14:55 +0100, Guido Günther wrote: > On Fri, Nov 28, 2014 at 12:11:01PM +0200, Markus Lehtonen wrote: > [..snip..] > > If you're willing to wait for few days I could look into this and provide > > a patchset with minimal pq-rpm implementation (i.e. all the new cmdline > > options, even configurable branch names, removed). What I'd like to have > > there are the unit tests. > > That would be awesome! I'd be great to have a second tool merged.
The attached series implements an initial version of the pq-rpm tool. The first four patches (0001-0004) are required to make the actual pq-rpm tool to work correctly. The next four patches (0005-0008) are requirements for the unit tests. The last patch finally implements gbp-pq-rpm tool itself. This series (plus some additional features) is also available in feature/pq-rpm branch in my Github repository: git clone git://github.com/marquiz/git-buildpackage-rpm.git -b feature/pq-rpm Best Regards, Markus Lehtonen
>From 4ce789c8b761e5f08f293df8a6b8879673722d93 Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Fri, 28 Nov 2014 18:28:51 +0200 Subject: [PATCH 1/9] pq: move switch_pq() to common So that it can be re-used by the upcoming pq-rpm tool. Signed-off-by: Markus Lehtonen <[email protected]> --- gbp/scripts/common/pq.py | 10 ++++++++++ gbp/scripts/pq.py | 12 +----------- tests/13_test_gbp_pq.py | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/gbp/scripts/common/pq.py b/gbp/scripts/common/pq.py index d3c07d1..f7bae23 100644 --- a/gbp/scripts/common/pq.py +++ b/gbp/scripts/common/pq.py @@ -304,3 +304,13 @@ def drop_pq(repo, branch): gbp.log.info("Dropped branch '%s'." % pq_branch) else: gbp.log.info("No patch queue branch found - doing nothing.") + + +def switch_pq(repo, current): + """Switch to patch-queue branch if on base branch and vice versa""" + if is_pq_branch(current): + base = pq_branch_base(current) + gbp.log.info("Switching to %s" % base) + repo.checkout(base) + else: + switch_to_pq_branch(repo, current) diff --git a/gbp/scripts/pq.py b/gbp/scripts/pq.py index 194145e..c32a36f 100755 --- a/gbp/scripts/pq.py +++ b/gbp/scripts/pq.py @@ -33,7 +33,7 @@ from gbp.patch_series import (PatchSeries, Patch) from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base, parse_gbp_commands, format_patch, switch_to_pq_branch, apply_single_patch, - apply_and_commit_patch, + apply_and_commit_patch, switch_pq, drop_pq, get_maintainer_from_control) from gbp.dch import extract_bts_cmds @@ -279,16 +279,6 @@ def rebase_pq(repo, branch): GitCommand("rebase")([base]) -def switch_pq(repo, current): - """Switch to patch-queue branch if on base branch and vice versa""" - if is_pq_branch(current): - base = pq_branch_base(current) - gbp.log.info("Switching to %s" % base) - repo.checkout(base) - else: - switch_to_pq_branch(repo, current) - - def build_parser(name): try: parser = GbpOptionParserDebian(command=os.path.basename(name), diff --git a/tests/13_test_gbp_pq.py b/tests/13_test_gbp_pq.py index 910ce20..d88f7ae 100644 --- a/tests/13_test_gbp_pq.py +++ b/tests/13_test_gbp_pq.py @@ -21,7 +21,7 @@ import os import logging import unittest -from gbp.scripts.pq import generate_patches, switch_pq, export_patches +from gbp.scripts.pq import generate_patches, export_patches import gbp.scripts.common.pq as pq import gbp.patch_series import tests.testutils as testutils @@ -145,12 +145,12 @@ class TestExport(testutils.DebianGitTestRepo): """Test if we drop the patch-queue branch with --drop""" repo = self.repo start = repo.get_branch() - pq = os.path.join('patch-queue', start) - switch_pq(repo, start) - self.assertEqual(repo.get_branch(), pq) - export_patches(repo, pq, TestExport.Options) + pq_branch = os.path.join('patch-queue', start) + pq.switch_pq(repo, start) + self.assertEqual(repo.get_branch(), pq_branch) + export_patches(repo, pq_branch, TestExport.Options) self.assertEqual(repo.get_branch(), start) - self.assertFalse(repo.has_branch(pq)) + self.assertFalse(repo.has_branch(pq_branch)) def _patch_path(name): -- 1.8.4.5
>From f153c5676cbd9f57310e97da67c1cb4ddb6bed4a Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Fri, 14 Sep 2012 13:40:14 +0300 Subject: [PATCH 2/9] GitRepository: add diff_status method This is a method of getting the filename and status information of a diff. That is, a list of files that changed and their status, "added", "modified" etc. Signed-off-by: Markus Lehtonen <[email protected]> --- gbp/git/repository.py | 27 +++++++++++++++++++++++++++ tests/test_GitRepository.py | 13 +++++++++++++ 2 files changed, 40 insertions(+) diff --git a/gbp/git/repository.py b/gbp/git/repository.py index 23f9482..edb8e21 100644 --- a/gbp/git/repository.py +++ b/gbp/git/repository.py @@ -1639,6 +1639,33 @@ class GitRepository(object): if ret: raise GitRepositoryError("Git diff failed") return output + + def diff_status(self, obj1, obj2): + """ + Get file-status of two git repository objects + + @param obj1: first object + @type obj1: C{str} + @param obj2: second object + @type obj2: C{str} + @return: name-status + @rtype: C{defaultdict} of C{str} + """ + options = GitArgs('--name-status', '-z', obj1, obj2) + output, stderr, ret = self._git_inout('diff', options.args) + + elements = output.split('\x00') + result = defaultdict(list) + + while elements[0] != '': + status = elements.pop(0)[0] + filepath = elements.pop(0) + # Expect to have two filenames for renames and copies + if status in ['R', 'C']: + filepath = elements.pop(0) + '\x00' + filepath + result[status].append(filepath) + + return result #} def archive(self, format, prefix, output, treeish, **kwargs): diff --git a/tests/test_GitRepository.py b/tests/test_GitRepository.py index 427370d..c5c5849 100644 --- a/tests/test_GitRepository.py +++ b/tests/test_GitRepository.py @@ -493,6 +493,19 @@ def test_diff(): True """ +def test_diff_status(): + """ + Methods tested: + - L{gbp.git.GitRepository.diff_status} + + >>> import gbp.git + >>> repo = gbp.git.GitRepository(repo_dir) + >>> repo.diff_status("HEAD", "HEAD") + defaultdict(<type 'list'>, {}) + >>> repo.diff_status("HEAD~1", "HEAD") + defaultdict(<type 'list'>, {'M': ['testfile']}) + """ + def test_mirror_clone(): """ Mirror a repository -- 1.8.4.5
>From 3e55ea3f3ce3876dd07002e4b1153271875a71c6 Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Fri, 13 Sep 2013 09:53:16 +0300 Subject: [PATCH 3/9] GitRepository.create_branch: add 'force' option Signed-off-by: Markus Lehtonen <[email protected]> --- gbp/git/repository.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gbp/git/repository.py b/gbp/git/repository.py index edb8e21..96921b1 100644 --- a/gbp/git/repository.py +++ b/gbp/git/repository.py @@ -295,16 +295,18 @@ class GitRepository(object): args = GitArgs("-m", branch, newbranch) self._git_command("branch", args.args) - def create_branch(self, branch, rev=None): + def create_branch(self, branch, rev=None, force=False): """ Create a new branch @param branch: the branch's name @param rev: where to start the branch from + @param force: reset branch HEAD to start point, if it already exists If rev is None the branch starts form the current HEAD. """ args = GitArgs(branch) + args.add_true(force, '--force') args.add_true(rev, rev) self._git_command("branch", args.args) -- 1.8.4.5
>From 7ac39064508965b86d8700af5568ebb8619cbbaf Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Tue, 17 Sep 2013 15:13:40 +0300 Subject: [PATCH 4/9] buildpackage/dump_tree: add 'recursive' option For selecting whether to dump all the files recursively or just the top level directory of the tree. Signed-off-by: Markus Lehtonen <[email protected]> --- gbp/scripts/common/buildpackage.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gbp/scripts/common/buildpackage.py b/gbp/scripts/common/buildpackage.py index 0522cd6..2e53b78 100644 --- a/gbp/scripts/common/buildpackage.py +++ b/gbp/scripts/common/buildpackage.py @@ -101,13 +101,19 @@ def git_archive_single(treeish, output, prefix, comp_type, comp_level, comp_opts #{ Functions to handle export-dir -def dump_tree(repo, export_dir, treeish, with_submodules): +def dump_tree(repo, export_dir, treeish, with_submodules, recursive=True): "dump a tree to output_dir" output_dir = os.path.dirname(export_dir) prefix = sanitize_prefix(os.path.basename(export_dir)) + if recursive: + paths = [] + else: + paths = ["'%s'" % nam for _mod, typ, _sha, nam in + repo.list_tree(treeish) if typ == 'blob'] pipe = pipes.Template() - pipe.prepend('git archive --format=tar --prefix=%s %s' % (prefix, treeish), '.-') + pipe.prepend('git archive --format=tar --prefix=%s %s -- %s' % + (prefix, treeish, ' '.join(paths)), '.-') pipe.append('tar -C %s -xf -' % output_dir, '-.') top = os.path.abspath(os.path.curdir) try: @@ -115,7 +121,7 @@ def dump_tree(repo, export_dir, treeish, with_submodules): if ret: raise GbpError("Error in dump_tree archive pipe") - if with_submodules: + if recursive and with_submodules: if repo.has_submodules(): repo.update_submodules() for (subdir, commit) in repo.get_submodules(treeish): -- 1.8.4.5
>From 970e6a24851b3cae8589e8e85a6ad0e800bdc52b Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Wed, 2 Oct 2013 16:35:32 +0300 Subject: [PATCH 5/9] ComponentTestBase: add a per-class toplevel temp dir Signed-off-by: Markus Lehtonen <[email protected]> --- tests/component/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/component/__init__.py b/tests/component/__init__.py index 7abb16d..44a84e2 100644 --- a/tests/component/__init__.py +++ b/tests/component/__init__.py @@ -85,6 +85,9 @@ class ComponentTestBase(object): # Don't let git see that we're (possibly) under a git directory cls.orig_env = os.environ.copy() os.environ['GIT_CEILING_DIRECTORIES'] = os.getcwd() + # Create a top-level tmpdir for the test + cls._tmproot = tempfile.mkdtemp(prefix='gbp_%s_' % cls.__name__, + dir='.') @classmethod def teardown_class(cls): @@ -92,6 +95,9 @@ class ComponentTestBase(object): # Return original environment os.environ.clear() os.environ.update(cls.orig_env) + # Remove top-level tmpdir + if not os.getenv("GBP_TESTS_NOCLEAN"): + shutil.rmtree(cls._tmproot) def __init__(self): """Object initialization""" @@ -104,7 +110,8 @@ class ComponentTestBase(object): """Test case setup""" # Change to a temporary directory self._orig_dir = os.getcwd() - self._tmpdir = tempfile.mkdtemp(prefix='gbp_%s_' % __name__, dir='.') + self._tmpdir = tempfile.mkdtemp(prefix='gbp_%s_' % __name__, + dir=self._tmproot) os.chdir(self._tmpdir) self._capture_log(True) -- 1.8.4.5
>From 5b5460fcfb1e222d548899f1e70ff2f1942c737c Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Thu, 26 Jun 2014 10:01:18 +0300 Subject: [PATCH 6/9] ComponentTestBase: add check_files() method Signed-off-by: Markus Lehtonen <[email protected]> --- tests/component/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/component/__init__.py b/tests/component/__init__.py index 44a84e2..ae81577 100644 --- a/tests/component/__init__.py +++ b/tests/component/__init__.py @@ -125,6 +125,15 @@ class ComponentTestBase(object): self._capture_log(False) + @staticmethod + def check_files(reference, filelist): + """Compare two file lists""" + extra = set(filelist) - set(reference) + missing = set(reference) - set(filelist) + assert_msg = "Unexpected files: %s, Missing files: %s" % \ + (list(extra), list(missing)) + assert not extra and not missing, assert_msg + @classmethod def _check_repo_state(cls, repo, current_branch, branches, files=None): """Check that repository is clean and given branches exist""" @@ -148,10 +157,7 @@ class ComponentTestBase(object): for dirname in dirnames: local.add(os.path.relpath(os.path.join(dirpath, dirname), repo.path) + '/') - extra = local - set(files) - ok_(not extra, "Unexpected files in repo: %s" % list(extra)) - missing = set(files) - local - ok_(not missing, "Files missing from repo: %s" % list(missing)) + cls.check_files(files, local) def _capture_log(self, capture=True): """ Capture log""" -- 1.8.4.5
>From 80167535622c09350f5fc2e41ac092f37591c71e Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Fri, 18 Jul 2014 15:37:48 +0300 Subject: [PATCH 7/9] ComponentTestBase: add dirs argument to _check_repo_state() Make difference between regular files and directories, eliminating the requirement of listing directories in the file list. Signed-off-by: Markus Lehtonen <[email protected]> --- tests/component/__init__.py | 21 +++++++++++++-------- tests/component/rpm/test_import_srpm.py | 11 +++++------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/component/__init__.py b/tests/component/__init__.py index ae81577..cfa0422 100644 --- a/tests/component/__init__.py +++ b/tests/component/__init__.py @@ -135,7 +135,8 @@ class ComponentTestBase(object): assert not extra and not missing, assert_msg @classmethod - def _check_repo_state(cls, repo, current_branch, branches, files=None): + def _check_repo_state(cls, repo, current_branch, branches, files=None, + dirs=None): """Check that repository is clean and given branches exist""" branch = repo.branch eq_(branch, current_branch) @@ -144,20 +145,24 @@ class ComponentTestBase(object): assert_msg = "Branches: expected %s, found %s" % (branches, local_branches) eq_(set(local_branches), set(branches), assert_msg) - if files is not None: + if files is not None or dirs is not None: # Get files of the working copy recursively - local = set() + local_f = set() + local_d = set() for dirpath, dirnames, filenames in os.walk(repo.path): # Skip git dir(s) if '.git' in dirnames: dirnames.remove('.git') for filename in filenames: - local.add(os.path.relpath(os.path.join(dirpath, filename), - repo.path)) + local_f.add(os.path.relpath(os.path.join(dirpath, filename), + repo.path)) for dirname in dirnames: - local.add(os.path.relpath(os.path.join(dirpath, dirname), - repo.path) + '/') - cls.check_files(files, local) + local_d.add(os.path.relpath(os.path.join(dirpath, dirname), + repo.path) + '/') + if files is not None: + cls.check_files(files, local_f) + if dirs is not None: + cls.check_files(dirs, local_d) def _capture_log(self, capture=True): """ Capture log""" diff --git a/tests/component/rpm/test_import_srpm.py b/tests/component/rpm/test_import_srpm.py index d0e7170..acd8443 100644 --- a/tests/component/rpm/test_import_srpm.py +++ b/tests/component/rpm/test_import_srpm.py @@ -94,7 +94,7 @@ class TestImportPacked(ComponentTestBase): srpm = os.path.join(DATA_DIR, 'gbp-test-native-1.0-1.src.rpm') eq_(mock_import(['--native', srpm]), 0) # Check repository state - files = {'.gbp.conf', 'Makefile', 'README', 'dummy.sh', 'packaging/', + files = {'.gbp.conf', 'Makefile', 'README', 'dummy.sh', 'packaging/gbp-test-native.spec'} repo = GitRepository('gbp-test-native') self._check_repo_state(repo, 'master', ['master'], files) @@ -195,11 +195,10 @@ class TestImportPacked(ComponentTestBase): srpm]), 0) # Check repository state repo = GitRepository('gbp-test2') - files = {'Makefile', 'README', 'dummy.sh', 'packaging/', - 'packaging/bar.tar.gz', 'packaging/foo.txt', - 'packaging/gbp-test2.spec', 'packaging/gbp-test2-alt.spec', - 'packaging/my.patch', 'packaging/my2.patch', - 'packaging/my3.patch'} + files = {'Makefile', 'README', 'dummy.sh', 'packaging/bar.tar.gz', + 'packaging/foo.txt', 'packaging/gbp-test2.spec', + 'packaging/gbp-test2-alt.spec', 'packaging/my.patch', + 'packaging/my2.patch', 'packaging/my3.patch'} self._check_repo_state(repo, 'pack', ['pack', 'orig'], files) eq_(len(repo.get_commits()), 2) # Check packaging dir -- 1.8.4.5
>From d24b8d2af305864b96087e764a2964db8fc897ef Mon Sep 17 00:00:00 2001 From: Ed Bartosh <[email protected]> Date: Wed, 6 Jun 2012 14:45:44 +0300 Subject: [PATCH 8/9] GitRepository: Implement status method Simple wrapper to the git-status command. Signed-off-by: Ed Bartosh <[email protected]> Signed-off-by: Markus Lehtonen <[email protected]> --- gbp/git/repository.py | 33 +++++++++++++++++++++++++++++++++ tests/test_GitRepository.py | 22 ++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/gbp/git/repository.py b/gbp/git/repository.py index 96921b1..6559921 100644 --- a/gbp/git/repository.py +++ b/gbp/git/repository.py @@ -820,6 +820,39 @@ class GitRepository(object): if ret: raise GitRepositoryError("Can't execute repository clean: %s" % err) + def status(self, pathlist=None): + """ + Check status of repository. + + @param pathlist: List of paths to check status for + @type pathlist: C{list} + @return C{dict} of C{lists} of paths, where key is a git status flag. + @rtype C{dict} + """ + options = GitArgs('--porcelain', '-z') + if pathlist: + for path in pathlist: + options.add(path) + + out, err, ret = self._git_inout('status', options.args, + extra_env={'LC_ALL': 'C'}) + if ret: + raise GitRepositoryError("Can't get repository status: %s" % err) + + elements = out.split('\x00') + result = defaultdict(list) + + while elements[0] != '': + element = elements.pop(0) + status = element[:2] + filepath = element[3:] + # Expect to have two filenames for renames and copies + if status[0] in ['R', 'C']: + filepath = elements.pop(0) + '\x00' + filepath + result[status].append(filepath) + + return result + def is_empty(self): """ Is the repository empty? diff --git a/tests/test_GitRepository.py b/tests/test_GitRepository.py index c5c5849..d9fc539 100644 --- a/tests/test_GitRepository.py +++ b/tests/test_GitRepository.py @@ -881,6 +881,28 @@ def test_get_merge_base(): GitRepositoryError: Failed to get common ancestor: fatal: Not a valid object name doesnotexist """ +def test_status(): + r""" + Methods tested: + - L{gbp.git.GitRepository.status} + + >>> import gbp.git, os, shutil + >>> repo = gbp.git.GitRepository(repo_dir) + >>> fname = os.path.join(repo.path, "test_status") + >>> shutil.copy(os.path.join(repo.path, ".git/HEAD"), fname) + >>> repo.status().items() + [('??', ['test_status'])] + >>> repo.status(['bla*']).items() + [] + >>> repo.status(['te*']).items() + [('??', ['test_status'])] + >>> repo.add_files(repo.path, force=True) + >>> repo.commit_all(msg='added %s' % fname) + >>> _ = repo._git_inout('mv', [fname, fname + 'new']) + >>> repo.status().items() + [('R ', ['test_status\x00test_statusnew'])] + """ + def test_cmd_has_feature(): r""" Methods tested: -- 1.8.4.5
>From 7ed50c4b2395fea23077caadd8a5e09f74c57a02 Mon Sep 17 00:00:00 2001 From: Markus Lehtonen <[email protected]> Date: Thu, 12 Jan 2012 15:38:29 +0200 Subject: [PATCH 9/9] Introduce gbp-pq-rpm Initial version of gbp-pq-rpm - a tool for managing patch queues for rpm packages. The functionality more or less corresponds to that of the (Debian) gbp-pq. The only major difference probably being (in addition to the obvious of working with .spec files instead of debian/) is that patches are always imported on top of the upstream version, not on top of the packaging branch (which might not even contain any source code). Signed-off-by: Markus Lehtonen <[email protected]> Signed-off-by: Olev Kartau <[email protected]> --- debian/git-buildpackage-rpm.install | 1 + gbp/config.py | 4 + gbp/scripts/pq_rpm.py | 464 ++++++++++++++++++++++++++++++++++++ tests/component/rpm/__init__.py | 78 +++++- tests/component/rpm/data | 2 +- tests/component/rpm/test_pq_rpm.py | 358 ++++++++++++++++++++++++++++ 6 files changed, 905 insertions(+), 2 deletions(-) create mode 100755 gbp/scripts/pq_rpm.py create mode 100644 tests/component/rpm/test_pq_rpm.py diff --git a/debian/git-buildpackage-rpm.install b/debian/git-buildpackage-rpm.install index 67c0309..2568b4c 100644 --- a/debian/git-buildpackage-rpm.install +++ b/debian/git-buildpackage-rpm.install @@ -1,2 +1,3 @@ usr/lib/python2.?/dist-packages/gbp/rpm/ usr/lib/python2.7/dist-packages/gbp/scripts/import_srpm.py +usr/lib/python2.7/dist-packages/gbp/scripts/pq_rpm.py diff --git a/gbp/config.py b/gbp/config.py index 174eba4..eecf547 100644 --- a/gbp/config.py +++ b/gbp/config.py @@ -541,6 +541,7 @@ class GbpOptionParserRpm(GbpOptionParser): 'packaging-branch' : 'master', 'packaging-dir' : '', 'packaging-tag' : 'packaging/%(version)s', + 'spec-file' : '', }) help = dict(GbpOptionParser.help) @@ -560,6 +561,9 @@ class GbpOptionParserRpm(GbpOptionParser): 'packaging-tag': "Format string for packaging tags, RPM counterpart of the " "'debian-tag' option, default is '%(packaging-tag)s'", + 'spec-file': + "Spec file to use, causes the packaging-dir option to be " + "ignored, default is '%(spec-file)s'", }) # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: diff --git a/gbp/scripts/pq_rpm.py b/gbp/scripts/pq_rpm.py new file mode 100755 index 0000000..3d1c4bc --- /dev/null +++ b/gbp/scripts/pq_rpm.py @@ -0,0 +1,464 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2011 Guido Günther <[email protected]> +# (C) 2012-2014 Intel Corporation <[email protected]> +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +"""manage patches in a patch queue""" + +import ConfigParser +import bz2 +import errno +import gzip +import os +import re +import shutil +import sys + +import gbp.log +import gbp.tmpfile as tempfile +from gbp.config import GbpOptionParserRpm +from gbp.rpm.git import GitRepositoryError, RpmGitRepository +from gbp.git.modifier import GitModifier +from gbp.command_wrappers import GitCommand, CommandExecFailed +from gbp.errors import GbpError +from gbp.patch_series import PatchSeries, Patch +from gbp.pkg import parse_archive_filename +from gbp.rpm import (SpecFile, NoSpecError, guess_spec, guess_spec_repo, + spec_from_repo) +from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base, + parse_gbp_commands, format_patch, format_diff, + switch_to_pq_branch, apply_single_patch, apply_and_commit_patch, + drop_pq, switch_pq) +from gbp.scripts.common.buildpackage import dump_tree + + +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, end, outdir, options): + """ + Generate patch files from git + """ + gbp.log.info("Generating patches from git (%s..%s)" % (start, end)) + patches = [] + commands = {} + for treeish in [start, end]: + if not repo.has_treeish(treeish): + raise GbpError('Invalid treeish object %s' % treeish) + + start_sha1 = repo.rev_parse("%s^0" % start) + try: + end_commit = end + except GitRepositoryError: + # In case of plain tree-ish objects, assume current branch head is the + # last commit + end_commit = "HEAD" + end_commit_sha1 = repo.rev_parse("%s^0" % end_commit) + + start_sha1 = repo.rev_parse("%s^0" % start) + + 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)) + # Check for merge commits, squash if merges found + merges = repo.get_commits(start, end_commit, options=['--merges']) + if merges: + # Shorten SHA1s + start_sha1 = repo.rev_parse(start, short=7) + merge_sha1 = repo.rev_parse(merges[0], short=7) + patch_fn = format_diff(outdir, None, repo, start_sha1, merge_sha1) + if patch_fn: + gbp.log.info("Merge commits found! Diff between %s..%s written " + "into one monolithic diff" % (start_sha1, merge_sha1)) + patches.append(patch_fn) + start = merge_sha1 + + # Generate patches + for commit in reversed(repo.get_commits(start, end_commit)): + info = repo.get_commit_info(commit) + cmds = parse_gbp_commands(info, 'gbp-rpm', ('ignore'), + ('if', 'ifarch')) + if not 'ignore' in cmds: + patch_fn = format_patch(outdir, repo, info, patches, + options.patch_numbers) + if patch_fn: + commands[os.path.basename(patch_fn)] = cmds + else: + gbp.log.info('Ignoring commit %s' % info['id']) + + # Generate diff to the tree-ish object + if end_commit != end: + gbp.log.info("Generating diff file %s..%s" % (end_commit, end)) + patch_fn = format_diff(outdir, None, repo, end_commit, end, + options.patch_export_ignore_path) + if patch_fn: + patches.append(patch_fn) + + return patches, commands + + +def rm_patch_files(spec): + """ + Delete the patch files listed in the spec file. Doesn't delete patches + marked as not maintained by gbp. + """ + # Remove all old patches from the spec dir + for patch in spec.patchseries(unapplied=True): + gbp.log.debug("Removing '%s'" % patch.path) + try: + os.unlink(patch.path) + except OSError as err: + if err.errno != errno.ENOENT: + raise GbpError("Failed to remove patch: %s" % err) + else: + gbp.log.debug("Patch %s does not exist." % patch.path) + + +def update_patch_series(repo, spec, start, end, options): + """ + Export patches to packaging directory and update spec file accordingly. + """ + # Unlink old patch files and generate new patches + rm_patch_files(spec) + + patches, commands = generate_patches(repo, start, end, + spec.specdir, options) + spec.update_patches(patches, commands) + spec.write_spec_file() + return patches + + +def parse_spec(options, repo, treeish=None): + """ + Find and parse spec file. + + If treeish is given, try to find the spec file from that. Otherwise, search + for the spec file in the working copy. + """ + try: + if options.spec_file: + options.packaging_dir = os.path.dirname(options.spec_file) + if not treeish: + spec = SpecFile(options.spec_file) + else: + spec = spec_from_repo(repo, treeish, options.spec_file) + else: + preferred_name = os.path.basename(repo.path) + '.spec' + if not treeish: + spec = guess_spec(options.packaging_dir, True, preferred_name) + else: + spec = guess_spec_repo(repo, treeish, options.packaging_dir, + True, preferred_name) + except NoSpecError as err: + raise GbpError("Can't parse spec: %s" % err) + relpath = spec.specpath if treeish else os.path.relpath(spec.specpath, + repo.path) + gbp.log.debug("Using '%s' from '%s'" % (relpath, treeish or 'working copy')) + return spec + + +def find_upstream_commit(repo, spec, upstream_tag): + """Find commit corresponding upstream version""" + tag_str_fields = {'upstreamversion': spec.upstreamversion, + 'version': spec.upstreamversion} + upstream_commit = repo.find_version(upstream_tag, tag_str_fields) + if not upstream_commit: + raise GbpError("Couldn't find upstream version %s" % + spec.upstreamversion) + return upstream_commit + + +def export_patches(repo, options): + """Export patches from the pq branch into a packaging branch""" + current = repo.get_branch() + if is_pq_branch(current): + base = pq_branch_base(current) + gbp.log.info("On branch '%s', switching to '%s'" % (current, base)) + repo.set_branch(base) + pq_branch = current + else: + pq_branch = pq_branch_name(current) + spec = parse_spec(options, repo) + upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag) + export_treeish = pq_branch + + update_patch_series(repo, spec, upstream_commit, export_treeish, options) + + GitCommand('status')(['--', spec.specdir]) + + +def safe_patches(queue, tmpdir_base): + """ + Safe the current patches in a temporary directory + below 'tmpdir_base'. Also, uncompress compressed patches here. + + @param queue: an existing patch queue + @param tmpdir_base: base under which to create tmpdir + @return: tmpdir and a safed queue (with patches in tmpdir) + @rtype: tuple + """ + + tmpdir = tempfile.mkdtemp(dir=tmpdir_base, prefix='patchimport_') + safequeue = PatchSeries() + + if len(queue) > 0: + gbp.log.debug("Safeing patches '%s' in '%s'" % + (os.path.dirname(queue[0].path), tmpdir)) + for patch in queue: + base, _archive_fmt, comp = parse_archive_filename(patch.path) + uncompressors = {'gzip': gzip.open, 'bzip2': bz2.BZ2File} + if comp in uncompressors: + gbp.log.debug("Uncompressing '%s'" % os.path.basename(patch.path)) + src = uncompressors[comp](patch.path, 'r') + dst_name = os.path.join(tmpdir, os.path.basename(base)) + elif comp: + raise GbpError("Unsupported patch compression '%s', giving up" + % comp) + else: + src = open(patch.path, 'r') + dst_name = os.path.join(tmpdir, os.path.basename(patch.path)) + + dst = open(dst_name, 'w') + dst.writelines(src) + src.close() + dst.close() + + safequeue.append(patch) + safequeue[-1].path = dst_name + + return safequeue + + +def get_packager(spec): + """Get packager information from spec""" + if spec.packager: + match = re.match(r'(?P<name>.*[^ ])\s*<(?P<email>\S*)>', + spec.packager.strip()) + if match: + return GitModifier(match.group('name'), match.group('email')) + return GitModifier() + + +def import_spec_patches(repo, options): + """ + apply a series of patches in a spec/packaging dir to branch + the patch-queue branch for 'branch' + + @param repo: git repository to work on + @param options: command options + """ + current = repo.get_branch() + # Get spec and related information + if is_pq_branch(current): + base = pq_branch_base(current) + if options.force: + spec = parse_spec(options, repo, base) + spec_treeish = base + else: + raise GbpError("Already on a patch-queue branch '%s' - doing " + "nothing." % current) + else: + spec = parse_spec(options, repo) + spec_treeish = None + base = current + upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag) + packager = get_packager(spec) + pq_branch = pq_branch_name(base) + + # Create pq-branch + if repo.has_branch(pq_branch) and not options.force: + raise GbpError("Patch-queue branch '%s' already exists. " + "Try 'switch' instead." % pq_branch) + try: + if repo.get_branch() == pq_branch: + repo.force_head(upstream_commit, hard=True) + else: + repo.create_branch(pq_branch, upstream_commit, force=True) + except GitRepositoryError as err: + raise GbpError("Cannot create patch-queue branch '%s': %s" % + (pq_branch, err)) + + # Put patches in a safe place + if spec_treeish: + packaging_tmp = tempfile.mkdtemp(prefix='dump_', dir=options.tmp_dir) + packaging_tree = '%s:%s' % (spec_treeish, options.packaging_dir) + dump_tree(repo, packaging_tmp, packaging_tree, with_submodules=False, + recursive=False) + spec.specdir = packaging_tmp + in_queue = spec.patchseries() + queue = safe_patches(in_queue, options.tmp_dir) + # Do import + try: + gbp.log.info("Switching to branch '%s'" % pq_branch) + repo.set_branch(pq_branch) + + if not queue: + return + gbp.log.info("Trying to apply patches from branch '%s' onto '%s'" % + (base, upstream_commit)) + for patch in queue: + gbp.log.debug("Applying %s" % patch.path) + apply_and_commit_patch(repo, patch, packager) + except (GbpError, GitRepositoryError) as err: + repo.set_branch(base) + repo.delete_branch(pq_branch) + raise GbpError('Import failed: %s' % err) + + gbp.log.info("Patches listed in '%s' imported on '%s'" % (spec.specfile, + pq_branch)) + + +def rebase_pq(repo, options): + """Rebase pq branch on the correct upstream version (from spec file).""" + current = repo.get_branch() + if is_pq_branch(current): + base = pq_branch_base(current) + spec = parse_spec(options, repo, base) + else: + base = current + spec = parse_spec(options, repo) + upstream_commit = find_upstream_commit(repo, spec, options.upstream_tag) + + switch_to_pq_branch(repo, base) + GitCommand("rebase")([upstream_commit]) + + +def build_parser(name): + """Construct command line parser""" + try: + parser = GbpOptionParserRpm(command=os.path.basename(name), + prefix='', usage= +"""%prog [options] action - maintain patches on a patch queue branch +tions: +export Export the patch queue / devel branch associated to the + current branch into a patch series in and update the spec file +import Create a patch queue / devel branch from spec file + and patches in current dir. +rebase Switch to patch queue / devel branch associated to the current + branch and rebase against upstream. +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.""") + + except ConfigParser.ParsingError as err: + gbp.log.err('Invalid config file: %s' % err) + return None + + parser.add_boolean_config_file_option(option_name="patch-numbers", + dest="patch_numbers") + parser.add_option("-v", "--verbose", action="store_true", dest="verbose", + default=False, help="Verbose command execution") + parser.add_option("--force", dest="force", action="store_true", + default=False, + help="In case of import even import if the branch already exists") + parser.add_config_file_option(option_name="color", dest="color", + type='tristate') + parser.add_config_file_option(option_name="color-scheme", + dest="color_scheme") + parser.add_config_file_option(option_name="tmp-dir", dest="tmp_dir") + parser.add_config_file_option(option_name="upstream-tag", + dest="upstream_tag") + parser.add_config_file_option(option_name="spec-file", dest="spec_file") + parser.add_config_file_option(option_name="packaging-dir", + dest="packaging_dir") + return parser + + +def parse_args(argv): + """Parse command line arguments""" + parser = build_parser(argv[0]) + if not parser: + return None, None + return parser.parse_args(argv) + + +def main(argv): + """Main function for the gbp pq-rpm command""" + retval = 0 + + (options, args) = parse_args(argv) + if not options: + return 1 + + gbp.log.setup(options.color, options.verbose, options.color_scheme) + + if len(args) < 2: + gbp.log.err("No action given.") + return 1 + else: + action = args[1] + + if args[1] in ["export", "import", "rebase", "drop", "switch", "convert"]: + pass + elif args[1] in ["apply"]: + if len(args) != 3: + gbp.log.err("No patch name given.") + return 1 + else: + patchfile = args[2] + else: + gbp.log.err("Unknown action '%s'." % args[1]) + return 1 + + try: + repo = RpmGitRepository(os.path.curdir) + except GitRepositoryError: + gbp.log.err("%s is not a git repository" % (os.path.abspath('.'))) + return 1 + + try: + # Create base temporary directory for this run + options.tmp_dir = tempfile.mkdtemp(dir=options.tmp_dir, + prefix='gbp-pq-rpm_') + current = repo.get_branch() + if action == "export": + export_patches(repo, options) + elif action == "import": + import_spec_patches(repo, options) + elif action == "drop": + drop_pq(repo, current) + elif action == "rebase": + rebase_pq(repo, options) + elif action == "apply": + patch = Patch(patchfile) + apply_single_patch(repo, current, patch, fallback_author=None) + elif action == "switch": + switch_pq(repo, current) + except CommandExecFailed: + retval = 1 + except GitRepositoryError as err: + gbp.log.err("Git command failed: %s" % err) + retval = 1 + except GbpError, err: + if len(err.__str__()): + gbp.log.err(err) + retval = 1 + finally: + shutil.rmtree(options.tmp_dir, ignore_errors=True) + + return retval + +if __name__ == '__main__': + sys.exit(main(sys.argv)) + diff --git a/tests/component/rpm/__init__.py b/tests/component/rpm/__init__.py index e84fca9..b5be3e7 100644 --- a/tests/component/rpm/__init__.py +++ b/tests/component/rpm/__init__.py @@ -16,15 +16,91 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Test module for RPM command line tools of the git-buildpackage suite""" +from nose.tools import nottest import os +import shutil +from xml.dom import minidom -from tests.component import ComponentTestGitRepository +from gbp.git import GitRepository, GitRepositoryError + +from tests.component import ComponentTestBase, ComponentTestGitRepository RPM_TEST_DATA_SUBMODULE = os.path.join('tests', 'component', 'rpm', 'data') RPM_TEST_DATA_DIR = os.path.abspath(RPM_TEST_DATA_SUBMODULE) +class RepoManifest(object): + """Class representing a test repo manifest file""" + def __init__(self, filename=None): + self._doc = minidom.Document() + if filename: + self._doc = minidom.parse(filename) + if self._doc.firstChild.nodeName != 'gbp-test-manifest': + raise Exception('%s is not a test repo manifest' % filename) + else: + self._doc.appendChild(self._doc.createElement("gbp-test-manifest")) + + def projects_iter(self): + """Return an iterator over projects""" + for prj_e in self._doc.getElementsByTagName('project'): + branches = {} + for br_e in prj_e.getElementsByTagName('branch'): + rev = br_e.getAttribute('revision') + branches[br_e.getAttribute('name')] = rev + yield prj_e.getAttribute('name'), branches + + + def write(self, filename): + """Write to file""" + with open(filename, 'w') as fileobj: + fileobj.write(self._doc.toprettyxml()) + def setup(): """Test Module setup""" ComponentTestGitRepository.check_testdata(RPM_TEST_DATA_SUBMODULE) + +class RpmRepoTestBase(ComponentTestBase): + """Baseclass for tests run in a Git repository with packaging data""" + + @classmethod + def setup_class(cls): + """Initializations only made once per test run""" + super(RpmRepoTestBase, cls).setup_class() + cls.manifest = RepoManifest(os.path.join(RPM_TEST_DATA_DIR, + 'test-repo-manifest.xml')) + cls.orig_repos = {} + for prj, brs in cls.manifest.projects_iter(): + repo = GitRepository.create(os.path.join(cls._tmproot, + '%s.repo' % prj)) + try: + repo.add_remote_repo('origin', RPM_TEST_DATA_DIR, fetch=True) + except GitRepositoryError: + # Workaround for older git working on submodules initialized + # with newer git + gitfile = os.path.join(RPM_TEST_DATA_DIR, '.git') + if os.path.isfile(gitfile): + with open(gitfile) as fobj: + link = fobj.readline().replace('gitdir:', '').strip() + link_dir = os.path.join(RPM_TEST_DATA_DIR, link) + repo.remove_remote_repo('origin') + repo.add_remote_repo('origin', link_dir, fetch=True) + else: + raise + # Fetch all remote refs of the orig repo, too + repo.fetch('origin', tags=True, + refspec='refs/remotes/*:refs/upstream/*') + for branch, rev in brs.iteritems(): + repo.create_branch(branch, rev) + repo.force_head('master', hard=True) + cls.orig_repos[prj] = repo + + @classmethod + @nottest + def init_test_repo(cls, pkg_name): + """Initialize git repository for testing""" + dirname = os.path.basename(cls.orig_repos[pkg_name].path) + shutil.copytree(cls.orig_repos[pkg_name].path, dirname) + os.chdir(dirname) + return GitRepository('.') + # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: diff --git a/tests/component/rpm/data b/tests/component/rpm/data index 90bf36d..bae44dd 160000 --- a/tests/component/rpm/data +++ b/tests/component/rpm/data @@ -1 +1 @@ -Subproject commit 90bf36d7981fdd1677cf7e734d9e1056a5fced1c +Subproject commit bae44ddc98ae0ed15ae078cb7c2fc597dee48da5 diff --git a/tests/component/rpm/test_pq_rpm.py b/tests/component/rpm/test_pq_rpm.py new file mode 100644 index 0000000..f0dac8d --- /dev/null +++ b/tests/component/rpm/test_pq_rpm.py @@ -0,0 +1,358 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2013 Intel Corporation <[email protected]> +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""Tests for the gbp pq-rpm tool""" + +import os +import tempfile +from nose.tools import assert_raises, eq_, ok_ # pylint: disable=E0611 + +from gbp.scripts.pq_rpm import main as pq +from gbp.git import GitRepository +from gbp.command_wrappers import GitCommand + +from tests.component.rpm import RpmRepoTestBase + +# Disable "Method could be a function warning" +# pylint: disable=R0201 + + +def mock_pq(args): + """Wrapper for pq""" + # Call pq-rpm with added arg0 + return pq(['arg0'] + args) + +class TestPqRpm(RpmRepoTestBase): + """Basic tests for gbp-pq-rpm""" + + def test_invalid_args(self): + """See that pq-rpm fails gracefully when called with invalid args""" + GitRepository.create('.') + # Test empty args + eq_(mock_pq([]), 1) + self._check_log(0, 'gbp:error: No action given.') + self._clear_log() + + # Test invalid command + eq_(mock_pq(['mycommand']), 1) + self._check_log(0, "gbp:error: Unknown action 'mycommand'") + self._clear_log() + + # Test invalid cmdline options + with assert_raises(SystemExit): + mock_pq(['--invalid-arg=123']) + + def test_import_outside_repo(self): + """Run pq-rpm when not in a git repository""" + eq_(mock_pq(['export']), 1) + self._check_log(0, 'gbp:error: %s is not a git repository' % + os.path.abspath(os.getcwd())) + + def test_invalid_config_file(self): + """Test invalid config file""" + # Create dummy invalid config file and run pq-rpm + GitRepository.create('.') + with open('.gbp.conf', 'w') as conffd: + conffd.write('foobar\n') + eq_(mock_pq(['foo']), 1) + self._check_log(0, 'gbp:error: Invalid config file: File contains no ' + 'section headers.') + + def test_import_export(self): + """Basic test for patch import and export""" + repo = self.init_test_repo('gbp-test') + branches = repo.get_local_branches() + ['patch-queue/master'] + # Test import + eq_(mock_pq(['import']), 0) + files = ['AUTHORS', 'dummy.sh', 'Makefile', 'NEWS', 'README', + 'mydir/myfile.txt'] + branches.append('patch-queue/master') + self._check_repo_state(repo, 'patch-queue/master', branches, files) + eq_(repo.get_merge_base('upstream', 'patch-queue/master'), + repo.rev_parse('upstream')) + ok_(len(repo.get_commits('', 'upstream')) < + len(repo.get_commits('', 'patch-queue/master'))) + + # Test export + eq_(mock_pq(['export', '--upstream-tag', + 'srcdata/gbp-test/upstream/%(version)s']), 0) + files = ['.gbp.conf', '.gitignore', 'bar.tar.gz', 'foo.txt', + 'gbp-test.spec', '0001-my-gz.patch', '0002-my-bzip2.patch', + '0003-my2.patch', 'my.patch'] + self._check_repo_state(repo, 'master', branches, files) + eq_(repo.status()[' M'], ['gbp-test.spec']) + + # Another export after removing some patches + os.unlink('0001-my-gz.patch') + eq_(mock_pq(['export']), 0) + self._check_repo_state(repo, 'master', branches, files) + + def test_import_export2(self): + """Another test for import and export""" + repo = self.init_test_repo('gbp-test2') + branches = repo.get_local_branches() + ['patch-queue/master-orphan'] + repo.set_branch('master-orphan') + # Import + eq_(mock_pq(['import']), 0) + files = ['dummy.sh', 'Makefile', 'README', 'mydir/myfile.txt'] + self._check_repo_state(repo, 'patch-queue/master-orphan', branches, + files) + + # Test export + eq_(mock_pq(['export', '--upstream-tag', + 'srcdata/gbp-test2/upstream/%(version)s', '--spec-file', + 'packaging/gbp-test2.spec']), 0) + self._check_repo_state(repo, 'master-orphan', branches) + eq_(repo.status()[' M'], ['packaging/gbp-test2.spec']) + + def test_rebase(self): + """Basic test for rebase action""" + repo = self.init_test_repo('gbp-test') + repo.rename_branch('pq/master', 'patch-queue/master') + repo.set_branch('patch-queue/master') + branches = repo.get_local_branches() + # Make development branch out-of-sync + GitCommand("rebase")(['--onto', 'upstream^', 'upstream']) + # Sanity check for our git rebase... + ok_(repo.get_merge_base('patch-queue/master', 'upstream') != + repo.rev_parse('upstream')) + + # Do rebase + eq_(mock_pq(['rebase']), 0) + self._check_repo_state(repo, 'patch-queue/master', branches) + ok_(repo.get_merge_base('patch-queue/master', 'upstream') == + repo.rev_parse('upstream')) + + # Get to out-of-sync, again, and try rebase from master branch + GitCommand("rebase")(['--onto', 'upstream^', 'upstream']) + eq_(mock_pq(['switch']), 0) + eq_(mock_pq(['rebase']), 0) + self._check_repo_state(repo, 'patch-queue/master', branches) + ok_(repo.get_merge_base('patch-queue/master', 'upstream') == + repo.rev_parse('upstream')) + + def test_switch(self): + """Basic test for switch action""" + repo = self.init_test_repo('gbp-test') + pkg_files = repo.list_files() + branches = repo.get_local_branches() + ['patch-queue/master'] + # Switch to non-existent pq-branch should create one + eq_(mock_pq(['switch']), 0) + self._check_repo_state(repo, 'patch-queue/master', branches) + + # Switch to base branch and back to pq + eq_(mock_pq(['switch']), 0) + self._check_repo_state(repo, 'master', branches) + eq_(mock_pq(['switch']), 0) + self._check_repo_state(repo, 'patch-queue/master', branches) + + def test_switch_drop(self): + """Basic test for drop action""" + repo = self.init_test_repo('gbp-test') + repo.rename_branch('pq/master', 'patch-queue/master') + repo.set_branch('patch-queue/master') + branches = repo.get_local_branches() + + # Drop pq should fail when on pq branch + eq_(mock_pq(['drop']), 1) + self._check_log(-1, "gbp:error: On a patch-queue branch, can't drop it") + self._check_repo_state(repo, 'patch-queue/master', branches) + + # Switch to master + eq_(mock_pq(['switch']), 0) + self._check_repo_state(repo, 'master', branches) + + # Drop should succeed when on master branch + eq_(mock_pq(['drop']), 0) + branches.remove('patch-queue/master') + self._check_repo_state(repo, 'master', branches) + + def test_force_import(self): + """Test force import""" + repo = self.init_test_repo('gbp-test') + pkg_files = repo.list_files() + repo.rename_branch('pq/master', 'patch-queue/master') + repo.set_branch('patch-queue/master') + branches = repo.get_local_branches() + pq_files = repo.list_files() + + # Re-import should fail + eq_(mock_pq(['import']), 1) + self._check_log(0, "gbp:error: Already on a patch-queue branch") + self._check_repo_state(repo, 'patch-queue/master', branches, pq_files) + + # Mangle pq branch and try force import on top of that + repo.force_head('master', hard=True) + self._check_repo_state(repo, 'patch-queue/master', branches, pkg_files) + eq_(mock_pq(['import', '--force']), 0) + # .gbp.conf won't get imported by pq + pq_files.remove('.gbp.conf') + self._check_repo_state(repo, 'patch-queue/master', branches, pq_files) + + # Switch back to master + eq_(mock_pq(['switch']), 0) + self._check_repo_state(repo, 'master', branches, pkg_files) + + # Import should fail + eq_(mock_pq(['import']), 1) + self._check_log(-1, "gbp:error: Patch-queue branch .* already exists") + self._check_repo_state(repo, 'master', branches, pkg_files) + + # Force import should succeed + eq_(mock_pq(['import', '--force']), 0) + self._check_repo_state(repo, 'patch-queue/master', branches, pq_files) + + def test_apply(self): + """Basic test for apply action""" + repo = self.init_test_repo('gbp-test') + upstr_files = ['dummy.sh', 'Makefile', 'README'] + branches = repo.get_local_branches() + ['patch-queue/master'] + + # No patch given + eq_(mock_pq(['apply']), 1) + self._check_log(-1, "gbp:error: No patch name given.") + + # Create a pristine pq-branch + repo.create_branch('patch-queue/master', 'upstream') + + # Apply patch + with tempfile.NamedTemporaryFile() as tmp_patch: + tmp_patch.write(repo.show('master:%s' % 'my.patch')) + tmp_patch.file.flush() + eq_(mock_pq(['apply', tmp_patch.name]), 0) + self._check_repo_state(repo, 'patch-queue/master', branches, + upstr_files) + + # Apply another patch, now when already on pq branch + with tempfile.NamedTemporaryFile() as tmp_patch: + tmp_patch.write(repo.show('master:%s' % 'my2.patch')) + tmp_patch.file.flush() + eq_(mock_pq(['apply', tmp_patch.name]), 0) + self._check_repo_state(repo, 'patch-queue/master', branches, + upstr_files + ['mydir/myfile.txt']) + + def test_option_patch_numbers(self): + """Test the --patch-numbers cmdline option""" + repo = self.init_test_repo('gbp-test') + repo.rename_branch('pq/master', 'patch-queue/master') + branches = repo.get_local_branches() + # Export + eq_(mock_pq(['export', '--no-patch-numbers']), 0) + files = ['.gbp.conf', '.gitignore', 'bar.tar.gz', 'foo.txt', + 'gbp-test.spec', 'my-gz.patch', 'my-bzip2.patch', 'my2.patch', + 'my.patch'] + self._check_repo_state(repo, 'master', branches, files) + + def test_option_tmp_dir(self): + """Test the --tmp-dir cmdline option""" + self.init_test_repo('gbp-test') + eq_(mock_pq(['import', '--tmp-dir=foo/bar']), 0) + # Check that the tmp dir basedir was created + ok_(os.path.isdir('foo/bar')) + + def test_option_upstream_tag(self): + """Test the --upstream-tag cmdline option""" + repo = self.init_test_repo('gbp-test') + + # Non-existent upstream-tag -> failure + eq_(mock_pq(['import', '--upstream-tag=foobar/%(upstreamversion)s']), 1) + self._check_log(-1, "gbp:error: Couldn't find upstream version") + + # Create tag -> import should succeed + repo.create_tag('foobar/1.1', msg="test tag", commit='upstream') + eq_(mock_pq(['import', '--upstream-tag=foobar/%(upstreamversion)s']), 0) + + def test_option_spec_file(self): + """Test --spec-file commandline option""" + self.init_test_repo('gbp-test') + + # Non-existent spec file should lead to failure + eq_(mock_pq(['import', '--spec-file=foo.spec']), 1) + self._check_log(-1, "gbp:error: Can't parse spec: Unable to read spec") + # Correct spec file + eq_(mock_pq(['import', '--spec-file=gbp-test.spec']), 0) + + # Force import on top to test parsing spec from another branch + eq_(mock_pq(['import', '--spec-file=gbp-test.spec', '--force', + '--upstream-tag', + 'srcdata/gbp-test/upstream/%(version)s']), 0) + + # Test with export, too + eq_(mock_pq(['export', '--spec-file=foo.spec']), 1) + self._check_log(-1, "gbp:error: Can't parse spec: Unable to read spec") + eq_(mock_pq(['export', '--spec-file=gbp-test.spec']), 0) + + def test_option_packaging_dir(self): + """Test --packaging-dir command line option""" + self.init_test_repo('gbp-test') + + # Wrong packaging dir should lead to failure + eq_(mock_pq(['import', '--packaging-dir=foo']), 1) + self._check_log(-1, "gbp:error: Can't parse spec: No spec file found") + # Use correct packaging dir + eq_(mock_pq(['import', '--packaging-dir=.']), 0) + + # Test with export, --spec-file option should override packaging dir + eq_(mock_pq(['export', '--packaging-dir=foo', '--upstream-tag', + 'srcdata/gbp-test/upstream/%(version)s', + '--spec-file=gbp-test.spec']), 0) + + def test_export_with_merges(self): + """Test exporting pq-branch with merge commits""" + repo = self.init_test_repo('gbp-test') + repo.rename_branch('pq/master', 'patch-queue/master') + repo.set_branch('patch-queue/master') + branches = repo.get_local_branches() + + # Create a merge commit in pq-branch + patches = repo.format_patches('HEAD^', 'HEAD', '.') + repo.force_head('HEAD^', hard=True) + repo.commit_dir('.', 'Merge with master', 'patch-queue/master', + ['master']) + merge_rev = repo.rev_parse('HEAD', short=7) + eq_(mock_pq(['apply', patches[0]]), 0) + upstr_rev = repo.rev_parse('upstream', short=7) + os.unlink(patches[0]) + + # Export should create diff up to the merge point and one "normal" patch + eq_(mock_pq(['export']), 0) + files = ['.gbp.conf', '.gitignore', 'bar.tar.gz', 'foo.txt', + 'gbp-test.spec', 'my.patch', + '%s-to-%s.diff' % (upstr_rev, merge_rev), '0002-my2.patch'] + self._check_repo_state(repo, 'master', branches, files) + + def test_import_unapplicable_patch(self): + """Test import when a patch does not apply""" + repo = self.init_test_repo('gbp-test') + branches = repo.get_local_branches() + # Mangle patch + with open('my2.patch', 'w') as patch_file: + patch_file.write('-this-does\n+not-apply\n') + eq_(mock_pq(['import']), 1) + self._check_log(-2, "(" + "Aborting|" + "Please, commit your changes or stash them|" + "gbp:error: Import failed.* You have local changes" + ")") + self._check_repo_state(repo, 'master', branches) + + # Now commit the changes to the patch and try again + repo.add_files(['my2.patch'], force=True) + repo.commit_files(['my2.patch'], msg="Mangle patch") + eq_(mock_pq(['import']), 1) + self._check_log(-2, "gbp:error: Import failed: Error running git apply") + self._check_repo_state(repo, 'master', branches) + -- 1.8.4.5

