Merge ~rhansen/git-build-recipe:upstream into git-build-recipe:master

Proposed by Richard Hansen
Status: Needs review
Proposed branch: ~rhansen/git-build-recipe:upstream
Merge into: git-build-recipe:master
Diff against target: 1211 lines (+568/-180)
7 files modified
gitbuildrecipe/deb_util.py (+0/-54)
gitbuildrecipe/main.py (+4/-5)
gitbuildrecipe/recipe.py (+266/-79)
gitbuildrecipe/tests/__init__.py (+3/-3)
gitbuildrecipe/tests/test_deb_version.py (+27/-10)
gitbuildrecipe/tests/test_functional.py (+203/-7)
gitbuildrecipe/tests/test_recipe.py (+65/-22)
Reviewer Review Type Date Requested Status
Colin Watson Pending
Review via email: mp+446340@code.launchpad.net

Description of the change

This branch adds a new `upstream` instruction that allows users to specify a revision (branch, tag, whatever) containing the pristine upstream sources. If the recipe contains an `upstream` instruction, the referenced commit will be used instead of the `pristine-tar` branch or the `upstream/<version>` and `upstream-<version>` tags.

This branch also has several refactor, cleanup, and minor bugfix commits to prepare for the new feature.

Fixes bug #2024971

To post a comment you must log in.

Unmerged commits

7eafc39... by Richard Hansen

new `upstream` command to specify location of pristine sources

356a635... by Richard Hansen

fix subdirectory existence check

83181c5... by Richard Hansen

use `can_have_children` to test if indentation is allowed

This will make it possible to add new commands that support child branches (or
extend existing commands, such as `nest_part`).

0bea610... by Richard Hansen

inline the `resolve_commit` method

acf176d... by Richard Hansen

ensure that the child branches have been fetched

6debc1a... by Richard Hansen

move `resolve_commit` call into `fetch` method

ff10ad5... by Richard Hansen

delete redundant call to `resolve_commit`

586d348... by Richard Hansen

improve handling of unknown revisions

cc12101... by Richard Hansen

move `FetchFailed` exception conversion to the `fetch` method

7c39665... by Richard Hansen

move fetch logic to a method on `RecipeBranch`

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/gitbuildrecipe/deb_util.py b/gitbuildrecipe/deb_util.py
index d0a6666..c3227ba 100644
--- a/gitbuildrecipe/deb_util.py
+++ b/gitbuildrecipe/deb_util.py
@@ -32,13 +32,6 @@ class MissingDependency(Exception):
32 pass32 pass
3333
3434
35class NoSuchTag(Exception):
36
37 def __init__(self, tag_name):
38 super().__init__()
39 self.tag_name = tag_name
40
41
42def debian_source_package_name(control_path):35def debian_source_package_name(control_path):
43 """Open a debian control file and extract the package name.36 """Open a debian control file and extract the package name.
4437
@@ -48,53 +41,6 @@ def debian_source_package_name(control_path):
48 return control["Source"]41 return control["Source"]
4942
5043
51def extract_upstream_tarball(path, package, version, dest_dir):
52 """Extract the upstream tarball from a Git repository.
53
54 :param path: Path to the Git repository
55 :param package: Package name
56 :param version: Package version
57 :param dest_dir: Destination directory
58 """
59 prefix = "%s_%s.orig.tar." % (package, version)
60 dest_filename = None
61 pristine_tar_list = subprocess.Popen(
62 ["pristine-tar", "list"], stdout=subprocess.PIPE, cwd=path)
63 try:
64 for line in pristine_tar_list.stdout:
65 line = line.decode("UTF-8", errors="replace").rstrip("\n")
66 if line.startswith(prefix):
67 dest_filename = line
68 finally:
69 pristine_tar_list.wait()
70 if dest_filename is not None:
71 subprocess.check_call(
72 ["pristine-tar", "checkout",
73 os.path.abspath(os.path.join(dest_dir, dest_filename))],
74 cwd=path)
75 else:
76 tag_names = ["upstream/%s" % version, "upstream-%s" % version]
77 git_tag_list = subprocess.Popen(
78 ["git", "tag"], stdout=subprocess.PIPE, cwd=path)
79 try:
80 for line in git_tag_list.stdout:
81 line = line.decode("UTF-8", errors="replace").rstrip("\n")
82 if line in tag_names:
83 tag = line
84 break
85 else:
86 raise NoSuchTag(tag_names[0])
87 finally:
88 git_tag_list.wait()
89 # Default to .tar.gz
90 dest_filename = prefix + "gz"
91 with open(os.path.join(dest_dir, dest_filename), "wb") as dest:
92 subprocess.check_call(
93 ["git", "archive", "--format=tar.gz",
94 "--prefix=%s-%s/" % (package, version), tag],
95 stdout=dest, cwd=path)
96
97
98def add_autobuild_changelog_entry(base_branch, basedir, package,44def add_autobuild_changelog_entry(base_branch, basedir, package,
99 distribution=None, author_name=None,45 distribution=None, author_name=None,
100 author_email=None, append_version=None):46 author_email=None, append_version=None):
diff --git a/gitbuildrecipe/main.py b/gitbuildrecipe/main.py
index 30e6bec..a241744 100644
--- a/gitbuildrecipe/main.py
+++ b/gitbuildrecipe/main.py
@@ -26,11 +26,9 @@ import tempfile
26from debian.changelog import Changelog26from debian.changelog import Changelog
2727
28from gitbuildrecipe.deb_util import (28from gitbuildrecipe.deb_util import (
29 NoSuchTag,
30 add_autobuild_changelog_entry,29 add_autobuild_changelog_entry,
31 build_source_package,30 build_source_package,
32 debian_source_package_name,31 debian_source_package_name,
33 extract_upstream_tarball,
34 force_native_format,32 force_native_format,
35 get_source_format,33 get_source_format,
36 )34 )
@@ -40,6 +38,7 @@ from gitbuildrecipe.deb_version import (
40 substitute_time,38 substitute_time,
41 )39 )
42from gitbuildrecipe.recipe import (40from gitbuildrecipe.recipe import (
41 NoSuchTag,
43 build_tree,42 build_tree,
44 parse_recipe,43 parse_recipe,
45 resolve_revisions,44 resolve_revisions,
@@ -159,9 +158,9 @@ def main():
159 current_format == "3.0 (quilt)"):158 current_format == "3.0 (quilt)"):
160 # Non-native package159 # Non-native package
161 try:160 try:
162 extract_upstream_tarball(161 base_branch.get_upstream_tarball(
163 base_branch.path, package_name,162 package_name, package_version.upstream_version,
164 package_version.upstream_version, working_basedir)163 working_basedir)
165 except NoSuchTag as e:164 except NoSuchTag as e:
166 if not args.allow_fallback_to_native:165 if not args.allow_fallback_to_native:
167 parser.error(166 parser.error(
diff --git a/gitbuildrecipe/recipe.py b/gitbuildrecipe/recipe.py
index f932ca7..0ebdfb2 100644
--- a/gitbuildrecipe/recipe.py
+++ b/gitbuildrecipe/recipe.py
@@ -30,16 +30,18 @@ MERGE_INSTRUCTION = "merge"
30NEST_PART_INSTRUCTION = "nest-part"30NEST_PART_INSTRUCTION = "nest-part"
31NEST_INSTRUCTION = "nest"31NEST_INSTRUCTION = "nest"
32RUN_INSTRUCTION = "run"32RUN_INSTRUCTION = "run"
33UPSTREAM_INSTRUCTION = "upstream"
33USAGE = {34USAGE = {
34 MERGE_INSTRUCTION: 'merge NAME BRANCH [REVISION]',35 MERGE_INSTRUCTION: 'merge NAME BRANCH [REVISION]',
35 NEST_INSTRUCTION: 'nest NAME BRANCH TARGET-DIR [REVISION]',36 NEST_INSTRUCTION: 'nest NAME BRANCH TARGET-DIR [REVISION]',
36 NEST_PART_INSTRUCTION:37 NEST_PART_INSTRUCTION:
37 'nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]',38 'nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]',
38 RUN_INSTRUCTION: 'run COMMAND',39 RUN_INSTRUCTION: 'run COMMAND',
40 UPSTREAM_INSTRUCTION: 'upstream NAME BRANCH [REVISION]',
39 }41 }
4042
41SAFE_INSTRUCTIONS = [43SAFE_INSTRUCTIONS = [
42 MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION]44 MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION, UPSTREAM_INSTRUCTION]
4345
4446
45class FormattedError(Exception):47class FormattedError(Exception):
@@ -68,6 +70,22 @@ class FormattedError(Exception):
68 __hash__ = Exception.__hash__70 __hash__ = Exception.__hash__
6971
7072
73class NoSuchTag(Exception):
74
75 def __init__(self, tag_name):
76 super().__init__()
77 self.tag_name = tag_name
78
79
80class UnknownRevisionError(Exception):
81
82 def __init__(self, rev):
83 self.rev = rev
84
85 def __str__(self):
86 return str(self.rev)
87
88
71class SubstitutionUnavailable(FormattedError):89class SubstitutionUnavailable(FormattedError):
7290
73 _fmt = "Substitution for %(name)s not available: %(reason)s"91 _fmt = "Substitution for %(name)s not available: %(reason)s"
@@ -161,7 +179,7 @@ class RevisionVariable(BranchSubstitutionVariable):
161 def __init__(self, branch):179 def __init__(self, branch):
162 super().__init__(branch.name)180 super().__init__(branch.name)
163 self.branch = branch181 self.branch = branch
164 self.commit = branch.commit182 self.commit = branch.commit_for_substvars
165183
166184
167class RevnoVariable(RevisionVariable):185class RevnoVariable(RevisionVariable):
@@ -299,17 +317,13 @@ def pull_or_clone(base_branch, target_path):
299 os.rmdir(target_path)317 os.rmdir(target_path)
300 if not os.path.exists(target_path):318 if not os.path.exists(target_path):
301 os.makedirs(target_path)319 os.makedirs(target_path)
302 base_branch.git_call("init")320 base_branch.git_call("init", "-b", "main")
303 for short, base in insteadof.items():321 for short, base in insteadof.items():
304 base_branch.git_call("config", "url.%s.insteadOf" % base, short)322 base_branch.git_call("config", "url.%s.insteadOf" % base, short)
305 elif not os.path.exists(os.path.join(target_path, ".git")):323 elif not os.path.exists(os.path.join(target_path, ".git")):
306 raise TargetAlreadyExists(target_path)324 raise TargetAlreadyExists(target_path)
325 base_branch.fetch()
307 try:326 try:
308 fetch_branches(base_branch)
309 except subprocess.CalledProcessError as e:
310 raise FetchFailed(e.output)
311 try:
312 base_branch.resolve_commit()
313 # Check out the commit hash directly to detach HEAD and ensure327 # Check out the commit hash directly to detach HEAD and ensure
314 # commits (eg. by a merge instruction) don't affect substitution328 # commits (eg. by a merge instruction) don't affect substitution
315 # variables.329 # variables.
@@ -322,30 +336,6 @@ def pull_or_clone(base_branch, target_path):
322 raise CheckoutFailed(e.output)336 raise CheckoutFailed(e.output)
323337
324338
325def fetch_branches(child_branch):
326 url = child_branch.url
327 parsed_url = urlparse(url)
328 if not parsed_url.scheme and not parsed_url.path.startswith("/"):
329 url = os.path.abspath(url)
330 # Fetch the remote HEAD. This may not exist, which is OK as long as the
331 # recipe uses explicit branch names.
332 try:
333 child_branch.git_call(
334 "fetch", url,
335 "HEAD:refs/remotes/%s/HEAD" % child_branch.remote_name,
336 silent=True)
337 except subprocess.CalledProcessError as e:
338 logging.info(e.output)
339 logging.info(
340 "Failed to fetch HEAD; recipe instructions for this repository "
341 "that do not specify a branch name will fail.")
342 # Fetch all remote branches and (implicitly) any tags that reference
343 # commits in those refs. Tags that aren't on a branch won't be fetched.
344 child_branch.git_call(
345 "fetch", url,
346 "refs/heads/*:refs/remotes/%s/*" % child_branch.remote_name)
347
348
349@lru_cache(maxsize=1)339@lru_cache(maxsize=1)
350def _git_version():340def _git_version():
351 raw_git_version = subprocess.check_output(341 raw_git_version = subprocess.check_output(
@@ -362,9 +352,8 @@ def merge_branch(child_branch, target_path):
362 :param target_path: The tree to merge into.352 :param target_path: The tree to merge into.
363 """353 """
364 child_branch.path = target_path354 child_branch.path = target_path
365 fetch_branches(child_branch)355 child_branch.fetch()
366 try:356 try:
367 child_branch.resolve_commit()
368 cmd = ["merge", "--commit"]357 cmd = ["merge", "--commit"]
369 if _git_version() >= "1:2.9.0":358 if _git_version() >= "1:2.9.0":
370 cmd.append("--allow-unrelated-histories")359 cmd.append("--allow-unrelated-histories")
@@ -391,10 +380,9 @@ def nest_part_branch(child_branch, target_path, subpath, target_subdir=None):
391 if target_subdir is None:380 if target_subdir is None:
392 target_subdir = os.path.basename(subpath)381 target_subdir = os.path.basename(subpath)
393 # XXX should handle updating as well382 # XXX should handle updating as well
394 assert not os.path.exists(target_subdir)383 assert not os.path.exists(os.path.join(target_path, target_subdir))
395 child_branch.path = target_path384 child_branch.path = target_path
396 fetch_branches(child_branch)385 child_branch.fetch()
397 child_branch.resolve_commit()
398 child_branch.git_call(386 child_branch.git_call(
399 "read-tree", "--prefix", target_subdir, "-u",387 "read-tree", "--prefix", target_subdir, "-u",
400 child_branch.commit + ":" + subpath)388 child_branch.commit + ":" + subpath)
@@ -408,7 +396,6 @@ def _build_inner_tree(base_branch, target_path):
408 "Retrieving %s'%s' to put at '%s'." %396 "Retrieving %s'%s' to put at '%s'." %
409 (revision_of, base_branch.url, target_path))397 (revision_of, base_branch.url, target_path))
410 pull_or_clone(base_branch, target_path)398 pull_or_clone(base_branch, target_path)
411 base_branch.resolve_commit()
412 for instruction in base_branch.child_branches:399 for instruction in base_branch.child_branches:
413 instruction.apply(target_path)400 instruction.apply(target_path)
414401
@@ -417,8 +404,8 @@ def _resolve_revisions_recurse(new_branch, substitute_branch_vars,
417 if_changed_from=None):404 if_changed_from=None):
418 changed = False405 changed = False
419 if substitute_branch_vars is not None:406 if substitute_branch_vars is not None:
420 # XXX need to make sure new_branch has been fetched407 if new_branch.commit is None:
421 new_branch.resolve_commit()408 new_branch.fetch()
422 substitute_branch_vars(new_branch)409 substitute_branch_vars(new_branch)
423 if (if_changed_from is not None and410 if (if_changed_from is not None and
424 (new_branch.revspec is not None or411 (new_branch.revspec is not None or
@@ -499,6 +486,10 @@ class ChildBranch:
499486
500 can_have_children = False487 can_have_children = False
501488
489 @property
490 def instruction(self):
491 raise NotImplementedError("instruction property not set")
492
502 def __init__(self, recipe_branch, nest_path=None):493 def __init__(self, recipe_branch, nest_path=None):
503 self.recipe_branch = recipe_branch494 self.recipe_branch = recipe_branch
504 self.nest_path = nest_path495 self.nest_path = nest_path
@@ -523,6 +514,8 @@ class ChildBranch:
523514
524class CommandInstruction(ChildBranch):515class CommandInstruction(ChildBranch):
525516
517 instruction = RUN_INSTRUCTION
518
526 def apply(self, target_path):519 def apply(self, target_path):
527 # it's a command520 # it's a command
528 logging.info("Running '%s' in '%s'." % (self.nest_path, target_path))521 logging.info("Running '%s' in '%s'." % (self.nest_path, target_path))
@@ -535,6 +528,8 @@ class CommandInstruction(ChildBranch):
535528
536class MergeInstruction(ChildBranch):529class MergeInstruction(ChildBranch):
537530
531 instruction = MERGE_INSTRUCTION
532
538 def apply(self, target_path):533 def apply(self, target_path):
539 revision_of = ""534 revision_of = ""
540 if self.recipe_branch.revspec is not None:535 if self.recipe_branch.revspec is not None:
@@ -556,6 +551,8 @@ class MergeInstruction(ChildBranch):
556551
557class NestPartInstruction(ChildBranch):552class NestPartInstruction(ChildBranch):
558553
554 instruction = NEST_PART_INSTRUCTION
555
559 def __init__(self, recipe_branch, subpath, target_subdir):556 def __init__(self, recipe_branch, subpath, target_subdir):
560 ChildBranch.__init__(self, recipe_branch)557 ChildBranch.__init__(self, recipe_branch)
561 self.subpath = subpath558 self.subpath = subpath
@@ -584,6 +581,8 @@ class NestPartInstruction(ChildBranch):
584581
585class NestInstruction(ChildBranch):582class NestInstruction(ChildBranch):
586583
584 instruction = NEST_INSTRUCTION
585
587 can_have_children = True586 can_have_children = True
588587
589 def apply(self, target_path):588 def apply(self, target_path):
@@ -602,6 +601,47 @@ class NestInstruction(ChildBranch):
602 self.recipe_branch.name)601 self.recipe_branch.name)
603602
604603
604class UpstreamInstruction(ChildBranch):
605
606 instruction = UPSTREAM_INSTRUCTION
607
608 can_have_children = True
609
610 def apply(self, target_path):
611 b = self.recipe_branch
612 b.path = target_path
613 if len(b.child_branches) == 0:
614 b.fetch()
615 return
616 is_clean = lambda: b.git_output(
617 "status", "--porcelain", "--ignored", "-u") == ""
618 head_backup = b._get_commit_id("HEAD")
619 clean = is_clean()
620 if not clean:
621 b.git_call("stash", "--all")
622 _build_inner_tree(b, target_path)
623 if not is_clean():
624 b.git_call("stash", "--all")
625 tree = b._get_commit_id("refs/stash@{0}")
626 b.git_call("stash", "pop", "--index")
627 b.git_call("read-tree", tree)
628 b.git_call("commit", "-m", "upstream after child modifications")
629 # The commit(s) made above should not affect any substitution variables.
630 b._commit_for_substvars = b.commit
631 b.commit = b._get_commit_id("HEAD")
632 b.git_call("checkout", head_backup)
633 if not clean:
634 b.git_call("stash", "pop", "--index")
635
636 def as_text(self):
637 b = self.recipe_branch
638 c = self._get_commit_part()
639 return f"{UPSTREAM_INSTRUCTION} {b.name} {b.url}{c}"
640
641 def __repr__(self):
642 return f"<{self.__class__.__name__} {self.recipe_branch.name!r}>"
643
644
605class RecipeBranch:645class RecipeBranch:
606 """A nested structure that represents a Recipe.646 """A nested structure that represents a Recipe.
607647
@@ -632,6 +672,14 @@ class RecipeBranch:
632 self.child_branches = []672 self.child_branches = []
633 self.commit = None673 self.commit = None
634 self.path = None674 self.path = None
675 self._commit_for_substvars = None
676 self._upstream_branch = None
677
678 @property
679 def commit_for_substvars(self):
680 if self._commit_for_substvars is None:
681 return self.commit
682 return self._commit_for_substvars
635683
636 @property684 @property
637 def remote_name(self):685 def remote_name(self):
@@ -649,6 +697,41 @@ class RecipeBranch:
649 raise Exception(697 raise Exception(
650 "Repository at %s has not been cloned yet" % self.url)698 "Repository at %s has not been cloned yet" % self.url)
651699
700 def fetch(self):
701 url = self.url
702 parsed_url = urlparse(url)
703 if not parsed_url.scheme and not parsed_url.path.startswith("/"):
704 url = os.path.abspath(url)
705 # Fetch the remote HEAD. This may not exist, which is OK as long as the
706 # recipe uses explicit branch names.
707 try:
708 self.git_call(
709 "fetch", url, "HEAD:refs/remotes/%s/HEAD" % self.remote_name,
710 silent=True)
711 except subprocess.CalledProcessError as e:
712 logging.info(e.output)
713 logging.info(
714 "Failed to fetch HEAD; recipe instructions for this repository "
715 "that do not specify a branch name will fail.")
716 # Fetch all remote branches and (implicitly) any tags that reference
717 # commits in those refs. Tags that aren't on a branch won't be fetched.
718 try:
719 self.git_call(
720 "fetch", url,
721 "refs/heads/*:refs/remotes/%s/*" % self.remote_name)
722 except subprocess.CalledProcessError as e:
723 raise FetchFailed(e.output)
724 try:
725 self.commit = self._get_commit_id(
726 self.remote_name + "/" + self.get_revspec())
727 return
728 except UnknownRevisionError:
729 pass
730 # Not a remote-prefixed ref. Try a global search.
731 # XXX: This allows cross-branch pollution, but we don't have
732 # much choice without reimplementing rev-parse ourselves.
733 self.commit = self._get_commit_id(self.get_revspec())
734
652 def git_call(self, *args, **kwargs):735 def git_call(self, *args, **kwargs):
653 cmd = ["git", "-C", self._get_git_path()] + list(args)736 cmd = ["git", "-C", self._get_git_path()] + list(args)
654 silent = kwargs.pop("silent", False)737 silent = kwargs.pop("silent", False)
@@ -675,24 +758,12 @@ class RecipeBranch:
675 def get_revspec(self):758 def get_revspec(self):
676 return self.revspec if self.revspec is not None else "HEAD"759 return self.revspec if self.revspec is not None else "HEAD"
677760
678 def resolve_commit(self):761 def _get_commit_id(self, rev):
679 """Resolve the commit for this branch."""
680 # Capturing stderr is a bit dodgy, but it's the most convenient way
681 # to capture it for any exceptions. We know that git rev-parse does
682 # not write to stderr on success.
683 try:762 try:
684 self.commit = self.git_output(763 return self.git_output(
685 "rev-parse", "%s/%s" % (self.remote_name, self.get_revspec()),764 "rev-parse", "--revs-only", "--verify", "-q", rev).rstrip("\n")
686 stderr=subprocess.STDOUT).rstrip("\n")765 except subprocess.CalledProcessError as e:
687 return766 raise UnknownRevisionError(rev) from e
688 except subprocess.CalledProcessError:
689 pass
690 # Not a remote-prefixed ref. Try a global search.
691 # XXX: This allows cross-branch pollution, but we don't have
692 # much choice without reimplementing rev-parse ourselves.
693 self.commit = self.git_output(
694 "rev-parse", self.get_revspec(),
695 stderr=subprocess.STDOUT).rstrip("\n")
696767
697 def merge_branch(self, branch):768 def merge_branch(self, branch):
698 """Merge a child branch into this one.769 """Merge a child branch into this one.
@@ -733,6 +804,19 @@ class RecipeBranch:
733 """804 """
734 self.child_branches.append(CommandInstruction(None, command))805 self.child_branches.append(CommandInstruction(None, command))
735806
807 def upstream_branch(self, branch):
808 """Mark a branch as containing the original pristine sources.
809
810 :param branch: The `RecipeBranch` referencing the pristine sources.
811 """
812 if self.name is not None:
813 raise Exception(
814 "the upstream command only applies to the base branch")
815 if self._upstream_branch is not None:
816 raise Exception("upstream already set for the base branch")
817 self.child_branches.append(UpstreamInstruction(branch))
818 self._upstream_branch = branch
819
736 def different_shape_to(self, other_branch):820 def different_shape_to(self, other_branch):
737 """Test whether the name, url, and child_branches are the same."""821 """Test whether the name, url, and child_branches are the same."""
738 if self.name != other_branch.name:822 if self.name != other_branch.name:
@@ -851,6 +935,65 @@ class BaseRecipeBranch(RecipeBranch):
851 RecipeParser(manifest).parse()935 RecipeParser(manifest).parse()
852 return manifest936 return manifest
853937
938 def get_upstream_tarball(self, package, version, dest_dir):
939 """Generate the upstream tarball (*.orig.tar.*).
940
941 :param package: Package name
942 :param version: Package version
943 :param dest_dir: Destination directory
944 """
945 prefix = "%s_%s.orig.tar." % (package, version)
946 if self._upstream_branch is not None:
947 # self._upstream_branch.fetch() has already been called, and it
948 # should NOT be called again here otherwise it will undo any changes
949 # made by child instructions.
950 dest_filename = prefix + "gz"
951 tarball = os.path.abspath(os.path.join(dest_dir, dest_filename))
952 # With `Format: 1.0` packages, dpkg-source does not delete the
953 # debian directory (if present in the *.orig.tar.gz) before applying
954 # the *.diff.gz. Exclude the debian directory when generating the
955 # orig archive to avoid problems applying the patch. (The diff.gz
956 # can't delete files, only empty them.) This isn't necessary for
957 # `Format: 3.0 (quilt)` packages, but it doesn't really hurt either.
958 # If the user is concerned with perfect representation of the
959 # upstream sources with `Format: 3.0 (quilt)` packages, they should
960 # use pristine-tar.
961 self.git_call(
962 "archive", f"--output={tarball}",
963 f"--prefix={package}-{version}/", self._upstream_branch.commit,
964 ":(exclude)debian")
965 return
966 dest_filename = None
967 pristine_tar_list = subprocess.Popen(
968 ["pristine-tar", "list"], stdout=subprocess.PIPE, cwd=self.path)
969 try:
970 for line in pristine_tar_list.stdout:
971 line = line.decode("UTF-8", errors="replace").rstrip("\n")
972 if line.startswith(prefix):
973 dest_filename = line
974 finally:
975 pristine_tar_list.wait()
976 if dest_filename is not None:
977 subprocess.check_call(
978 ["pristine-tar", "checkout",
979 os.path.abspath(os.path.join(dest_dir, dest_filename))],
980 cwd=self.path)
981 else:
982 tag_names = ["upstream/%s" % version, "upstream-%s" % version]
983 for line in self.git_output("tag").rstrip("\n").split("\n"):
984 if line in tag_names:
985 tag = line
986 break
987 else:
988 raise NoSuchTag(tag_names[0])
989 # Default to .tar.gz
990 dest_filename = prefix + "gz"
991 with open(os.path.join(dest_dir, dest_filename), "wb") as dest:
992 subprocess.check_call(
993 ["git", "archive", "--format=tar.gz",
994 "--prefix=%s-%s/" % (package, version), tag],
995 stdout=dest, cwd=self.path)
996
854997
855class RecipeParseError(FormattedError):998class RecipeParseError(FormattedError):
856999
@@ -887,7 +1030,7 @@ class RecipeParser:
887 eol_char = "\n"1030 eol_char = "\n"
888 digit_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")1031 digit_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
8891032
890 NEWEST_VERSION = 0.41033 NEWEST_VERSION = 0.5
8911034
892 def __init__(self, f):1035 def __init__(self, f):
893 """Create a `RecipeParser`.1036 """Create a `RecipeParser`.
@@ -922,10 +1065,44 @@ class RecipeParser:
922 self.seen_paths = {".": 1}1065 self.seen_paths = {".": 1}
923 version, deb_version = self.parse_header()1066 version, deb_version = self.parse_header()
924 self.version = version1067 self.version = version
1068
1069 # active_instructions contains the ChildBranch objects in the current
1070 # instruction's parent path, in ancestry order. Thus,
1071 # active_instructions[-1] always refers to the current instruction's
1072 # parent instruction. For example, consider the following recipe:
1073 #
1074 # # git-build-recipe format 0.4 deb-version 1.0-1
1075 # http://example.test/repo.git
1076 # nest nest1 http://example.test/nest1.git nest1
1077 # nest nest2 http://example.test/nest2.git nest2
1078 # nest nest3 http://example.test/nest3.git nest3
1079 # nest nest4 http://example.test/nest4.git nest4
1080 # nest nest5 http://example.test/nest5.git nest5
1081 #
1082 # When the nest5 line is processed, active_instructions will contain the
1083 # following entries:
1084 #
1085 # 0. None (This entry is for the base branch, which doesn't have a
1086 # corresponding ChildBranch object.)
1087 # 1. <NestInstruction 'nest3'>
1088 # 2. <NestInstruction 'nest4'>
1089 active_instructions = []
925 last_instruction = None1090 last_instruction = None
1091
1092 # active_branches has the same structure as active_instructions, except
1093 # it holds the RecipeBranch objects in the parent path.
1094 # active_branches[-1] always refers to the parent instruction's branch.
1095 # With the above example recipe, active_branches would hold the
1096 # following entries when processing the nest5 line:
1097 #
1098 # 0. <BaseRecipeBranch None>
1099 # 1. <RecipeBranch 'nest3'>
1100 # 2. <RecipeBranch 'nest4'>
926 active_branches = []1101 active_branches = []
927 last_branch = None1102 last_branch = None
1103
928 while self.line_index < len(self.lines):1104 while self.line_index < len(self.lines):
1105 assert len(active_instructions) == len(active_branches)
929 if self.is_blankline():1106 if self.is_blankline():
930 self.new_line()1107 self.new_line()
931 continue1108 continue
@@ -935,24 +1112,34 @@ class RecipeParser:
935 continue1112 continue
936 old_indent_level = self.parse_indent()1113 old_indent_level = self.parse_indent()
937 if old_indent_level is not None:1114 if old_indent_level is not None:
938 if (old_indent_level < self.current_indent_level
939 and last_instruction != NEST_INSTRUCTION):
940 self.throw_parse_error(
941 "Not allowed to indent unless after a '%s' line" %
942 NEST_INSTRUCTION)
943 if old_indent_level < self.current_indent_level:1115 if old_indent_level < self.current_indent_level:
1116 if len(active_instructions) == 0:
1117 self.throw_parse_error(
1118 "indentation of base branch not allowed")
1119 p = last_instruction
1120 if p is None:
1121 self.throw_parse_error(
1122 "indentation under base branch not allowed")
1123 if not p.can_have_children:
1124 self.throw_parse_error(
1125 f"indentation under '{p.instruction}' not allowed")
1126 active_instructions.append(last_instruction)
944 active_branches.append(last_branch)1127 active_branches.append(last_branch)
945 else:1128 else:
946 unindent = self.current_indent_level - old_indent_level1129 unindent = self.current_indent_level - old_indent_level
1130 active_instructions = active_instructions[:unindent]
947 active_branches = active_branches[:unindent]1131 active_branches = active_branches[:unindent]
948 if last_instruction is None:1132 if len(active_instructions) == 0:
949 url = self.take_to_whitespace("branch to start from")1133 url = self.take_to_whitespace("branch to start from")
950 revspec = self.parse_optional_revspec()1134 revspec = self.parse_optional_revspec()
951 self.new_line()1135 self.new_line()
952 last_branch = BaseRecipeBranch(1136 last_branch = BaseRecipeBranch(
953 url, deb_version, self.version, revspec=revspec)1137 url, deb_version, self.version, revspec=revspec)
954 active_branches = [last_branch]1138 active_branches = [last_branch]
955 last_instruction = ""1139 # The base branch doesn't have a corresponding ChildBranch, so
1140 # None is inserted in the base branch's slot (index 0).
1141 last_instruction = None
1142 active_instructions = [None]
956 else:1143 else:
957 instruction = self.parse_instruction(1144 instruction = self.parse_instruction(
958 permitted_instructions=permitted_instructions)1145 permitted_instructions=permitted_instructions)
@@ -986,7 +1173,9 @@ class RecipeParser:
986 elif instruction == NEST_PART_INSTRUCTION:1173 elif instruction == NEST_PART_INSTRUCTION:
987 active_branches[-1].nest_part_branch(1174 active_branches[-1].nest_part_branch(
988 last_branch, path, target_subdir)1175 last_branch, path, target_subdir)
989 last_instruction = instruction1176 elif instruction == UPSTREAM_INSTRUCTION:
1177 active_branches[-1].upstream_branch(last_branch)
1178 last_instruction = active_branches[-1].child_branches[-1]
990 if len(active_branches) == 0:1179 if len(active_branches) == 0:
991 self.throw_parse_error("Empty recipe")1180 self.throw_parse_error("Empty recipe")
992 return active_branches[0]1181 return active_branches[0]
@@ -1009,21 +1198,19 @@ class RecipeParser:
1009 return version, deb_version1198 return version, deb_version
10101199
1011 def parse_instruction(self, permitted_instructions=None):1200 def parse_instruction(self, permitted_instructions=None):
1012 if self.version < 0.2:1201 options = [MERGE_INSTRUCTION, NEST_INSTRUCTION]
1013 options = (MERGE_INSTRUCTION, NEST_INSTRUCTION)1202 if self.version >= 0.2:
1014 options_str = "'%s' or '%s'" % options1203 options.append(RUN_INSTRUCTION)
1015 elif self.version < 0.3:1204 if self.version >= 0.3:
1016 options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, RUN_INSTRUCTION)1205 options.append(NEST_PART_INSTRUCTION)
1017 options_str = "'%s', '%s', or '%s'" % options1206 if self.version >= 0.5:
1018 else:1207 options.append(UPSTREAM_INSTRUCTION)
1019 options = (1208 options.sort()
1020 MERGE_INSTRUCTION, NEST_INSTRUCTION, NEST_PART_INSTRUCTION,1209 options_str = ", ".join(f"'{o}'" for o in options)
1021 RUN_INSTRUCTION)
1022 options_str = "'%s', '%s', '%s', or '%s'" % options
1023 instruction = self.peek_to_whitespace()1210 instruction = self.peek_to_whitespace()
1024 if instruction is None:1211 if instruction is None:
1025 self.throw_parse_error(1212 self.throw_parse_error(
1026 "End of line while looking for %s" % options_str)1213 "End of line while looking for one of: %s" % options_str)
1027 if instruction in options:1214 if instruction in options:
1028 if permitted_instructions is not None:1215 if permitted_instructions is not None:
1029 if instruction not in permitted_instructions:1216 if instruction not in permitted_instructions:
@@ -1034,7 +1221,7 @@ class RecipeParser:
1034 self.take_chars(len(instruction))1221 self.take_chars(len(instruction))
1035 return instruction1222 return instruction
1036 self.throw_parse_error(1223 self.throw_parse_error(
1037 "Expecting %s, got '%s'" % (options_str, instruction))1224 "Expecting one of %s; got '%s'" % (options_str, instruction))
10381225
1039 def parse_branch_id(self, instruction):1226 def parse_branch_id(self, instruction):
1040 self.parse_whitespace("the branch id", instruction=instruction)1227 self.parse_whitespace("the branch id", instruction=instruction)
diff --git a/gitbuildrecipe/tests/__init__.py b/gitbuildrecipe/tests/__init__.py
index d968f5b..e6cfde0 100644
--- a/gitbuildrecipe/tests/__init__.py
+++ b/gitbuildrecipe/tests/__init__.py
@@ -29,7 +29,7 @@ class GitRepository:
29 if not os.path.exists(path):29 if not os.path.exists(path):
30 os.makedirs(path)30 os.makedirs(path)
31 if allow_create and not os.path.exists(os.path.join(path, ".git")):31 if allow_create and not os.path.exists(os.path.join(path, ".git")):
32 self._git_call("init", "-q")32 self._git_call("init", "-b", "main")
3333
34 def _git_call(self, *args, **kwargs):34 def _git_call(self, *args, **kwargs):
35 subprocess.check_call(["git", "-C", self.path] + list(args), **kwargs)35 subprocess.check_call(["git", "-C", self.path] + list(args), **kwargs)
@@ -55,7 +55,7 @@ class GitRepository:
55 env = os.environ.copy()55 env = os.environ.copy()
56 if date is not None:56 if date is not None:
57 env["GIT_COMMITTER_DATE"] = date57 env["GIT_COMMITTER_DATE"] = date
58 self._git_call("commit", "-q", "--allow-empty", "-m", message, env=env)58 self._git_call("commit", "--allow-empty", "-m", message, env=env)
59 return self.last_revision()59 return self.last_revision()
6060
61 def branch(self, branch_name, commit):61 def branch(self, branch_name, commit):
@@ -76,7 +76,7 @@ class GitRepository:
76 "log", "-1", "--format=%P", commit).rstrip("\n").split()76 "log", "-1", "--format=%P", commit).rstrip("\n").split()
7777
78 def clone_to(self, new_path):78 def clone_to(self, new_path):
79 subprocess.check_call(["git", "clone", "-q", self.path, new_path])79 subprocess.check_call(["git", "clone", self.path, new_path])
80 return GitRepository(new_path, allow_create=False)80 return GitRepository(new_path, allow_create=False)
8181
82 def build_tree(self, shape):82 def build_tree(self, shape):
diff --git a/gitbuildrecipe/tests/test_deb_version.py b/gitbuildrecipe/tests/test_deb_version.py
index 662e811..9c005f0 100644
--- a/gitbuildrecipe/tests/test_deb_version.py
+++ b/gitbuildrecipe/tests/test_deb_version.py
@@ -33,6 +33,7 @@ from gitbuildrecipe.recipe import (
33 BaseRecipeBranch,33 BaseRecipeBranch,
34 build_tree,34 build_tree,
35 RecipeBranch,35 RecipeBranch,
36 RecipeParser,
36 resolve_revisions,37 resolve_revisions,
37 )38 )
38from gitbuildrecipe.tests import (39from gitbuildrecipe.tests import (
@@ -231,7 +232,7 @@ class ResolveRevisionsTests(GitTestCase):
231 def test_substitute_revno(self):232 def test_substitute_revno(self):
232 source = GitRepository("source")233 source = GitRepository("source")
233 source.commit("one")234 source.commit("one")
234 source._git_call("checkout", "-q", "-b", "branch")235 source._git_call("checkout", "-b", "branch")
235 source.build_tree(["a"])236 source.build_tree(["a"])
236 source.add(["a"])237 source.add(["a"])
237 source.commit("two")238 source.commit("two")
@@ -240,10 +241,9 @@ class ResolveRevisionsTests(GitTestCase):
240 resolve_revisions(241 resolve_revisions(
241 branch1, substitute_branch_vars=substitute_branch_vars)242 branch1, substitute_branch_vars=substitute_branch_vars)
242 self.assertEqual("foo-3", branch1.deb_version)243 self.assertEqual("foo-3", branch1.deb_version)
243 source._git_call("checkout", "-q", "master")244 source._git_call("checkout", "main")
244 source._git_call(245 source._git_call(
245 "merge", "-q", "--no-ff", "--commit", "-m", "merge branch",246 "merge", "--no-ff", "--commit", "-m", "merge branch", "branch")
246 "branch")
247 branch2 = BaseRecipeBranch("source", "foo-{revno}", 0.1)247 branch2 = BaseRecipeBranch("source", "foo-{revno}", 0.1)
248 resolve_revisions(248 resolve_revisions(
249 branch2, substitute_branch_vars=substitute_branch_vars)249 branch2, substitute_branch_vars=substitute_branch_vars)
@@ -254,13 +254,13 @@ class ResolveRevisionsTests(GitTestCase):
254 # the base branch was committed to by instructions such as merge.254 # the base branch was committed to by instructions such as merge.
255 source = GitRepository("source")255 source = GitRepository("source")
256 source.commit("one")256 source.commit("one")
257 source._git_call("checkout", "-q", "-b", "branch")257 source._git_call("checkout", "-b", "branch")
258 source.build_tree(["a"])258 source.build_tree(["a"])
259 source.add(["a"])259 source.add(["a"])
260 source.commit("two")260 source.commit("two")
261 source.commit("three")261 source.commit("three")
262 branch1 = BaseRecipeBranch(262 branch1 = BaseRecipeBranch(
263 "source", "foo-{revno}+{revno:branch}", 0.1, revspec="master")263 "source", "foo-{revno}+{revno:branch}", 0.1, revspec="main")
264 branch2 = RecipeBranch("branch", "source", revspec="branch")264 branch2 = RecipeBranch("branch", "source", revspec="branch")
265 branch1.merge_branch(branch2)265 branch1.merge_branch(branch2)
266 build_tree(branch1, "target")266 build_tree(branch1, "target")
@@ -268,6 +268,23 @@ class ResolveRevisionsTests(GitTestCase):
268 branch1, substitute_branch_vars=substitute_branch_vars)268 branch1, substitute_branch_vars=substitute_branch_vars)
269 self.assertEqual("foo-1+3", branch1.deb_version)269 self.assertEqual("foo-1+3", branch1.deb_version)
270270
271 def test_upstream(self):
272 source = GitRepository("source")
273 source.commit("one")
274 source._git_call("checkout", "-b", "debianized")
275 source.build_tree(["a"])
276 source.add(["a"])
277 source.commit("two")
278 source._git_call("checkout", "main")
279 recipe = (
280 "# git-build-recipe format 0.5 deb-version {revno:pristine}-{revno}\n"
281 "source debianized\n"
282 "upstream pristine source\n"
283 )
284 branch = RecipeParser(recipe).parse()
285 resolve_revisions(branch, substitute_branch_vars=substitute_branch_vars)
286 self.assertEqual(branch.deb_version, "1-2")
287
271288
272class DebUpstreamVariableTests(GitTestCase):289class DebUpstreamVariableTests(GitTestCase):
273290
@@ -398,21 +415,21 @@ class RecipeBranchTests(GitTestCase):
398 self.assertEqual("1", base_branch.deb_version)415 self.assertEqual("1", base_branch.deb_version)
399 base_branch = BaseRecipeBranch(416 base_branch = BaseRecipeBranch(
400 "base_url", "{revdate}", 0.4, revspec=commit1)417 "base_url", "{revdate}", 0.4, revspec=commit1)
401 base_branch.resolve_commit()418 base_branch.fetch()
402 substitute_branch_vars(base_branch, base_branch)419 substitute_branch_vars(base_branch, base_branch)
403 self.assertEqual("20150101", base_branch.deb_version)420 self.assertEqual("20150101", base_branch.deb_version)
404 base_branch = BaseRecipeBranch(421 base_branch = BaseRecipeBranch(
405 "base_url", "{revdate}", 0.4, revspec=commit1)422 "base_url", "{revdate}", 0.4, revspec=commit1)
406 base_branch.resolve_commit()423 base_branch.fetch()
407 child_branch = RecipeBranch("foo", "base_url", revspec=commit2)424 child_branch = RecipeBranch("foo", "base_url", revspec=commit2)
408 child_branch.resolve_commit()425 child_branch.fetch()
409 substitute_branch_vars(base_branch, child_branch)426 substitute_branch_vars(base_branch, child_branch)
410 self.assertEqual("{revdate}", base_branch.deb_version)427 self.assertEqual("{revdate}", base_branch.deb_version)
411 substitute_branch_vars(base_branch, child_branch)428 substitute_branch_vars(base_branch, child_branch)
412 self.assertEqual("{revdate}", base_branch.deb_version)429 self.assertEqual("{revdate}", base_branch.deb_version)
413 base_branch = BaseRecipeBranch(430 base_branch = BaseRecipeBranch(
414 "base_url", "{revdate:foo}", 0.4, revspec=commit1)431 "base_url", "{revdate:foo}", 0.4, revspec=commit1)
415 base_branch.resolve_commit()432 base_branch.fetch()
416 substitute_branch_vars(base_branch, child_branch)433 substitute_branch_vars(base_branch, child_branch)
417 self.assertEqual("20150102", base_branch.deb_version)434 self.assertEqual("20150102", base_branch.deb_version)
418435
diff --git a/gitbuildrecipe/tests/test_functional.py b/gitbuildrecipe/tests/test_functional.py
index 56fd216..dc796d3 100644
--- a/gitbuildrecipe/tests/test_functional.py
+++ b/gitbuildrecipe/tests/test_functional.py
@@ -195,8 +195,8 @@ class FunctionalBuilderTests(GitTestCase):
195195
196 def make_upstream_version(self, source, package_name, version, contents,196 def make_upstream_version(self, source, package_name, version, contents,
197 pristine_tar_format=None):197 pristine_tar_format=None):
198 source._git_call("checkout", "-q", "--orphan", "upstream")198 source._git_call("checkout", "--orphan", "upstream")
199 source._git_call("rm", "-qrf", ".")199 source._git_call("rm", "-rf", ".")
200 source.build_tree_contents(contents)200 source.build_tree_contents(contents)
201 source.add(["."])201 source.add(["."])
202 commit = source.commit("import upstream %s" % version)202 commit = source.commit("import upstream %s" % version)
@@ -221,12 +221,11 @@ class FunctionalBuilderTests(GitTestCase):
221 stderr=subprocess.DEVNULL, cwd=source.path)221 stderr=subprocess.DEVNULL, cwd=source.path)
222 tarfile_sha1 = sha1_file_by_name(tarfile_path)222 tarfile_sha1 = sha1_file_by_name(tarfile_path)
223 source.tag("upstream/%s" % version, commit)223 source.tag("upstream/%s" % version, commit)
224 source._git_call("checkout", "-q", "master")224 source._git_call("checkout", "main")
225 return tarfile_sha1225 return tarfile_sha1
226226
227 def make_simple_package(self, path):227 def add_simple_package_files(self, source):
228 source = GitRepository(path)228 source.build_tree(["debian/"])
229 source.build_tree(["a", "debian/"])
230 cl_contents = ("package (0.1-1) unstable; urgency=low\n * foo\n"229 cl_contents = ("package (0.1-1) unstable; urgency=low\n * foo\n"
231 " -- maint <maint@maint.org> Tue, 04 Aug 2009 "230 " -- maint <maint@maint.org> Tue, 04 Aug 2009 "
232 "10:03:10 +0100\n")231 "10:03:10 +0100\n")
@@ -237,7 +236,13 @@ class FunctionalBuilderTests(GitTestCase):
237 "Package: package\nArchitecture: all\n"),236 "Package: package\nArchitecture: all\n"),
238 ("debian/changelog", cl_contents)237 ("debian/changelog", cl_contents)
239 ])238 ])
240 source.add(["a", "debian/rules", "debian/control", "debian/changelog"])239 source.add(["debian/rules", "debian/control", "debian/changelog"])
240
241 def make_simple_package(self, path):
242 source = GitRepository(path)
243 source.build_tree(["a"])
244 source.add(["a"])
245 self.add_simple_package_files(source)
241 source.commit("one")246 source.commit("one")
242 return source247 return source
243248
@@ -579,3 +584,194 @@ class FunctionalBuilderTests(GitTestCase):
579 out, err = self.run_recipe(584 out, err = self.run_recipe(
580 "--allow-fallback-to-native test.recipe working", retcode=1)585 "--allow-fallback-to-native test.recipe working", retcode=1)
581 self.assertIn("Unknown source format 2.0\n", err)586 self.assertIn("Unknown source format 2.0\n", err)
587
588 def test_upstream_command_default_branch(self):
589 source = GitRepository("source")
590 # The debian/ directory should be filtered out when generating the orig
591 # tarball, but not the foo/debian/ directory.
592 files = ["a", "debian/a", "foo/debian/a"]
593 source.build_tree(files)
594 source.add(files)
595 source.commit("pristine sources")
596 source._git_call("checkout", "-b", "debianized")
597 source._git_call("rm", "-rf", "debian")
598 self.add_simple_package_files(source)
599 source.commit("debianized sources")
600 source._git_call("checkout", "main")
601 with open("package-0.1.recipe", "w") as f:
602 f.write(
603 "# git-build-recipe format 0.5 deb-version 0.1-1\n"
604 "source debianized\n"
605 "upstream upstream source\n")
606 self.run_recipe("package-0.1.recipe working")
607 self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
608 self.assertThat("working/package_0.1-1.diff.gz", PathExists())
609 os.mkdir("extracted-orig")
610 subprocess.check_call(
611 ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
612 cwd="extracted-orig")
613 self.assertThat("extracted-orig/package-0.1/a", PathExists())
614 self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
615 self.assertThat("extracted-orig/package-0.1/foo/debian/a", PathExists())
616
617 def test_upstream_command_specific_branch(self):
618 source = GitRepository("source")
619 # The debian/ directory should be filtered out when generating the orig
620 # tarball, but not the foo/debian/ directory.
621 files = ["a", "debian/a", "foo/debian/a"]
622 source.build_tree(files)
623 source.add(files)
624 source.commit("pristine sources")
625 source._git_call("checkout", "-b", "debianized")
626 source._git_call("rm", "-rf", "debian")
627 self.add_simple_package_files(source)
628 source.commit("debianized sources")
629 with open("package-0.1.recipe", "w") as f:
630 f.write(
631 "# git-build-recipe format 0.5 deb-version 0.1-1\n"
632 "source\n"
633 "upstream upstream source main\n")
634 self.run_recipe("package-0.1.recipe working")
635 self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
636 self.assertThat("working/package_0.1-1.diff.gz", PathExists())
637 os.mkdir("extracted-orig")
638 subprocess.check_call(
639 ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
640 cwd="extracted-orig")
641 self.assertThat("extracted-orig/package-0.1/a", PathExists())
642 self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
643 self.assertThat("extracted-orig/package-0.1/foo/debian/a", PathExists())
644
645 def test_upstream_command_bad_repo(self):
646 source = GitRepository("source")
647 files = ["a"]
648 source.build_tree(files)
649 source.add(files)
650 self.add_simple_package_files(source)
651 source.commit("one")
652 with open("package-0.1.recipe", "w") as f:
653 f.write(
654 "# git-build-recipe format 0.5 deb-version 0.1-1\n"
655 "source\n"
656 "upstream upstream missingupstream\n")
657 _, err = self.run_recipe("package-0.1.recipe working", retcode=1)
658 self.assertIn("missingupstream", err)
659
660 def test_upstream_command_bad_rev(self):
661 source = GitRepository("source")
662 files = ["a"]
663 source.build_tree(files)
664 source.add(files)
665 self.add_simple_package_files(source)
666 source.commit("one")
667 with open("package-0.1.recipe", "w") as f:
668 f.write(
669 "# git-build-recipe format 0.5 deb-version 0.1-1\n"
670 "source\n"
671 "upstream upstream source missingrev\n")
672 _, err = self.run_recipe("package-0.1.recipe working", retcode=1)
673 self.assertIn("missingrev", err)
674
675 def test_upstream_command_with_child_clean(self):
676 """Child branch that does not dirty the working dir or index."""
677 source = GitRepository("source")
678 for f in ["a", "b"]:
679 source.build_tree([f])
680 source.add([f])
681 source.commit(f)
682 self.add_simple_package_files(source)
683 source.commit("packaging")
684 source._git_call("checkout", "-b", "tomerge", "HEAD^^")
685 source.build_tree(["c"])
686 source.add(["c"])
687 source.commit("c")
688 with open("package-0.1.recipe", "w") as f:
689 f.write(
690 "# git-build-recipe format 0.5 deb-version 0.1-{revno:upstream}\n"
691 "source main\n"
692 "upstream upstream source main\n"
693 # The merge instruction creates a commit, so the working
694 # directory and index should be clean. That commit should not
695 # affect the substitution variables.
696 " merge merge source tomerge")
697 self.run_recipe("package-0.1.recipe working")
698 self.run_recipe("package-0.1.recipe working")
699 cl = changelog.Changelog(self._get_file_contents(
700 "working/package-0.1/debian/changelog"))
701 self.assertEqual("0.1-3", str(cl._blocks[0].version))
702 self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
703 self.assertThat("working/package_0.1-3.diff.gz", PathExists())
704 os.mkdir("extracted-orig")
705 subprocess.check_call(
706 ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
707 cwd="extracted-orig")
708 self.assertThat("extracted-orig/package-0.1/a", PathExists())
709 self.assertThat("extracted-orig/package-0.1/b", PathExists())
710 self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
711
712 def test_upstream_command_with_child_dirty(self):
713 """Child branch that modifies the working dir/index without commit."""
714 source = GitRepository("source")
715 source.build_tree(["a"])
716 source.add(["a"])
717 self.add_simple_package_files(source)
718 source.commit("one")
719 nested = GitRepository("nested")
720 nested.build_tree(["nestsrc/b"])
721 nested.add(["nestsrc/b"])
722 nested.commit("to nest")
723 with open("package-0.1.recipe", "w") as f:
724 f.write(
725 "# git-build-recipe format 0.5 deb-version 0.1-{revno:upstream}\n"
726 "source main\n"
727 "upstream upstream source main\n"
728 # The nest-part instruction modifies the index (and working
729 # directory to match) without committing, so it will be dirty.
730 # A commit will be created to hold the dirty contents; that
731 # commit should not affect the substitution variables.
732 " nest-part nested nested nestsrc nestdst\n")
733 self.run_recipe("package-0.1.recipe working")
734 cl = changelog.Changelog(self._get_file_contents(
735 "working/package-0.1/debian/changelog"))
736 self.assertEqual("0.1-1", str(cl._blocks[0].version))
737 self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
738 self.assertThat("working/package_0.1-1.diff.gz", PathExists())
739 os.mkdir("extracted-orig")
740 subprocess.check_call(
741 ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
742 cwd="extracted-orig")
743 self.assertThat("extracted-orig/package-0.1/a", PathExists())
744 self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
745 self.assertThat("extracted-orig/package-0.1/nestdst/b", PathExists())
746
747 def test_upstream_command_with_child_and_base_dirty(self):
748 """The working dir or index is dirty before the upstream is applied."""
749 source = GitRepository("source")
750 source.build_tree(["a"])
751 source.add(["a"])
752 self.add_simple_package_files(source)
753 source.commit("one")
754 nested = GitRepository("nested")
755 nested.build_tree(["nestsrc/b"])
756 nested.add(["nestsrc/b"])
757 nested.commit("to nest")
758 with open("package-0.1.recipe", "w") as f:
759 f.write(
760 "# git-build-recipe format 0.5 deb-version 0.1-1\n"
761 "source main\n"
762 "nest-part nested-source nested nestsrc nestdst-source\n"
763 "upstream upstream source main\n"
764 # The nest-part instruction modifies the index (and working
765 # directory to match) without committing, so it will be dirty.
766 " nest-part nested-upstream nested nestsrc nestdst-upstream\n")
767 self.run_recipe("package-0.1.recipe working")
768 self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
769 self.assertThat("working/package_0.1-1.diff.gz", PathExists())
770 os.mkdir("extracted-orig")
771 subprocess.check_call(
772 ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
773 cwd="extracted-orig")
774 self.assertThat("extracted-orig/package-0.1/a", PathExists())
775 self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
776 self.assertThat(
777 "extracted-orig/package-0.1/nestdst-upstream/b", PathExists())
diff --git a/gitbuildrecipe/tests/test_recipe.py b/gitbuildrecipe/tests/test_recipe.py
index bfc6452..cb051f3 100644
--- a/gitbuildrecipe/tests/test_recipe.py
+++ b/gitbuildrecipe/tests/test_recipe.py
@@ -39,6 +39,7 @@ from gitbuildrecipe.recipe import (
39 SAFE_INSTRUCTIONS,39 SAFE_INSTRUCTIONS,
40 TargetAlreadyExists,40 TargetAlreadyExists,
41 USAGE,41 USAGE,
42 UnknownRevisionError,
42 )43 )
43from gitbuildrecipe.tests import (44from gitbuildrecipe.tests import (
44 GitRepository,45 GitRepository,
@@ -144,9 +145,9 @@ class RecipeParserTests(GitTestCase):
144 "# git-build-recipe format 0.1 deb-version 1 foo")145 "# git-build-recipe format 0.1 deb-version 1 foo")
145146
146 def tests_rejects_indented_base_branch(self):147 def tests_rejects_indented_base_branch(self):
147 self.assertParseError(2, 3, "Not allowed to indent unless after "148 self.assertParseError(
148 "a 'nest' line", self.get_recipe,149 2, 3, "indentation of base branch not allowed", self.get_recipe,
149 self.basic_header + " http://foo.org/")150 self.basic_header + " http://foo.org/")
150151
151 def tests_rejects_text_after_base_branch(self):152 def tests_rejects_text_after_base_branch(self):
152 self.assertParseError(2, 19, "Expecting the end of the line, "153 self.assertParseError(2, 19, "Expecting the end of the line, "
@@ -154,8 +155,8 @@ class RecipeParserTests(GitTestCase):
154 self.basic_header + "http://foo.org/ 2 foo")155 self.basic_header + "http://foo.org/ 2 foo")
155156
156 def tests_rejects_unknown_instruction(self):157 def tests_rejects_unknown_instruction(self):
157 self.assertParseError(3, 1, "Expecting 'merge', 'nest', 'nest-part', "158 self.assertParseError(3, 1, "Expecting one of 'merge', 'nest', 'nest-part', 'run'; "
158 "or 'run', got 'cat'", self.get_recipe,159 "got 'cat'", self.get_recipe,
159 self.basic_header + "http://foo.org/\n" + "cat")160 self.basic_header + "http://foo.org/\n" + "cat")
160161
161 def test_rejects_merge_no_name(self):162 def test_rejects_merge_no_name(self):
@@ -225,15 +226,14 @@ class RecipeParserTests(GitTestCase):
225 self.basic_header_and_branch + "nest foo url bar 2 baz")226 self.basic_header_and_branch + "nest foo url bar 2 baz")
226227
227 def test_rejects_indent_after_first_branch(self):228 def test_rejects_indent_after_first_branch(self):
228 self.assertParseError(3, 3, "Not allowed to indent unless after "229 self.assertParseError(
229 "a 'nest' line", self.get_recipe,230 3, 3, "indentation under base branch not allowed", self.get_recipe,
230 self.basic_header_and_branch + " nest foo url bar")231 self.basic_header_and_branch + " nest foo url bar")
231232
232 def test_rejects_indent_after_merge(self):233 def test_rejects_indent_after_merge(self):
233 self.assertParseError(4, 3, "Not allowed to indent unless after "234 self.assertParseError(
234 "a 'nest' line", self.get_recipe,235 4, 3, "indentation under 'merge' not allowed", self.get_recipe,
235 self.basic_header_and_branch + "merge foo url\n"236 self.basic_header_and_branch + "merge foo url\n nest baz url bar")
236 + " nest baz url bar")
237237
238 def test_rejects_tab_indent(self):238 def test_rejects_tab_indent(self):
239 self.assertParseError(4, 3, "Indents may not be done by tabs",239 self.assertParseError(4, 3, "Indents may not be done by tabs",
@@ -466,14 +466,14 @@ class RecipeParserTests(GitTestCase):
466 def test_old_format_rejects_run(self):466 def test_old_format_rejects_run(self):
467 header = ("# git-build-recipe format 0.1 deb-version "467 header = ("# git-build-recipe format 0.1 deb-version "
468 + self.deb_version +"\n")468 + self.deb_version +"\n")
469 self.assertParseError(3, 1, "Expecting 'merge' or 'nest', got 'run'"469 self.assertParseError(3, 1, "Expecting one of 'merge', 'nest'; got 'run'"
470 , self.get_recipe, header + "http://foo.org/\n"470 , self.get_recipe, header + "http://foo.org/\n"
471 + "run touch test \n")471 + "run touch test \n")
472472
473 def test_old_format_rejects_nest_part(self):473 def test_old_format_rejects_nest_part(self):
474 header = ("# git-build-recipe format 0.2 deb-version "474 header = ("# git-build-recipe format 0.2 deb-version "
475 + self.deb_version +"\n")475 + self.deb_version +"\n")
476 self.assertParseError(3, 1, "Expecting 'merge', 'nest', or 'run', "476 self.assertParseError(3, 1, "Expecting one of 'merge', 'nest', 'run'; "
477 "got 'nest-part'" , self.get_recipe,477 "got 'nest-part'" , self.get_recipe,
478 header + "http://foo.org/\nnest-part packaging foo test \n")478 header + "http://foo.org/\nnest-part packaging foo test \n")
479479
@@ -522,6 +522,52 @@ class RecipeParserTests(GitTestCase):
522 " ./foo/../..", NEST_INSTRUCTION, self.get_recipe,522 " ./foo/../..", NEST_INSTRUCTION, self.get_recipe,
523 self.basic_header_and_branch + "nest nest url ./foo/../..\n")523 self.basic_header_and_branch + "nest nest url ./foo/../..\n")
524524
525 def test_upstream_default_branch(self):
526 recipe = (
527 "# git-build-recipe format 0.5 deb-version 1.0-1\n"
528 "http://exmaple.test/repo.git\n"
529 "upstream pristine http://example.test/upstream.git\n"
530 )
531 base = self.get_recipe(recipe)
532 self.assertEqual(str(base), recipe)
533
534 def test_upstream_specific_branch(self):
535 recipe = (
536 "# git-build-recipe format 0.5 deb-version 1.0-1\n"
537 "http://exmaple.test/repo.git\n"
538 "upstream pristine http://example.test/upstream.git somebranch\n"
539 )
540 base = self.get_recipe(recipe)
541 self.assertEqual(str(base), recipe)
542
543 def test_upstream_with_child(self):
544 recipe = (
545 "# git-build-recipe format 0.5 deb-version 1.0-1\n"
546 "http://exmaple.test/repo.git\n"
547 "upstream pristine http://example.test/upstream.git somebranch\n"
548 " merge tomerge http://example.test/tomerge.git\n"
549 )
550 base = self.get_recipe(recipe)
551 self.assertEqual(str(base), recipe)
552
553 def test_error_upstream_as_child(self):
554 recipe = (
555 "# git-build-recipe format 0.5 deb-version 1.0-1\n"
556 "http://exmaple.test/repo.git\n"
557 "nest nested http://example.test/nest.git foo\n"
558 " upstream pristine http://example.test/upstream.git somebranch\n"
559 )
560 self.assertRaises(Exception, self.get_recipe, recipe)
561
562 def test_error_upstream_twice(self):
563 recipe = (
564 "# git-build-recipe format 0.5 deb-version 1.0-1\n"
565 "http://exmaple.test/repo.git\n"
566 "upstream pristine http://example.test/upstream.git somebranch\n"
567 "upstream other http://example.test/upstream.git somebranch\n"
568 )
569 self.assertRaises(Exception, self.get_recipe, recipe)
570
525571
526class BuildTreeTests(GitTestCase):572class BuildTreeTests(GitTestCase):
527573
@@ -896,14 +942,12 @@ class BuildTreeTests(GitTestCase):
896 source.build_tree(["a"])942 source.build_tree(["a"])
897 source.add(["a"])943 source.add(["a"])
898 commit = source.commit("one")944 commit = source.commit("one")
899 source.branch("source/master", commit)
900 source.set_head("refs/heads/nonexistent")945 source.set_head("refs/heads/nonexistent")
901 base_branch = BaseRecipeBranch(946 base_branch = BaseRecipeBranch("source", "1", 0.2, revspec="main")
902 "source", "1", 0.2, revspec="source/master")
903 pull_or_clone(base_branch, "target")947 pull_or_clone(base_branch, "target")
904 target = GitRepository("target", allow_create=False)948 target = GitRepository("target", allow_create=False)
905 self.assertEqual(commit, target.last_revision())949 self.assertEqual(commit, target.last_revision())
906 self.assertEqual(commit, target.rev_parse("source/master"))950 self.assertEqual(commit, target.rev_parse("source/main"))
907951
908 def test_build_tree_runs_commands(self):952 def test_build_tree_runs_commands(self):
909 source = GitRepository("source")953 source = GitRepository("source")
@@ -917,15 +961,14 @@ class BuildTreeTests(GitTestCase):
917 self.assertEqual(commit, target.rev_parse("HEAD"))961 self.assertEqual(commit, target.rev_parse("HEAD"))
918 self.assertEqual(commit, base_branch.commit)962 self.assertEqual(commit, base_branch.commit)
919963
920 def test_error_on_merge_revspec(self):964 def test_error_on_unknown_revision(self):
921 # See bug 416950
922 source = GitRepository("source")965 source = GitRepository("source")
923 source.commit("one")966 source.commit("one")
924 base_branch = BaseRecipeBranch("source", "1", 0.2)967 base_branch = BaseRecipeBranch("source", "1", 0.2)
925 merged_branch = RecipeBranch("merged", "source", revspec="debian")968 merged_branch = RecipeBranch("merged", "source", revspec="debian")
926 base_branch.merge_branch(merged_branch)969 base_branch.merge_branch(merged_branch)
927 e = self.assertRaises(MergeFailed, build_tree, base_branch, "target")970 e = self.assertRaises(UnknownRevisionError, build_tree, base_branch, "target")
928 self.assertIn("ambiguous argument 'debian'", str(e))971 self.assertIn("debian", str(e))
929972
930973
931class StringifyTests(GitTestCase):974class StringifyTests(GitTestCase):

Subscribers

People subscribed via source and target branches