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
1diff --git a/gitbuildrecipe/deb_util.py b/gitbuildrecipe/deb_util.py
2index d0a6666..c3227ba 100644
3--- a/gitbuildrecipe/deb_util.py
4+++ b/gitbuildrecipe/deb_util.py
5@@ -32,13 +32,6 @@ class MissingDependency(Exception):
6 pass
7
8
9-class NoSuchTag(Exception):
10-
11- def __init__(self, tag_name):
12- super().__init__()
13- self.tag_name = tag_name
14-
15-
16 def debian_source_package_name(control_path):
17 """Open a debian control file and extract the package name.
18
19@@ -48,53 +41,6 @@ def debian_source_package_name(control_path):
20 return control["Source"]
21
22
23-def extract_upstream_tarball(path, package, version, dest_dir):
24- """Extract the upstream tarball from a Git repository.
25-
26- :param path: Path to the Git repository
27- :param package: Package name
28- :param version: Package version
29- :param dest_dir: Destination directory
30- """
31- prefix = "%s_%s.orig.tar." % (package, version)
32- dest_filename = None
33- pristine_tar_list = subprocess.Popen(
34- ["pristine-tar", "list"], stdout=subprocess.PIPE, cwd=path)
35- try:
36- for line in pristine_tar_list.stdout:
37- line = line.decode("UTF-8", errors="replace").rstrip("\n")
38- if line.startswith(prefix):
39- dest_filename = line
40- finally:
41- pristine_tar_list.wait()
42- if dest_filename is not None:
43- subprocess.check_call(
44- ["pristine-tar", "checkout",
45- os.path.abspath(os.path.join(dest_dir, dest_filename))],
46- cwd=path)
47- else:
48- tag_names = ["upstream/%s" % version, "upstream-%s" % version]
49- git_tag_list = subprocess.Popen(
50- ["git", "tag"], stdout=subprocess.PIPE, cwd=path)
51- try:
52- for line in git_tag_list.stdout:
53- line = line.decode("UTF-8", errors="replace").rstrip("\n")
54- if line in tag_names:
55- tag = line
56- break
57- else:
58- raise NoSuchTag(tag_names[0])
59- finally:
60- git_tag_list.wait()
61- # Default to .tar.gz
62- dest_filename = prefix + "gz"
63- with open(os.path.join(dest_dir, dest_filename), "wb") as dest:
64- subprocess.check_call(
65- ["git", "archive", "--format=tar.gz",
66- "--prefix=%s-%s/" % (package, version), tag],
67- stdout=dest, cwd=path)
68-
69-
70 def add_autobuild_changelog_entry(base_branch, basedir, package,
71 distribution=None, author_name=None,
72 author_email=None, append_version=None):
73diff --git a/gitbuildrecipe/main.py b/gitbuildrecipe/main.py
74index 30e6bec..a241744 100644
75--- a/gitbuildrecipe/main.py
76+++ b/gitbuildrecipe/main.py
77@@ -26,11 +26,9 @@ import tempfile
78 from debian.changelog import Changelog
79
80 from gitbuildrecipe.deb_util import (
81- NoSuchTag,
82 add_autobuild_changelog_entry,
83 build_source_package,
84 debian_source_package_name,
85- extract_upstream_tarball,
86 force_native_format,
87 get_source_format,
88 )
89@@ -40,6 +38,7 @@ from gitbuildrecipe.deb_version import (
90 substitute_time,
91 )
92 from gitbuildrecipe.recipe import (
93+ NoSuchTag,
94 build_tree,
95 parse_recipe,
96 resolve_revisions,
97@@ -159,9 +158,9 @@ def main():
98 current_format == "3.0 (quilt)"):
99 # Non-native package
100 try:
101- extract_upstream_tarball(
102- base_branch.path, package_name,
103- package_version.upstream_version, working_basedir)
104+ base_branch.get_upstream_tarball(
105+ package_name, package_version.upstream_version,
106+ working_basedir)
107 except NoSuchTag as e:
108 if not args.allow_fallback_to_native:
109 parser.error(
110diff --git a/gitbuildrecipe/recipe.py b/gitbuildrecipe/recipe.py
111index f932ca7..0ebdfb2 100644
112--- a/gitbuildrecipe/recipe.py
113+++ b/gitbuildrecipe/recipe.py
114@@ -30,16 +30,18 @@ MERGE_INSTRUCTION = "merge"
115 NEST_PART_INSTRUCTION = "nest-part"
116 NEST_INSTRUCTION = "nest"
117 RUN_INSTRUCTION = "run"
118+UPSTREAM_INSTRUCTION = "upstream"
119 USAGE = {
120 MERGE_INSTRUCTION: 'merge NAME BRANCH [REVISION]',
121 NEST_INSTRUCTION: 'nest NAME BRANCH TARGET-DIR [REVISION]',
122 NEST_PART_INSTRUCTION:
123 'nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]',
124 RUN_INSTRUCTION: 'run COMMAND',
125+ UPSTREAM_INSTRUCTION: 'upstream NAME BRANCH [REVISION]',
126 }
127
128 SAFE_INSTRUCTIONS = [
129- MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION]
130+ MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION, UPSTREAM_INSTRUCTION]
131
132
133 class FormattedError(Exception):
134@@ -68,6 +70,22 @@ class FormattedError(Exception):
135 __hash__ = Exception.__hash__
136
137
138+class NoSuchTag(Exception):
139+
140+ def __init__(self, tag_name):
141+ super().__init__()
142+ self.tag_name = tag_name
143+
144+
145+class UnknownRevisionError(Exception):
146+
147+ def __init__(self, rev):
148+ self.rev = rev
149+
150+ def __str__(self):
151+ return str(self.rev)
152+
153+
154 class SubstitutionUnavailable(FormattedError):
155
156 _fmt = "Substitution for %(name)s not available: %(reason)s"
157@@ -161,7 +179,7 @@ class RevisionVariable(BranchSubstitutionVariable):
158 def __init__(self, branch):
159 super().__init__(branch.name)
160 self.branch = branch
161- self.commit = branch.commit
162+ self.commit = branch.commit_for_substvars
163
164
165 class RevnoVariable(RevisionVariable):
166@@ -299,17 +317,13 @@ def pull_or_clone(base_branch, target_path):
167 os.rmdir(target_path)
168 if not os.path.exists(target_path):
169 os.makedirs(target_path)
170- base_branch.git_call("init")
171+ base_branch.git_call("init", "-b", "main")
172 for short, base in insteadof.items():
173 base_branch.git_call("config", "url.%s.insteadOf" % base, short)
174 elif not os.path.exists(os.path.join(target_path, ".git")):
175 raise TargetAlreadyExists(target_path)
176+ base_branch.fetch()
177 try:
178- fetch_branches(base_branch)
179- except subprocess.CalledProcessError as e:
180- raise FetchFailed(e.output)
181- try:
182- base_branch.resolve_commit()
183 # Check out the commit hash directly to detach HEAD and ensure
184 # commits (eg. by a merge instruction) don't affect substitution
185 # variables.
186@@ -322,30 +336,6 @@ def pull_or_clone(base_branch, target_path):
187 raise CheckoutFailed(e.output)
188
189
190-def fetch_branches(child_branch):
191- url = child_branch.url
192- parsed_url = urlparse(url)
193- if not parsed_url.scheme and not parsed_url.path.startswith("/"):
194- url = os.path.abspath(url)
195- # Fetch the remote HEAD. This may not exist, which is OK as long as the
196- # recipe uses explicit branch names.
197- try:
198- child_branch.git_call(
199- "fetch", url,
200- "HEAD:refs/remotes/%s/HEAD" % child_branch.remote_name,
201- silent=True)
202- except subprocess.CalledProcessError as e:
203- logging.info(e.output)
204- logging.info(
205- "Failed to fetch HEAD; recipe instructions for this repository "
206- "that do not specify a branch name will fail.")
207- # Fetch all remote branches and (implicitly) any tags that reference
208- # commits in those refs. Tags that aren't on a branch won't be fetched.
209- child_branch.git_call(
210- "fetch", url,
211- "refs/heads/*:refs/remotes/%s/*" % child_branch.remote_name)
212-
213-
214 @lru_cache(maxsize=1)
215 def _git_version():
216 raw_git_version = subprocess.check_output(
217@@ -362,9 +352,8 @@ def merge_branch(child_branch, target_path):
218 :param target_path: The tree to merge into.
219 """
220 child_branch.path = target_path
221- fetch_branches(child_branch)
222+ child_branch.fetch()
223 try:
224- child_branch.resolve_commit()
225 cmd = ["merge", "--commit"]
226 if _git_version() >= "1:2.9.0":
227 cmd.append("--allow-unrelated-histories")
228@@ -391,10 +380,9 @@ def nest_part_branch(child_branch, target_path, subpath, target_subdir=None):
229 if target_subdir is None:
230 target_subdir = os.path.basename(subpath)
231 # XXX should handle updating as well
232- assert not os.path.exists(target_subdir)
233+ assert not os.path.exists(os.path.join(target_path, target_subdir))
234 child_branch.path = target_path
235- fetch_branches(child_branch)
236- child_branch.resolve_commit()
237+ child_branch.fetch()
238 child_branch.git_call(
239 "read-tree", "--prefix", target_subdir, "-u",
240 child_branch.commit + ":" + subpath)
241@@ -408,7 +396,6 @@ def _build_inner_tree(base_branch, target_path):
242 "Retrieving %s'%s' to put at '%s'." %
243 (revision_of, base_branch.url, target_path))
244 pull_or_clone(base_branch, target_path)
245- base_branch.resolve_commit()
246 for instruction in base_branch.child_branches:
247 instruction.apply(target_path)
248
249@@ -417,8 +404,8 @@ def _resolve_revisions_recurse(new_branch, substitute_branch_vars,
250 if_changed_from=None):
251 changed = False
252 if substitute_branch_vars is not None:
253- # XXX need to make sure new_branch has been fetched
254- new_branch.resolve_commit()
255+ if new_branch.commit is None:
256+ new_branch.fetch()
257 substitute_branch_vars(new_branch)
258 if (if_changed_from is not None and
259 (new_branch.revspec is not None or
260@@ -499,6 +486,10 @@ class ChildBranch:
261
262 can_have_children = False
263
264+ @property
265+ def instruction(self):
266+ raise NotImplementedError("instruction property not set")
267+
268 def __init__(self, recipe_branch, nest_path=None):
269 self.recipe_branch = recipe_branch
270 self.nest_path = nest_path
271@@ -523,6 +514,8 @@ class ChildBranch:
272
273 class CommandInstruction(ChildBranch):
274
275+ instruction = RUN_INSTRUCTION
276+
277 def apply(self, target_path):
278 # it's a command
279 logging.info("Running '%s' in '%s'." % (self.nest_path, target_path))
280@@ -535,6 +528,8 @@ class CommandInstruction(ChildBranch):
281
282 class MergeInstruction(ChildBranch):
283
284+ instruction = MERGE_INSTRUCTION
285+
286 def apply(self, target_path):
287 revision_of = ""
288 if self.recipe_branch.revspec is not None:
289@@ -556,6 +551,8 @@ class MergeInstruction(ChildBranch):
290
291 class NestPartInstruction(ChildBranch):
292
293+ instruction = NEST_PART_INSTRUCTION
294+
295 def __init__(self, recipe_branch, subpath, target_subdir):
296 ChildBranch.__init__(self, recipe_branch)
297 self.subpath = subpath
298@@ -584,6 +581,8 @@ class NestPartInstruction(ChildBranch):
299
300 class NestInstruction(ChildBranch):
301
302+ instruction = NEST_INSTRUCTION
303+
304 can_have_children = True
305
306 def apply(self, target_path):
307@@ -602,6 +601,47 @@ class NestInstruction(ChildBranch):
308 self.recipe_branch.name)
309
310
311+class UpstreamInstruction(ChildBranch):
312+
313+ instruction = UPSTREAM_INSTRUCTION
314+
315+ can_have_children = True
316+
317+ def apply(self, target_path):
318+ b = self.recipe_branch
319+ b.path = target_path
320+ if len(b.child_branches) == 0:
321+ b.fetch()
322+ return
323+ is_clean = lambda: b.git_output(
324+ "status", "--porcelain", "--ignored", "-u") == ""
325+ head_backup = b._get_commit_id("HEAD")
326+ clean = is_clean()
327+ if not clean:
328+ b.git_call("stash", "--all")
329+ _build_inner_tree(b, target_path)
330+ if not is_clean():
331+ b.git_call("stash", "--all")
332+ tree = b._get_commit_id("refs/stash@{0}")
333+ b.git_call("stash", "pop", "--index")
334+ b.git_call("read-tree", tree)
335+ b.git_call("commit", "-m", "upstream after child modifications")
336+ # The commit(s) made above should not affect any substitution variables.
337+ b._commit_for_substvars = b.commit
338+ b.commit = b._get_commit_id("HEAD")
339+ b.git_call("checkout", head_backup)
340+ if not clean:
341+ b.git_call("stash", "pop", "--index")
342+
343+ def as_text(self):
344+ b = self.recipe_branch
345+ c = self._get_commit_part()
346+ return f"{UPSTREAM_INSTRUCTION} {b.name} {b.url}{c}"
347+
348+ def __repr__(self):
349+ return f"<{self.__class__.__name__} {self.recipe_branch.name!r}>"
350+
351+
352 class RecipeBranch:
353 """A nested structure that represents a Recipe.
354
355@@ -632,6 +672,14 @@ class RecipeBranch:
356 self.child_branches = []
357 self.commit = None
358 self.path = None
359+ self._commit_for_substvars = None
360+ self._upstream_branch = None
361+
362+ @property
363+ def commit_for_substvars(self):
364+ if self._commit_for_substvars is None:
365+ return self.commit
366+ return self._commit_for_substvars
367
368 @property
369 def remote_name(self):
370@@ -649,6 +697,41 @@ class RecipeBranch:
371 raise Exception(
372 "Repository at %s has not been cloned yet" % self.url)
373
374+ def fetch(self):
375+ url = self.url
376+ parsed_url = urlparse(url)
377+ if not parsed_url.scheme and not parsed_url.path.startswith("/"):
378+ url = os.path.abspath(url)
379+ # Fetch the remote HEAD. This may not exist, which is OK as long as the
380+ # recipe uses explicit branch names.
381+ try:
382+ self.git_call(
383+ "fetch", url, "HEAD:refs/remotes/%s/HEAD" % self.remote_name,
384+ silent=True)
385+ except subprocess.CalledProcessError as e:
386+ logging.info(e.output)
387+ logging.info(
388+ "Failed to fetch HEAD; recipe instructions for this repository "
389+ "that do not specify a branch name will fail.")
390+ # Fetch all remote branches and (implicitly) any tags that reference
391+ # commits in those refs. Tags that aren't on a branch won't be fetched.
392+ try:
393+ self.git_call(
394+ "fetch", url,
395+ "refs/heads/*:refs/remotes/%s/*" % self.remote_name)
396+ except subprocess.CalledProcessError as e:
397+ raise FetchFailed(e.output)
398+ try:
399+ self.commit = self._get_commit_id(
400+ self.remote_name + "/" + self.get_revspec())
401+ return
402+ except UnknownRevisionError:
403+ pass
404+ # Not a remote-prefixed ref. Try a global search.
405+ # XXX: This allows cross-branch pollution, but we don't have
406+ # much choice without reimplementing rev-parse ourselves.
407+ self.commit = self._get_commit_id(self.get_revspec())
408+
409 def git_call(self, *args, **kwargs):
410 cmd = ["git", "-C", self._get_git_path()] + list(args)
411 silent = kwargs.pop("silent", False)
412@@ -675,24 +758,12 @@ class RecipeBranch:
413 def get_revspec(self):
414 return self.revspec if self.revspec is not None else "HEAD"
415
416- def resolve_commit(self):
417- """Resolve the commit for this branch."""
418- # Capturing stderr is a bit dodgy, but it's the most convenient way
419- # to capture it for any exceptions. We know that git rev-parse does
420- # not write to stderr on success.
421+ def _get_commit_id(self, rev):
422 try:
423- self.commit = self.git_output(
424- "rev-parse", "%s/%s" % (self.remote_name, self.get_revspec()),
425- stderr=subprocess.STDOUT).rstrip("\n")
426- return
427- except subprocess.CalledProcessError:
428- pass
429- # Not a remote-prefixed ref. Try a global search.
430- # XXX: This allows cross-branch pollution, but we don't have
431- # much choice without reimplementing rev-parse ourselves.
432- self.commit = self.git_output(
433- "rev-parse", self.get_revspec(),
434- stderr=subprocess.STDOUT).rstrip("\n")
435+ return self.git_output(
436+ "rev-parse", "--revs-only", "--verify", "-q", rev).rstrip("\n")
437+ except subprocess.CalledProcessError as e:
438+ raise UnknownRevisionError(rev) from e
439
440 def merge_branch(self, branch):
441 """Merge a child branch into this one.
442@@ -733,6 +804,19 @@ class RecipeBranch:
443 """
444 self.child_branches.append(CommandInstruction(None, command))
445
446+ def upstream_branch(self, branch):
447+ """Mark a branch as containing the original pristine sources.
448+
449+ :param branch: The `RecipeBranch` referencing the pristine sources.
450+ """
451+ if self.name is not None:
452+ raise Exception(
453+ "the upstream command only applies to the base branch")
454+ if self._upstream_branch is not None:
455+ raise Exception("upstream already set for the base branch")
456+ self.child_branches.append(UpstreamInstruction(branch))
457+ self._upstream_branch = branch
458+
459 def different_shape_to(self, other_branch):
460 """Test whether the name, url, and child_branches are the same."""
461 if self.name != other_branch.name:
462@@ -851,6 +935,65 @@ class BaseRecipeBranch(RecipeBranch):
463 RecipeParser(manifest).parse()
464 return manifest
465
466+ def get_upstream_tarball(self, package, version, dest_dir):
467+ """Generate the upstream tarball (*.orig.tar.*).
468+
469+ :param package: Package name
470+ :param version: Package version
471+ :param dest_dir: Destination directory
472+ """
473+ prefix = "%s_%s.orig.tar." % (package, version)
474+ if self._upstream_branch is not None:
475+ # self._upstream_branch.fetch() has already been called, and it
476+ # should NOT be called again here otherwise it will undo any changes
477+ # made by child instructions.
478+ dest_filename = prefix + "gz"
479+ tarball = os.path.abspath(os.path.join(dest_dir, dest_filename))
480+ # With `Format: 1.0` packages, dpkg-source does not delete the
481+ # debian directory (if present in the *.orig.tar.gz) before applying
482+ # the *.diff.gz. Exclude the debian directory when generating the
483+ # orig archive to avoid problems applying the patch. (The diff.gz
484+ # can't delete files, only empty them.) This isn't necessary for
485+ # `Format: 3.0 (quilt)` packages, but it doesn't really hurt either.
486+ # If the user is concerned with perfect representation of the
487+ # upstream sources with `Format: 3.0 (quilt)` packages, they should
488+ # use pristine-tar.
489+ self.git_call(
490+ "archive", f"--output={tarball}",
491+ f"--prefix={package}-{version}/", self._upstream_branch.commit,
492+ ":(exclude)debian")
493+ return
494+ dest_filename = None
495+ pristine_tar_list = subprocess.Popen(
496+ ["pristine-tar", "list"], stdout=subprocess.PIPE, cwd=self.path)
497+ try:
498+ for line in pristine_tar_list.stdout:
499+ line = line.decode("UTF-8", errors="replace").rstrip("\n")
500+ if line.startswith(prefix):
501+ dest_filename = line
502+ finally:
503+ pristine_tar_list.wait()
504+ if dest_filename is not None:
505+ subprocess.check_call(
506+ ["pristine-tar", "checkout",
507+ os.path.abspath(os.path.join(dest_dir, dest_filename))],
508+ cwd=self.path)
509+ else:
510+ tag_names = ["upstream/%s" % version, "upstream-%s" % version]
511+ for line in self.git_output("tag").rstrip("\n").split("\n"):
512+ if line in tag_names:
513+ tag = line
514+ break
515+ else:
516+ raise NoSuchTag(tag_names[0])
517+ # Default to .tar.gz
518+ dest_filename = prefix + "gz"
519+ with open(os.path.join(dest_dir, dest_filename), "wb") as dest:
520+ subprocess.check_call(
521+ ["git", "archive", "--format=tar.gz",
522+ "--prefix=%s-%s/" % (package, version), tag],
523+ stdout=dest, cwd=self.path)
524+
525
526 class RecipeParseError(FormattedError):
527
528@@ -887,7 +1030,7 @@ class RecipeParser:
529 eol_char = "\n"
530 digit_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
531
532- NEWEST_VERSION = 0.4
533+ NEWEST_VERSION = 0.5
534
535 def __init__(self, f):
536 """Create a `RecipeParser`.
537@@ -922,10 +1065,44 @@ class RecipeParser:
538 self.seen_paths = {".": 1}
539 version, deb_version = self.parse_header()
540 self.version = version
541+
542+ # active_instructions contains the ChildBranch objects in the current
543+ # instruction's parent path, in ancestry order. Thus,
544+ # active_instructions[-1] always refers to the current instruction's
545+ # parent instruction. For example, consider the following recipe:
546+ #
547+ # # git-build-recipe format 0.4 deb-version 1.0-1
548+ # http://example.test/repo.git
549+ # nest nest1 http://example.test/nest1.git nest1
550+ # nest nest2 http://example.test/nest2.git nest2
551+ # nest nest3 http://example.test/nest3.git nest3
552+ # nest nest4 http://example.test/nest4.git nest4
553+ # nest nest5 http://example.test/nest5.git nest5
554+ #
555+ # When the nest5 line is processed, active_instructions will contain the
556+ # following entries:
557+ #
558+ # 0. None (This entry is for the base branch, which doesn't have a
559+ # corresponding ChildBranch object.)
560+ # 1. <NestInstruction 'nest3'>
561+ # 2. <NestInstruction 'nest4'>
562+ active_instructions = []
563 last_instruction = None
564+
565+ # active_branches has the same structure as active_instructions, except
566+ # it holds the RecipeBranch objects in the parent path.
567+ # active_branches[-1] always refers to the parent instruction's branch.
568+ # With the above example recipe, active_branches would hold the
569+ # following entries when processing the nest5 line:
570+ #
571+ # 0. <BaseRecipeBranch None>
572+ # 1. <RecipeBranch 'nest3'>
573+ # 2. <RecipeBranch 'nest4'>
574 active_branches = []
575 last_branch = None
576+
577 while self.line_index < len(self.lines):
578+ assert len(active_instructions) == len(active_branches)
579 if self.is_blankline():
580 self.new_line()
581 continue
582@@ -935,24 +1112,34 @@ class RecipeParser:
583 continue
584 old_indent_level = self.parse_indent()
585 if old_indent_level is not None:
586- if (old_indent_level < self.current_indent_level
587- and last_instruction != NEST_INSTRUCTION):
588- self.throw_parse_error(
589- "Not allowed to indent unless after a '%s' line" %
590- NEST_INSTRUCTION)
591 if old_indent_level < self.current_indent_level:
592+ if len(active_instructions) == 0:
593+ self.throw_parse_error(
594+ "indentation of base branch not allowed")
595+ p = last_instruction
596+ if p is None:
597+ self.throw_parse_error(
598+ "indentation under base branch not allowed")
599+ if not p.can_have_children:
600+ self.throw_parse_error(
601+ f"indentation under '{p.instruction}' not allowed")
602+ active_instructions.append(last_instruction)
603 active_branches.append(last_branch)
604 else:
605 unindent = self.current_indent_level - old_indent_level
606+ active_instructions = active_instructions[:unindent]
607 active_branches = active_branches[:unindent]
608- if last_instruction is None:
609+ if len(active_instructions) == 0:
610 url = self.take_to_whitespace("branch to start from")
611 revspec = self.parse_optional_revspec()
612 self.new_line()
613 last_branch = BaseRecipeBranch(
614 url, deb_version, self.version, revspec=revspec)
615 active_branches = [last_branch]
616- last_instruction = ""
617+ # The base branch doesn't have a corresponding ChildBranch, so
618+ # None is inserted in the base branch's slot (index 0).
619+ last_instruction = None
620+ active_instructions = [None]
621 else:
622 instruction = self.parse_instruction(
623 permitted_instructions=permitted_instructions)
624@@ -986,7 +1173,9 @@ class RecipeParser:
625 elif instruction == NEST_PART_INSTRUCTION:
626 active_branches[-1].nest_part_branch(
627 last_branch, path, target_subdir)
628- last_instruction = instruction
629+ elif instruction == UPSTREAM_INSTRUCTION:
630+ active_branches[-1].upstream_branch(last_branch)
631+ last_instruction = active_branches[-1].child_branches[-1]
632 if len(active_branches) == 0:
633 self.throw_parse_error("Empty recipe")
634 return active_branches[0]
635@@ -1009,21 +1198,19 @@ class RecipeParser:
636 return version, deb_version
637
638 def parse_instruction(self, permitted_instructions=None):
639- if self.version < 0.2:
640- options = (MERGE_INSTRUCTION, NEST_INSTRUCTION)
641- options_str = "'%s' or '%s'" % options
642- elif self.version < 0.3:
643- options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, RUN_INSTRUCTION)
644- options_str = "'%s', '%s', or '%s'" % options
645- else:
646- options = (
647- MERGE_INSTRUCTION, NEST_INSTRUCTION, NEST_PART_INSTRUCTION,
648- RUN_INSTRUCTION)
649- options_str = "'%s', '%s', '%s', or '%s'" % options
650+ options = [MERGE_INSTRUCTION, NEST_INSTRUCTION]
651+ if self.version >= 0.2:
652+ options.append(RUN_INSTRUCTION)
653+ if self.version >= 0.3:
654+ options.append(NEST_PART_INSTRUCTION)
655+ if self.version >= 0.5:
656+ options.append(UPSTREAM_INSTRUCTION)
657+ options.sort()
658+ options_str = ", ".join(f"'{o}'" for o in options)
659 instruction = self.peek_to_whitespace()
660 if instruction is None:
661 self.throw_parse_error(
662- "End of line while looking for %s" % options_str)
663+ "End of line while looking for one of: %s" % options_str)
664 if instruction in options:
665 if permitted_instructions is not None:
666 if instruction not in permitted_instructions:
667@@ -1034,7 +1221,7 @@ class RecipeParser:
668 self.take_chars(len(instruction))
669 return instruction
670 self.throw_parse_error(
671- "Expecting %s, got '%s'" % (options_str, instruction))
672+ "Expecting one of %s; got '%s'" % (options_str, instruction))
673
674 def parse_branch_id(self, instruction):
675 self.parse_whitespace("the branch id", instruction=instruction)
676diff --git a/gitbuildrecipe/tests/__init__.py b/gitbuildrecipe/tests/__init__.py
677index d968f5b..e6cfde0 100644
678--- a/gitbuildrecipe/tests/__init__.py
679+++ b/gitbuildrecipe/tests/__init__.py
680@@ -29,7 +29,7 @@ class GitRepository:
681 if not os.path.exists(path):
682 os.makedirs(path)
683 if allow_create and not os.path.exists(os.path.join(path, ".git")):
684- self._git_call("init", "-q")
685+ self._git_call("init", "-b", "main")
686
687 def _git_call(self, *args, **kwargs):
688 subprocess.check_call(["git", "-C", self.path] + list(args), **kwargs)
689@@ -55,7 +55,7 @@ class GitRepository:
690 env = os.environ.copy()
691 if date is not None:
692 env["GIT_COMMITTER_DATE"] = date
693- self._git_call("commit", "-q", "--allow-empty", "-m", message, env=env)
694+ self._git_call("commit", "--allow-empty", "-m", message, env=env)
695 return self.last_revision()
696
697 def branch(self, branch_name, commit):
698@@ -76,7 +76,7 @@ class GitRepository:
699 "log", "-1", "--format=%P", commit).rstrip("\n").split()
700
701 def clone_to(self, new_path):
702- subprocess.check_call(["git", "clone", "-q", self.path, new_path])
703+ subprocess.check_call(["git", "clone", self.path, new_path])
704 return GitRepository(new_path, allow_create=False)
705
706 def build_tree(self, shape):
707diff --git a/gitbuildrecipe/tests/test_deb_version.py b/gitbuildrecipe/tests/test_deb_version.py
708index 662e811..9c005f0 100644
709--- a/gitbuildrecipe/tests/test_deb_version.py
710+++ b/gitbuildrecipe/tests/test_deb_version.py
711@@ -33,6 +33,7 @@ from gitbuildrecipe.recipe import (
712 BaseRecipeBranch,
713 build_tree,
714 RecipeBranch,
715+ RecipeParser,
716 resolve_revisions,
717 )
718 from gitbuildrecipe.tests import (
719@@ -231,7 +232,7 @@ class ResolveRevisionsTests(GitTestCase):
720 def test_substitute_revno(self):
721 source = GitRepository("source")
722 source.commit("one")
723- source._git_call("checkout", "-q", "-b", "branch")
724+ source._git_call("checkout", "-b", "branch")
725 source.build_tree(["a"])
726 source.add(["a"])
727 source.commit("two")
728@@ -240,10 +241,9 @@ class ResolveRevisionsTests(GitTestCase):
729 resolve_revisions(
730 branch1, substitute_branch_vars=substitute_branch_vars)
731 self.assertEqual("foo-3", branch1.deb_version)
732- source._git_call("checkout", "-q", "master")
733+ source._git_call("checkout", "main")
734 source._git_call(
735- "merge", "-q", "--no-ff", "--commit", "-m", "merge branch",
736- "branch")
737+ "merge", "--no-ff", "--commit", "-m", "merge branch", "branch")
738 branch2 = BaseRecipeBranch("source", "foo-{revno}", 0.1)
739 resolve_revisions(
740 branch2, substitute_branch_vars=substitute_branch_vars)
741@@ -254,13 +254,13 @@ class ResolveRevisionsTests(GitTestCase):
742 # the base branch was committed to by instructions such as merge.
743 source = GitRepository("source")
744 source.commit("one")
745- source._git_call("checkout", "-q", "-b", "branch")
746+ source._git_call("checkout", "-b", "branch")
747 source.build_tree(["a"])
748 source.add(["a"])
749 source.commit("two")
750 source.commit("three")
751 branch1 = BaseRecipeBranch(
752- "source", "foo-{revno}+{revno:branch}", 0.1, revspec="master")
753+ "source", "foo-{revno}+{revno:branch}", 0.1, revspec="main")
754 branch2 = RecipeBranch("branch", "source", revspec="branch")
755 branch1.merge_branch(branch2)
756 build_tree(branch1, "target")
757@@ -268,6 +268,23 @@ class ResolveRevisionsTests(GitTestCase):
758 branch1, substitute_branch_vars=substitute_branch_vars)
759 self.assertEqual("foo-1+3", branch1.deb_version)
760
761+ def test_upstream(self):
762+ source = GitRepository("source")
763+ source.commit("one")
764+ source._git_call("checkout", "-b", "debianized")
765+ source.build_tree(["a"])
766+ source.add(["a"])
767+ source.commit("two")
768+ source._git_call("checkout", "main")
769+ recipe = (
770+ "# git-build-recipe format 0.5 deb-version {revno:pristine}-{revno}\n"
771+ "source debianized\n"
772+ "upstream pristine source\n"
773+ )
774+ branch = RecipeParser(recipe).parse()
775+ resolve_revisions(branch, substitute_branch_vars=substitute_branch_vars)
776+ self.assertEqual(branch.deb_version, "1-2")
777+
778
779 class DebUpstreamVariableTests(GitTestCase):
780
781@@ -398,21 +415,21 @@ class RecipeBranchTests(GitTestCase):
782 self.assertEqual("1", base_branch.deb_version)
783 base_branch = BaseRecipeBranch(
784 "base_url", "{revdate}", 0.4, revspec=commit1)
785- base_branch.resolve_commit()
786+ base_branch.fetch()
787 substitute_branch_vars(base_branch, base_branch)
788 self.assertEqual("20150101", base_branch.deb_version)
789 base_branch = BaseRecipeBranch(
790 "base_url", "{revdate}", 0.4, revspec=commit1)
791- base_branch.resolve_commit()
792+ base_branch.fetch()
793 child_branch = RecipeBranch("foo", "base_url", revspec=commit2)
794- child_branch.resolve_commit()
795+ child_branch.fetch()
796 substitute_branch_vars(base_branch, child_branch)
797 self.assertEqual("{revdate}", base_branch.deb_version)
798 substitute_branch_vars(base_branch, child_branch)
799 self.assertEqual("{revdate}", base_branch.deb_version)
800 base_branch = BaseRecipeBranch(
801 "base_url", "{revdate:foo}", 0.4, revspec=commit1)
802- base_branch.resolve_commit()
803+ base_branch.fetch()
804 substitute_branch_vars(base_branch, child_branch)
805 self.assertEqual("20150102", base_branch.deb_version)
806
807diff --git a/gitbuildrecipe/tests/test_functional.py b/gitbuildrecipe/tests/test_functional.py
808index 56fd216..dc796d3 100644
809--- a/gitbuildrecipe/tests/test_functional.py
810+++ b/gitbuildrecipe/tests/test_functional.py
811@@ -195,8 +195,8 @@ class FunctionalBuilderTests(GitTestCase):
812
813 def make_upstream_version(self, source, package_name, version, contents,
814 pristine_tar_format=None):
815- source._git_call("checkout", "-q", "--orphan", "upstream")
816- source._git_call("rm", "-qrf", ".")
817+ source._git_call("checkout", "--orphan", "upstream")
818+ source._git_call("rm", "-rf", ".")
819 source.build_tree_contents(contents)
820 source.add(["."])
821 commit = source.commit("import upstream %s" % version)
822@@ -221,12 +221,11 @@ class FunctionalBuilderTests(GitTestCase):
823 stderr=subprocess.DEVNULL, cwd=source.path)
824 tarfile_sha1 = sha1_file_by_name(tarfile_path)
825 source.tag("upstream/%s" % version, commit)
826- source._git_call("checkout", "-q", "master")
827+ source._git_call("checkout", "main")
828 return tarfile_sha1
829
830- def make_simple_package(self, path):
831- source = GitRepository(path)
832- source.build_tree(["a", "debian/"])
833+ def add_simple_package_files(self, source):
834+ source.build_tree(["debian/"])
835 cl_contents = ("package (0.1-1) unstable; urgency=low\n * foo\n"
836 " -- maint <maint@maint.org> Tue, 04 Aug 2009 "
837 "10:03:10 +0100\n")
838@@ -237,7 +236,13 @@ class FunctionalBuilderTests(GitTestCase):
839 "Package: package\nArchitecture: all\n"),
840 ("debian/changelog", cl_contents)
841 ])
842- source.add(["a", "debian/rules", "debian/control", "debian/changelog"])
843+ source.add(["debian/rules", "debian/control", "debian/changelog"])
844+
845+ def make_simple_package(self, path):
846+ source = GitRepository(path)
847+ source.build_tree(["a"])
848+ source.add(["a"])
849+ self.add_simple_package_files(source)
850 source.commit("one")
851 return source
852
853@@ -579,3 +584,194 @@ class FunctionalBuilderTests(GitTestCase):
854 out, err = self.run_recipe(
855 "--allow-fallback-to-native test.recipe working", retcode=1)
856 self.assertIn("Unknown source format 2.0\n", err)
857+
858+ def test_upstream_command_default_branch(self):
859+ source = GitRepository("source")
860+ # The debian/ directory should be filtered out when generating the orig
861+ # tarball, but not the foo/debian/ directory.
862+ files = ["a", "debian/a", "foo/debian/a"]
863+ source.build_tree(files)
864+ source.add(files)
865+ source.commit("pristine sources")
866+ source._git_call("checkout", "-b", "debianized")
867+ source._git_call("rm", "-rf", "debian")
868+ self.add_simple_package_files(source)
869+ source.commit("debianized sources")
870+ source._git_call("checkout", "main")
871+ with open("package-0.1.recipe", "w") as f:
872+ f.write(
873+ "# git-build-recipe format 0.5 deb-version 0.1-1\n"
874+ "source debianized\n"
875+ "upstream upstream source\n")
876+ self.run_recipe("package-0.1.recipe working")
877+ self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
878+ self.assertThat("working/package_0.1-1.diff.gz", PathExists())
879+ os.mkdir("extracted-orig")
880+ subprocess.check_call(
881+ ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
882+ cwd="extracted-orig")
883+ self.assertThat("extracted-orig/package-0.1/a", PathExists())
884+ self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
885+ self.assertThat("extracted-orig/package-0.1/foo/debian/a", PathExists())
886+
887+ def test_upstream_command_specific_branch(self):
888+ source = GitRepository("source")
889+ # The debian/ directory should be filtered out when generating the orig
890+ # tarball, but not the foo/debian/ directory.
891+ files = ["a", "debian/a", "foo/debian/a"]
892+ source.build_tree(files)
893+ source.add(files)
894+ source.commit("pristine sources")
895+ source._git_call("checkout", "-b", "debianized")
896+ source._git_call("rm", "-rf", "debian")
897+ self.add_simple_package_files(source)
898+ source.commit("debianized sources")
899+ with open("package-0.1.recipe", "w") as f:
900+ f.write(
901+ "# git-build-recipe format 0.5 deb-version 0.1-1\n"
902+ "source\n"
903+ "upstream upstream source main\n")
904+ self.run_recipe("package-0.1.recipe working")
905+ self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
906+ self.assertThat("working/package_0.1-1.diff.gz", PathExists())
907+ os.mkdir("extracted-orig")
908+ subprocess.check_call(
909+ ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
910+ cwd="extracted-orig")
911+ self.assertThat("extracted-orig/package-0.1/a", PathExists())
912+ self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
913+ self.assertThat("extracted-orig/package-0.1/foo/debian/a", PathExists())
914+
915+ def test_upstream_command_bad_repo(self):
916+ source = GitRepository("source")
917+ files = ["a"]
918+ source.build_tree(files)
919+ source.add(files)
920+ self.add_simple_package_files(source)
921+ source.commit("one")
922+ with open("package-0.1.recipe", "w") as f:
923+ f.write(
924+ "# git-build-recipe format 0.5 deb-version 0.1-1\n"
925+ "source\n"
926+ "upstream upstream missingupstream\n")
927+ _, err = self.run_recipe("package-0.1.recipe working", retcode=1)
928+ self.assertIn("missingupstream", err)
929+
930+ def test_upstream_command_bad_rev(self):
931+ source = GitRepository("source")
932+ files = ["a"]
933+ source.build_tree(files)
934+ source.add(files)
935+ self.add_simple_package_files(source)
936+ source.commit("one")
937+ with open("package-0.1.recipe", "w") as f:
938+ f.write(
939+ "# git-build-recipe format 0.5 deb-version 0.1-1\n"
940+ "source\n"
941+ "upstream upstream source missingrev\n")
942+ _, err = self.run_recipe("package-0.1.recipe working", retcode=1)
943+ self.assertIn("missingrev", err)
944+
945+ def test_upstream_command_with_child_clean(self):
946+ """Child branch that does not dirty the working dir or index."""
947+ source = GitRepository("source")
948+ for f in ["a", "b"]:
949+ source.build_tree([f])
950+ source.add([f])
951+ source.commit(f)
952+ self.add_simple_package_files(source)
953+ source.commit("packaging")
954+ source._git_call("checkout", "-b", "tomerge", "HEAD^^")
955+ source.build_tree(["c"])
956+ source.add(["c"])
957+ source.commit("c")
958+ with open("package-0.1.recipe", "w") as f:
959+ f.write(
960+ "# git-build-recipe format 0.5 deb-version 0.1-{revno:upstream}\n"
961+ "source main\n"
962+ "upstream upstream source main\n"
963+ # The merge instruction creates a commit, so the working
964+ # directory and index should be clean. That commit should not
965+ # affect the substitution variables.
966+ " merge merge source tomerge")
967+ self.run_recipe("package-0.1.recipe working")
968+ self.run_recipe("package-0.1.recipe working")
969+ cl = changelog.Changelog(self._get_file_contents(
970+ "working/package-0.1/debian/changelog"))
971+ self.assertEqual("0.1-3", str(cl._blocks[0].version))
972+ self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
973+ self.assertThat("working/package_0.1-3.diff.gz", PathExists())
974+ os.mkdir("extracted-orig")
975+ subprocess.check_call(
976+ ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
977+ cwd="extracted-orig")
978+ self.assertThat("extracted-orig/package-0.1/a", PathExists())
979+ self.assertThat("extracted-orig/package-0.1/b", PathExists())
980+ self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
981+
982+ def test_upstream_command_with_child_dirty(self):
983+ """Child branch that modifies the working dir/index without commit."""
984+ source = GitRepository("source")
985+ source.build_tree(["a"])
986+ source.add(["a"])
987+ self.add_simple_package_files(source)
988+ source.commit("one")
989+ nested = GitRepository("nested")
990+ nested.build_tree(["nestsrc/b"])
991+ nested.add(["nestsrc/b"])
992+ nested.commit("to nest")
993+ with open("package-0.1.recipe", "w") as f:
994+ f.write(
995+ "# git-build-recipe format 0.5 deb-version 0.1-{revno:upstream}\n"
996+ "source main\n"
997+ "upstream upstream source main\n"
998+ # The nest-part instruction modifies the index (and working
999+ # directory to match) without committing, so it will be dirty.
1000+ # A commit will be created to hold the dirty contents; that
1001+ # commit should not affect the substitution variables.
1002+ " nest-part nested nested nestsrc nestdst\n")
1003+ self.run_recipe("package-0.1.recipe working")
1004+ cl = changelog.Changelog(self._get_file_contents(
1005+ "working/package-0.1/debian/changelog"))
1006+ self.assertEqual("0.1-1", str(cl._blocks[0].version))
1007+ self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
1008+ self.assertThat("working/package_0.1-1.diff.gz", PathExists())
1009+ os.mkdir("extracted-orig")
1010+ subprocess.check_call(
1011+ ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
1012+ cwd="extracted-orig")
1013+ self.assertThat("extracted-orig/package-0.1/a", PathExists())
1014+ self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
1015+ self.assertThat("extracted-orig/package-0.1/nestdst/b", PathExists())
1016+
1017+ def test_upstream_command_with_child_and_base_dirty(self):
1018+ """The working dir or index is dirty before the upstream is applied."""
1019+ source = GitRepository("source")
1020+ source.build_tree(["a"])
1021+ source.add(["a"])
1022+ self.add_simple_package_files(source)
1023+ source.commit("one")
1024+ nested = GitRepository("nested")
1025+ nested.build_tree(["nestsrc/b"])
1026+ nested.add(["nestsrc/b"])
1027+ nested.commit("to nest")
1028+ with open("package-0.1.recipe", "w") as f:
1029+ f.write(
1030+ "# git-build-recipe format 0.5 deb-version 0.1-1\n"
1031+ "source main\n"
1032+ "nest-part nested-source nested nestsrc nestdst-source\n"
1033+ "upstream upstream source main\n"
1034+ # The nest-part instruction modifies the index (and working
1035+ # directory to match) without committing, so it will be dirty.
1036+ " nest-part nested-upstream nested nestsrc nestdst-upstream\n")
1037+ self.run_recipe("package-0.1.recipe working")
1038+ self.assertThat("working/package_0.1.orig.tar.gz", PathExists())
1039+ self.assertThat("working/package_0.1-1.diff.gz", PathExists())
1040+ os.mkdir("extracted-orig")
1041+ subprocess.check_call(
1042+ ["tar", "xvfa", "../working/package_0.1.orig.tar.gz"],
1043+ cwd="extracted-orig")
1044+ self.assertThat("extracted-orig/package-0.1/a", PathExists())
1045+ self.assertThat("extracted-orig/package-0.1/debian", Not(PathExists()))
1046+ self.assertThat(
1047+ "extracted-orig/package-0.1/nestdst-upstream/b", PathExists())
1048diff --git a/gitbuildrecipe/tests/test_recipe.py b/gitbuildrecipe/tests/test_recipe.py
1049index bfc6452..cb051f3 100644
1050--- a/gitbuildrecipe/tests/test_recipe.py
1051+++ b/gitbuildrecipe/tests/test_recipe.py
1052@@ -39,6 +39,7 @@ from gitbuildrecipe.recipe import (
1053 SAFE_INSTRUCTIONS,
1054 TargetAlreadyExists,
1055 USAGE,
1056+ UnknownRevisionError,
1057 )
1058 from gitbuildrecipe.tests import (
1059 GitRepository,
1060@@ -144,9 +145,9 @@ class RecipeParserTests(GitTestCase):
1061 "# git-build-recipe format 0.1 deb-version 1 foo")
1062
1063 def tests_rejects_indented_base_branch(self):
1064- self.assertParseError(2, 3, "Not allowed to indent unless after "
1065- "a 'nest' line", self.get_recipe,
1066- self.basic_header + " http://foo.org/")
1067+ self.assertParseError(
1068+ 2, 3, "indentation of base branch not allowed", self.get_recipe,
1069+ self.basic_header + " http://foo.org/")
1070
1071 def tests_rejects_text_after_base_branch(self):
1072 self.assertParseError(2, 19, "Expecting the end of the line, "
1073@@ -154,8 +155,8 @@ class RecipeParserTests(GitTestCase):
1074 self.basic_header + "http://foo.org/ 2 foo")
1075
1076 def tests_rejects_unknown_instruction(self):
1077- self.assertParseError(3, 1, "Expecting 'merge', 'nest', 'nest-part', "
1078- "or 'run', got 'cat'", self.get_recipe,
1079+ self.assertParseError(3, 1, "Expecting one of 'merge', 'nest', 'nest-part', 'run'; "
1080+ "got 'cat'", self.get_recipe,
1081 self.basic_header + "http://foo.org/\n" + "cat")
1082
1083 def test_rejects_merge_no_name(self):
1084@@ -225,15 +226,14 @@ class RecipeParserTests(GitTestCase):
1085 self.basic_header_and_branch + "nest foo url bar 2 baz")
1086
1087 def test_rejects_indent_after_first_branch(self):
1088- self.assertParseError(3, 3, "Not allowed to indent unless after "
1089- "a 'nest' line", self.get_recipe,
1090- self.basic_header_and_branch + " nest foo url bar")
1091+ self.assertParseError(
1092+ 3, 3, "indentation under base branch not allowed", self.get_recipe,
1093+ self.basic_header_and_branch + " nest foo url bar")
1094
1095 def test_rejects_indent_after_merge(self):
1096- self.assertParseError(4, 3, "Not allowed to indent unless after "
1097- "a 'nest' line", self.get_recipe,
1098- self.basic_header_and_branch + "merge foo url\n"
1099- + " nest baz url bar")
1100+ self.assertParseError(
1101+ 4, 3, "indentation under 'merge' not allowed", self.get_recipe,
1102+ self.basic_header_and_branch + "merge foo url\n nest baz url bar")
1103
1104 def test_rejects_tab_indent(self):
1105 self.assertParseError(4, 3, "Indents may not be done by tabs",
1106@@ -466,14 +466,14 @@ class RecipeParserTests(GitTestCase):
1107 def test_old_format_rejects_run(self):
1108 header = ("# git-build-recipe format 0.1 deb-version "
1109 + self.deb_version +"\n")
1110- self.assertParseError(3, 1, "Expecting 'merge' or 'nest', got 'run'"
1111+ self.assertParseError(3, 1, "Expecting one of 'merge', 'nest'; got 'run'"
1112 , self.get_recipe, header + "http://foo.org/\n"
1113 + "run touch test \n")
1114
1115 def test_old_format_rejects_nest_part(self):
1116 header = ("# git-build-recipe format 0.2 deb-version "
1117 + self.deb_version +"\n")
1118- self.assertParseError(3, 1, "Expecting 'merge', 'nest', or 'run', "
1119+ self.assertParseError(3, 1, "Expecting one of 'merge', 'nest', 'run'; "
1120 "got 'nest-part'" , self.get_recipe,
1121 header + "http://foo.org/\nnest-part packaging foo test \n")
1122
1123@@ -522,6 +522,52 @@ class RecipeParserTests(GitTestCase):
1124 " ./foo/../..", NEST_INSTRUCTION, self.get_recipe,
1125 self.basic_header_and_branch + "nest nest url ./foo/../..\n")
1126
1127+ def test_upstream_default_branch(self):
1128+ recipe = (
1129+ "# git-build-recipe format 0.5 deb-version 1.0-1\n"
1130+ "http://exmaple.test/repo.git\n"
1131+ "upstream pristine http://example.test/upstream.git\n"
1132+ )
1133+ base = self.get_recipe(recipe)
1134+ self.assertEqual(str(base), recipe)
1135+
1136+ def test_upstream_specific_branch(self):
1137+ recipe = (
1138+ "# git-build-recipe format 0.5 deb-version 1.0-1\n"
1139+ "http://exmaple.test/repo.git\n"
1140+ "upstream pristine http://example.test/upstream.git somebranch\n"
1141+ )
1142+ base = self.get_recipe(recipe)
1143+ self.assertEqual(str(base), recipe)
1144+
1145+ def test_upstream_with_child(self):
1146+ recipe = (
1147+ "# git-build-recipe format 0.5 deb-version 1.0-1\n"
1148+ "http://exmaple.test/repo.git\n"
1149+ "upstream pristine http://example.test/upstream.git somebranch\n"
1150+ " merge tomerge http://example.test/tomerge.git\n"
1151+ )
1152+ base = self.get_recipe(recipe)
1153+ self.assertEqual(str(base), recipe)
1154+
1155+ def test_error_upstream_as_child(self):
1156+ recipe = (
1157+ "# git-build-recipe format 0.5 deb-version 1.0-1\n"
1158+ "http://exmaple.test/repo.git\n"
1159+ "nest nested http://example.test/nest.git foo\n"
1160+ " upstream pristine http://example.test/upstream.git somebranch\n"
1161+ )
1162+ self.assertRaises(Exception, self.get_recipe, recipe)
1163+
1164+ def test_error_upstream_twice(self):
1165+ recipe = (
1166+ "# git-build-recipe format 0.5 deb-version 1.0-1\n"
1167+ "http://exmaple.test/repo.git\n"
1168+ "upstream pristine http://example.test/upstream.git somebranch\n"
1169+ "upstream other http://example.test/upstream.git somebranch\n"
1170+ )
1171+ self.assertRaises(Exception, self.get_recipe, recipe)
1172+
1173
1174 class BuildTreeTests(GitTestCase):
1175
1176@@ -896,14 +942,12 @@ class BuildTreeTests(GitTestCase):
1177 source.build_tree(["a"])
1178 source.add(["a"])
1179 commit = source.commit("one")
1180- source.branch("source/master", commit)
1181 source.set_head("refs/heads/nonexistent")
1182- base_branch = BaseRecipeBranch(
1183- "source", "1", 0.2, revspec="source/master")
1184+ base_branch = BaseRecipeBranch("source", "1", 0.2, revspec="main")
1185 pull_or_clone(base_branch, "target")
1186 target = GitRepository("target", allow_create=False)
1187 self.assertEqual(commit, target.last_revision())
1188- self.assertEqual(commit, target.rev_parse("source/master"))
1189+ self.assertEqual(commit, target.rev_parse("source/main"))
1190
1191 def test_build_tree_runs_commands(self):
1192 source = GitRepository("source")
1193@@ -917,15 +961,14 @@ class BuildTreeTests(GitTestCase):
1194 self.assertEqual(commit, target.rev_parse("HEAD"))
1195 self.assertEqual(commit, base_branch.commit)
1196
1197- def test_error_on_merge_revspec(self):
1198- # See bug 416950
1199+ def test_error_on_unknown_revision(self):
1200 source = GitRepository("source")
1201 source.commit("one")
1202 base_branch = BaseRecipeBranch("source", "1", 0.2)
1203 merged_branch = RecipeBranch("merged", "source", revspec="debian")
1204 base_branch.merge_branch(merged_branch)
1205- e = self.assertRaises(MergeFailed, build_tree, base_branch, "target")
1206- self.assertIn("ambiguous argument 'debian'", str(e))
1207+ e = self.assertRaises(UnknownRevisionError, build_tree, base_branch, "target")
1208+ self.assertIn("debian", str(e))
1209
1210
1211 class StringifyTests(GitTestCase):

Subscribers

People subscribed via source and target branches