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