Merge ~rhansen/git-build-recipe:upstream into git-build-recipe:master
- Git
- lp:~rhansen/git-build-recipe
- upstream
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson | Pending | ||
Review via email: mp+446340@code.launchpad.net |
Commit message
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/
This branch also has several refactor, cleanup, and minor bugfix commits to prepare for the new feature.
Fixes bug #2024971
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
1 | diff --git a/gitbuildrecipe/deb_util.py b/gitbuildrecipe/deb_util.py |
2 | index 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): |
73 | diff --git a/gitbuildrecipe/main.py b/gitbuildrecipe/main.py |
74 | index 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( |
110 | diff --git a/gitbuildrecipe/recipe.py b/gitbuildrecipe/recipe.py |
111 | index 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) |
676 | diff --git a/gitbuildrecipe/tests/__init__.py b/gitbuildrecipe/tests/__init__.py |
677 | index 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): |
707 | diff --git a/gitbuildrecipe/tests/test_deb_version.py b/gitbuildrecipe/tests/test_deb_version.py |
708 | index 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 | |
807 | diff --git a/gitbuildrecipe/tests/test_functional.py b/gitbuildrecipe/tests/test_functional.py |
808 | index 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()) |
1048 | diff --git a/gitbuildrecipe/tests/test_recipe.py b/gitbuildrecipe/tests/test_recipe.py |
1049 | index 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): |