diff --git a/adopt-project.py b/adopt-project.py index 5e12d8d0..b924b41b 100755 --- a/adopt-project.py +++ b/adopt-project.py @@ -67,10 +67,13 @@ #514 no line `execute` writes them into is) (#181), and the source's own submodules kept on one leg with their `.gitmodules`, registered there and never in the root, each - relative url naming the same repository from the leg as from the - source, and each url a leg keeps that names a place on this - machine's disk one where `git ls-remote` reaches a repository (a - url on a network is never asked, and a NOTE says so) (#418), no + relative url naming the same repository from the leg -- its + GitHub remote, or, given `--local-remote-dir`, the bare + repository `execute --local-remote-dir` would create there + (#417) -- as from the source, and each url a leg keeps that + names a place on this machine's disk one where `git ls-remote` + reaches a repository (a url on a network is never asked, and a + NOTE says so) (#418), no symlink kept in the root that the shape would write one of its files, or that file's copy under `shape/`, through where it cannot (one that dangles, loops, or leads out of the @@ -109,8 +112,9 @@ any `leg:` is still null, and on every other finding `check` reports, before it has created a leg repository or pushed anything -- a source submodule's relative url resolved again from the leg remotes it is about to create, which -under `--local-remote-dir` are not the GitHub ones `check` resolves from, and -asked there whether it answers, and +under `--local-remote-dir` are the bare repositories `check` resolves from +only when it is given the same `--local-remote-dir` (#417), and asked there +whether it answers, and the place outside the assembly that a symlink kept at a path the shape writes leads to asked again of the disk, a climb from its own work directory. @@ -5273,20 +5277,22 @@ def _kept_submodule_problems(kept: list, registered: Registrations, "assembly, the leg -- so it must name the same repository from the leg " "as from the source. Commit the submodule's absolute url in the source's " "`.gitmodules` and re-run `plan`, or `drop` the submodule: its " - "registration then stays behind in the leg, inert. `check` resolves it " - "from the GitHub remotes the plan's `org:` names and `execute` from the " - "remotes it is about to create, before it creates any; under " - "`--local-remote-dir` those are bare repositories on disk, where " - "`check`'s `plan ok` settles nothing about it.") + "registration then stays behind in the leg, inert. `check` and " + "`execute` resolve it from the leg remotes `execute` would create: the " + "GitHub repositories the plan's `org:` names, or, under " + "`--local-remote-dir`, the bare repositories there, which `check` asks " + "only when given the same `--local-remote-dir`. `execute` resolves it " + "again before it creates any leg.") class _FromTheLeg(str): """A FINDING about a RELATIVE url as git resolves it from a leg's remote: #190's, and #418's of one that answers nothing there. Under `--local-remote-dir` `execute` resolves it from the bare repositories it - would create, where `check` resolves it from GitHub's, so its refusal - says that the finding is its own (`_as_resolved_here`). A `str`, as - `InTheHistory` is, so that every reader takes it as the line it is.""" + would create, which `check` resolves it from only when given the same + `--local-remote-dir` (#417), so its refusal says which `check` prints + it (`_as_resolved_here`). A `str`, as `InTheHistory` is, so that every + reader takes it as the line it is.""" class _Resolution: @@ -5466,13 +5472,15 @@ def _relative_url_findings(plan: Plan, source: Source, tree: list, THE LEG'S REMOTE IS THE ONE `execute` WOULD CREATE: under `local_remote_dir`, the bare repository there; otherwise the GitHub - repository the plan's `org:` and leg name give. `check` takes no - `--local-remote-dir`, so it decides on GitHub's, and `execute` decides - again on the remotes it is about to create, before it creates any. They - are read only when there is a relative url to resolve, and the NOTE - says, where `check` resolved them all alike, that it was GitHub's it - asked: a plan it passes can still be refused by `execute - --local-remote-dir`.""" + repository the plan's `org:` and leg name give. `check` takes the same + `--local-remote-dir` (#417): given the one `execute` is given, it + decides on the same remotes, and without one on GitHub's; `execute` + decides again on the remotes it is about to create, before it creates + any. They are read only when there is a relative url to resolve, and + the NOTE says, where `check` resolved them all alike from GitHub's, + that it was GitHub's it asked: a plan it passes there can still be + refused by `execute --local-remote-dir`, as `check` given the same + directory finds.""" leg, kept = _kept_relative_urls(plan.entries, tree, registered) if not kept: return [], [], {} @@ -5506,7 +5514,8 @@ def _relative_url_findings(plan: Plan, source: Source, tree: list, f"as from the source: {'; '.join(alike)}. `execute " "--local-remote-dir` resolves it again from the bare repository it " "would create instead, and refuses there, before it creates any leg, " - "a url that names another repository."], from_the_leg + "a url that names another repository; `check`, given the same " + "`--local-remote-dir`, resolves it there too."], from_the_leg # --------------------------------------------------------------------------- @@ -5557,8 +5566,9 @@ def _relative_url_findings(plan: Plan, source: Source, tree: list, "`drop` the submodule: its registration then stays behind in the leg, " "inert. `check` and `execute` ask, with `git ls-remote`, only a url " "that names a repository on this machine's disk; a relative one is " - "resolved from the leg's remote, which for `check` is on GitHub and for " - "`execute --local-remote-dir` is the bare repository it would create.") + "resolved from the leg's remote, which is on GitHub, or, under " + "`--local-remote-dir`, the bare repository `execute` would create " + "there.") #: What `_answers` holds `git ls-remote` to: the disk, and no transport #: that could reach a network. ON_THIS_DISK_ONLY = {"GIT_ALLOW_PROTOCOL": "file"} @@ -5637,8 +5647,7 @@ def _first_words(failed: subprocess.CompletedProcess) -> str: def _unanswered_url_findings(plan: Plan, tree: list, registered: Registrations | str, - from_the_leg: dict[str, tuple[str, str]], - local_remote_dir: Path | None + from_the_leg: dict[str, tuple[str, str]] ) -> tuple[list[str], list[str]]: """`(findings, notes)` for the url of each submodule the plan keeps in the leg its `.gitmodules` goes to (`_kept_urls`), as that leg resolves @@ -5647,7 +5656,9 @@ def _unanswered_url_findings(plan: Plan, tree: list, tree's order, and the one NOTE `check` prints about those it did not ask. `from_the_leg` is `_relative_url_findings`' own: an absolute url is its own resolution, and a relative one not in it is that function's - finding and is not asked.""" + finding and is not asked. The NOTE is the same whichever remotes a + relative url was resolved from: a url on a network is not asked under + `--local-remote-dir` either, and `check` given one names it (#417).""" leg, kept = _kept_urls(plan.entries, tree, registered) candidates = [] for path, entry, url in kept: @@ -5667,7 +5678,7 @@ def _unanswered_url_findings(plan: Plan, tree: list, elif said is not None: found.append(_answers_nothing(path, entry, leg, url, remote, there, said)) - if not unasked or local_remote_dir is not None: + if not unasked: return found, [] return found, [ "NOTE `check` did not ask whether the url of each submodule the " @@ -5738,7 +5749,7 @@ def _submodule_findings(plan: Plan, source: Source, tree: list, moved, notes, from_the_leg = _relative_url_findings( plan, source, tree, registered, local_remote_dir) unanswered, unasked = _unanswered_url_findings( - plan, tree, registered, from_the_leg, local_remote_dir) + plan, tree, registered, from_the_leg) return ((found, [SUBMODULE_REMEDIATION] if found else []), (moved + unanswered, ([RELATIVE_URL_REMEDIATION] if moved else []) @@ -6649,10 +6660,12 @@ def _entry_findings(plan: Plan, source: Source, alone, to be refused again for the rest once that was fixed. Composed once, the two commands cannot differ about a plan -- but in the two places something outside the plan decides it. `local_remote_dir` is - `execute`'s, and a relative submodule url is resolved from the leg - remotes it names, where `check` has none and resolves from GitHub's - (#190). And two things about a symlink AT a shape path are asked of the - disk by each command when it runs (#183): whether anything is at the + the run's, and a relative submodule url is resolved from the leg + remotes it names, and from GitHub's without one (#190): `check` takes + the same `--local-remote-dir` as `execute` (#417), so the two differ + there only where they are given different ones. And two things about a + symlink AT a shape path are asked of the disk by each command when it + runs (#183): whether anything is at the place outside the assembly it leads to, a climb above the root from its own work directory; and whether `git` there checks it out as a symlink at all, or as a plain file the shape's copy goes beside. The second is asked @@ -6819,9 +6832,10 @@ def _entry_findings(plan: Plan, source: Source, # cannot disagree about it, as #168 has them agree about every other finding. # A branch in the way is `plan-split-branch-taken` in both. # -# WHERE `check` ASKS. It does not know whether `--local-remote-dir` will be -# passed, so it asks the two places it can see without the network, and each -# finding says which run it is about: the source's own branches, and, where +# WHERE `check` ASKS. It does not know whether `execute` will be given +# `--local-remote-dir`, and its own (#417) is not read here, so it asks the +# two places it can see without the network, and each finding says which +# run it is about: the source's own branches, and, where # the source's `origin` is the assembly repository, what that remote held # when the source last fetched it -- for an `org/repo` source, which # `Source.open` clones, when it was cloned. A source branch in the way is @@ -8676,7 +8690,14 @@ def cmd_check(args) -> int: # a split branch the source keeps, and the branch is named first. taken, taken_remediation = _check_split_branch(plan, source) findings.extend(taken) - entry_findings, remediation, notes = _entry_findings(plan, source) + # The directory `execute --local-remote-dir` will be given: each relative + # submodule url is resolved from the bare repositories that run would + # create there, as it resolves them, and from GitHub's without one, and + # the topics line below is that run's (#417). Read as `work_dir` is, so + # a Namespace made without it has none. + local_remote_dir = getattr(args, "local_remote_dir", None) + entry_findings, remediation, notes = _entry_findings(plan, source, + local_remote_dir) findings.extend(entry_findings) remediation = taken_remediation + remediation @@ -8698,7 +8719,8 @@ def cmd_check(args) -> int: findings.append(f"FINDING {exc.code}: {exc.detail}") _print_what_will_happen(plan, source, names, - naming.topic_for(plan.project_id)) + naming.topic_for(plan.project_id), + local_remote_dir is not None) seeded = seeded_legs(assigned_paths(plan.entries)) for line in seeding_warnings(seeded, plan.allowed_empty_legs()): @@ -9810,7 +9832,8 @@ def _remote_urls(org: str, names: dict, local_remote_dir: Path | None, source: Source) -> dict: """The url of each of the three repositories, by role: what `execute` pushes to, and what a relative submodule url kept in a leg is resolved - from (#190), which `check` asks with no `local_remote_dir`.""" + from (#190), which `check` asks with the `local_remote_dir` it is given, + or none (#417).""" if local_remote_dir is None: return {role: f"https://github.com/{org}/{name}.git" for role, name in names.items()} @@ -9923,11 +9946,12 @@ def _refuse_what_check_finds(plan: Plan, source: Source, BUT FROM THE REMOTES THIS RUN WOULD CREATE (#190). A relative submodule url kept in a leg is resolved from that leg's remote, and under - `local_remote_dir` that is a bare repository `check` cannot know of: such - a finding is this run's own, and the refusal says so. A url `check` finds - from GitHub's remotes may name the same repository from these, and one it - passes may not; and one these resolve to a repository on this disk is - asked whether it answers (#418), which a url on GitHub never is. + `local_remote_dir` that is a bare repository `check` resolves it from + only when given the same `--local-remote-dir` (#417), and the refusal + says so. A url `check` finds from GitHub's remotes may name the same + repository from these, and one it passes there may not; and one these + resolve to a repository on this disk is asked whether it answers + (#418), which a url on GitHub never is. """ findings, remediation, _ = _entry_findings(plan, source, local_remote_dir) if not findings: @@ -9954,17 +9978,16 @@ def _refuse_what_check_finds(plan: Plan, source: Source, def _as_resolved_here(findings: list[str], local_remote_dir: Path | None) -> str: - """What the refusal's first line adds when a finding in it is not one - `check` prints: a relative url resolved from `local_remote_dir`'s - remotes, where `check` resolves it from GitHub's -- #190's finding, or - #418's of one that answers nothing there (`_FromTheLeg`). Nothing - otherwise.""" + """What the refusal's first line adds when a finding in it is one + `check` prints only when given this run's `--local-remote-dir` (#417): + a relative url resolved from `local_remote_dir`'s remotes -- #190's + finding, or #418's of one that answers nothing there (`_FromTheLeg`). + Nothing otherwise.""" if local_remote_dir is None or not any( isinstance(finding, _FromTheLeg) for finding in findings): return "" - return (", but with each relative submodule url resolved from the leg " - "remotes this run would create under --local-remote-dir, where " - "`check` resolves it from GitHub's") + return (" given the same --local-remote-dir, with each relative submodule " + "url resolved from the leg remotes this run would create there") def _refuse_unconsented_seeding(args, plan: Plan, seeded: list) -> None: @@ -10779,7 +10802,8 @@ def cmd_execute(args) -> int: # tool's own. # # THE CLONE URL IS `execute`'S OWN. Under `--local-remote-dir` it is the -# source's path, and `check` has no such flag to know it will be written. +# source's path, and `check` does not ask about it: its own +# `--local-remote-dir` (#417) is not read for it. # Against GitHub it is `https://github.com//.git`, and the path is # written nowhere. So `execute` refuses an unwritable one itself, right # after `_repository_urls`, and `plan` and `check` still pass a source @@ -12337,6 +12361,14 @@ def main(argv: list[str] | None = None) -> int: check.add_argument("--plan", required=True) check.add_argument("--source", default=None, help="override the source the plan names") + check.add_argument("--local-remote-dir", type=Path, default=None, + help="the directory `execute --local-remote-dir` will " + "be given (the TEST path): each relative " + "submodule url the plan keeps in a leg is " + "resolved from the bare repository `execute` " + "would create there, as that run resolves it, " + "and not from GitHub's. Nothing is created " + "there.") check.add_argument( "--work-dir", type=Path, default=None, help=_work_dir_help( diff --git a/docs/cli.md b/docs/cli.md index 13b0ed94..6211f0d4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -537,23 +537,31 @@ options: ``` usage: adopt-project.py check [-h] --plan PLAN [--source SOURCE] + [--local-remote-dir LOCAL_REMOTE_DIR] [--work-dir WORK_DIR] options: - -h, --help show this help message and exit + -h, --help show this help message and exit --plan PLAN - --source SOURCE override the source the plan names - --work-dir WORK_DIR where `check` works: an `org/repo` --source, or the - plan's `source.repository` where its recorded clone is - gone, is cloned there. Give the one `execute` will be - given. Default: a new directory in the system temp - folder, removed when the run ends if it holds no file. - A --work-dir you give is made where it is missing, and - nothing in it is removed. One this user cannot write in - is a finding, `adopt-work-dir-unwritable`, and is - refused under that code where the source would be - cloned into it or cannot be opened, as one that cannot - be made is. + --source SOURCE override the source the plan names + --local-remote-dir LOCAL_REMOTE_DIR + the directory `execute --local-remote-dir` will be + given (the TEST path): each relative submodule url the + plan keeps in a leg is resolved from the bare + repository `execute` would create there, as that run + resolves it, and not from GitHub's. Nothing is created + there. + --work-dir WORK_DIR where `check` works: an `org/repo` --source, or the + plan's `source.repository` where its recorded clone is + gone, is cloned there. Give the one `execute` will be + given. Default: a new directory in the system temp + folder, removed when the run ends if it holds no file. + A --work-dir you give is made where it is missing, and + nothing in it is removed. One this user cannot write + in is a finding, `adopt-work-dir-unwritable`, and is + refused under that code where the source would be + cloned into it or cannot be opened, as one that cannot + be made is. ``` #### `adopt-project.py execute` diff --git a/tests/test_adopt_relative_submodule_url.py b/tests/test_adopt_relative_submodule_url.py index 703cb9a7..b959af3f 100644 --- a/tests/test_adopt_relative_submodule_url.py +++ b/tests/test_adopt_relative_submodule_url.py @@ -8,7 +8,9 @@ --recurse-submodules` then failed wherever the dependency did not sit beside the leg. `check` now resolves it from both places, as git does, and finds it where the two differ; `execute` resolves it again from the remotes it would -create and refuses it before it creates any. +create and refuses it before it creates any. Given the `--local-remote-dir` +`execute` is given, `check` resolves from the same bare repositories, and +the two give one verdict (#417). No network: a GitHub url here is only ever a string git resolves against. Every repository is local, and the end-to-end tests clone from disk. @@ -83,7 +85,8 @@ #: The source's remote beside them, as `git remote add origin #: ../remotes/Thing.git` writes it from `/Thing`: RELATIVE. RELATIVE_ORIGIN = f"../{REMOTES}/{THING_REMOTE}" -#: The code leg's GitHub remote, as `check` resolves from it. +#: The code leg's GitHub remote, as `check` with no `--local-remote-dir` +#: resolves from it. CODE_LEG = "https://github.com/{org}/" + THING + "-code.git" #: The code leg's bare remote, as `execute --local-remote-dir` names it. CODE_BARE = f"{THING}-code.git" @@ -183,8 +186,12 @@ def in_another_org(plan: Path, org: str) -> Path: return plan -def check(plan: Path): - return run_script(ADOPT, "check", "--plan", str(plan)) +def check(plan: Path, remotes: Path | None = None): + """`check` of `plan`; given `remotes`, `check --local-remote-dir + ` (#417), which resolves each relative url from the bare + repositories `execute --local-remote-dir ` would create.""" + given = () if remotes is None else ("--local-remote-dir", str(remotes)) + return run_script(ADOPT, "check", "--plan", str(plan), *given) def found(stderr: str) -> list[tuple[str, str]]: @@ -193,6 +200,14 @@ def found(stderr: str) -> list[tuple[str, str]]: for line in stderr.splitlines() if line.startswith("FINDING ")] +def finding_lines(text: str) -> list[str]: + """Each `FINDING` line of `text`, as `check` prints it: a refusal of + `execute`'s lists `check`'s lines under its first, each indented by + two spaces.""" + return [line.removeprefix(" ") for line in text.splitlines() + if line.removeprefix(" ").startswith("FINDING ")] + + def notes(stdout: str) -> list[str]: """The NOTE lines `check` prints about the relative urls it resolved.""" return [line for line in stdout.splitlines() @@ -227,7 +242,9 @@ def alike(org: str, *resolved: tuple[str, str, str]) -> str: for url, path, to in resolved) + ". `execute --local-remote-dir` resolves it again from the bare " "repository it would create instead, and refuses there, before " - "it creates any leg, a url that names another repository.") + "it creates any leg, a url that names another repository; " + "`check`, given the same `--local-remote-dir`, resolves it there " + "too.") #: `from the leg, at ``, to ``; ` in a finding. @@ -487,20 +504,23 @@ def refusal_of(adopter, plan: Path, tmp_path: Path, ], ids=["https", "scp", "scp-other-case", "dotdot-dotdot"]) def test_execute_resolves_again_from_the_remotes_it_would_create( adopter, tmp_path, monkeypatch, origin, url, org): - """(c) `check` cannot know where `execute` will make the legs, so it - resolves from GitHub's, where these plans name one repository from both - and pass, and says so. `execute --local-remote-dir` resolves again from - the bare repositories it would make there, where they do not, and - refuses before it makes any -- saying the finding is its own and not one - `check` printed, and that `check`'s `plan ok` settles nothing here.""" + """(c) `check` with no `--local-remote-dir` resolves from GitHub's + remotes, where these plans name one repository from both and pass, and + says so. `execute --local-remote-dir` resolves again from the bare + repositories it would make there, where they do not, and refuses before + it makes any -- saying that `check` prints the finding given the same + `--local-remote-dir`, which it does, line for line, making nothing + (#417).""" source = source_with(tmp_path, url, origin) plan = planned(source, tmp_path, org) checked = check(plan) passes(checked) assert len(notes(checked.stdout)) == 1 + remotes = tmp_path / REMOTES + given = check(plan, remotes) + assert given.returncode == 1, given.stderr + given.stdout # Every refusal here comes before the tool would be used. monkeypatch.setattr(adopter, "_require_filter_repo", lambda: None) - remotes = tmp_path / REMOTES with pytest.raises(adopter.Refusal) as caught: adopter.cmd_execute(Namespace( plan=str(plan), source=None, local_remote_dir=remotes, @@ -509,17 +529,18 @@ def test_execute_resolves_again_from_the_remotes_it_would_create( assert refusal.code == RELATIVE first, finding = refusal.detail.splitlines() assert first == ( - f"1 finding(s) in {plan}, as `check` prints them, but with each " - "relative submodule url resolved from the leg remotes this run would " - "create under --local-remote-dir, where `check` resolves it from " - "GitHub's:") + f"1 finding(s) in {plan}, as `check` prints them given the same " + "--local-remote-dir, with each relative submodule url resolved from " + "the leg remotes this run would create there:") + assert finding_lines(given.stderr) == finding_lines(refusal.detail) leg = adopter._spelled(str(remotes.resolve() / CODE_BARE)) assert finding.startswith(f" FINDING {RELATIVE}: the submodule " f"`{DEPENDENCY}` (entry `{UPSTREAM}`)"), finding assert f", and from the code leg, at {leg}, to " in finding assert refusal.remediation.endswith( "\n" + adopter.RELATIVE_URL_REMEDIATION) - assert "`check`'s `plan ok` settles nothing" in refusal.remediation + assert ("which `check` asks only when given the same " + "`--local-remote-dir`") in refusal.remediation assert "no leg repository was created" in refusal.remediation assert not remotes.exists() @@ -594,16 +615,23 @@ def test_a_relative_origin_resolves_from_the_source_working_tree( which name it too, so nothing is refused, as on main. With the legs anywhere else it names another repository and is refused, and the finding names the directory git opens and says why. The same remote - spelled absolute resolves to an absolute path, said as before.""" + spelled absolute resolves to an absolute path, said as before. `check` + given either directory says what `execute` given it says (#417).""" remotes = tmp_path / REMOTES origin = RELATIVE_ORIGIN if relative else str(remotes / THING_REMOTE) source = source_with(tmp_path, SIBLING, origin, remotes / "dependency") plan = planned(source, tmp_path, its_org(origin)) assert refusal_of(adopter, plan, tmp_path, remotes) is None + given = check(plan, remotes) + passes(given) + # It checks for the run that skips the topics, and says so as that run. + assert " topics skipped for local remotes" in given.stdout.splitlines() elsewhere = tmp_path / "elsewhere" refusal = refusal_of(adopter, plan, tmp_path, elsewhere) assert refusal is not None and refusal.code == RELATIVE + assert finding_lines(check(plan, elsewhere).stderr) \ + == finding_lines(refusal.detail) [_, finding] = refusal.detail.splitlines() if relative: which = f"{ITS_ORIGIN}, relative to its working tree" @@ -647,11 +675,12 @@ def clone_the_split(source: Path, clone: Path): def test_execute_refuses_a_relative_url_its_leg_resolves_elsewhere( tmp_path, url, leg, there): """A source with no remote of its own, as the re-verifier built it: - `check` finds it from GitHub's remotes, and `execute --local-remote-dir` - from the bare ones it would make. On main both passed, `execute` said - `adoption verified`, and the assembly's recursive clone exited 1. Now - `execute` exits 2 before the remotes directory exists, and the source - is untouched -- from either leg, and for `..\\` as for `../`.""" + `check` with no `--local-remote-dir` finds it from GitHub's remotes, and + `execute --local-remote-dir` from the bare ones it would make. On main + both passed, `execute` said `adoption verified`, and the assembly's + recursive clone exited 1. Now `execute` exits 2 before the remotes + directory exists, and the source is untouched -- from either leg, and + for `..\\` as for `../`.""" source = source_with(tmp_path, url) head = git("rev-parse", "HEAD", cwd=source).stdout plan = planned(source, tmp_path, answers={GITMODULES: leg, UPSTREAM: leg}) @@ -711,14 +740,14 @@ def test_the_remediation_adopts_and_clones(tmp_path): def test_a_relative_url_its_leg_resolves_alike_adopts_and_clones( tmp_path, url, origin, legs): """(a) end to end. The source's `origin` and the legs are local, in a - layout where its relative url names the dependency from both: `check`, - which resolves from GitHub's remotes, finds it, and `execute`, which - resolves from the ones it makes, adopts and verifies, and the assembly's - `git clone --recurse-submodules` and `bootstrap.py` both exit 0 with the - dependency checked out in the leg, whose `.gitmodules` still says what - the source committed. An `origin` beginning `..` is written as it - stands, relative to the source's working tree; any other is a path in - the remotes directory.""" + layout where its relative url names the dependency from both: `check` + with no `--local-remote-dir`, which resolves from GitHub's remotes, finds + it, and `execute`, which resolves from the ones it makes, adopts and + verifies, and the assembly's `git clone --recurse-submodules` and + `bootstrap.py` both exit 0 with the dependency checked out in the leg, + whose `.gitmodules` still says what the source committed. An `origin` + beginning `..` is written as it stands, relative to the source's working + tree; any other is a path in the remotes directory.""" remotes = tmp_path / REMOTES dependency = remotes / "dependency" origin = origin if origin.startswith("..") else str(remotes / origin) @@ -738,3 +767,48 @@ def test_a_relative_url_its_leg_resolves_alike_adopts_and_clones( bootstrapped = run_script(clone / "scripts/bootstrap.py", cwd=clone) assert bootstrapped.returncode == 0, (bootstrapped.stderr + bootstrapped.stdout) + + +# --- `check --local-remote-dir`, on the remotes `execute` would create ------ + + +@needs_filter_repo +@pytest.mark.parametrize("local", [True, False], + ids=["A-local-origin", "B-github-origin"]) +def test_check_and_execute_given_one_local_remote_dir_agree(tmp_path, local): + """#417's two layouts, end to end as the issue ran them: `../dependency` + both times, and the dependency beside the legs `execute + --local-remote-dir` makes. A: the source's `origin` is there too, so the + url names one repository from the source and from a leg there. B: the + `origin` is on GitHub, and the url names another repository from a leg + there. `check` with no `--local-remote-dir` resolves from GitHub's + remotes and says the opposite in each: a finding over an adoption that + verifies in A, and `plan ok` over one `execute` refuses in B. Given the + directory `execute` is given, `check` says what `execute` says: in A it + passes, `execute` verifies and the assembly's recursive clone checks the + dependency out in the leg; in B it finds what `execute` refuses, line + for line, and neither makes a leg.""" + remotes = tmp_path / REMOTES + origin = str(remotes / THING_REMOTE) if local else HTTPS_ORIGIN + source = source_with(tmp_path, SIBLING, origin, remotes / "dependency") + plan = planned(source, tmp_path, its_org(origin) if local else ORG) + on_github = check(plan) + given = check(plan, remotes) + result = execute(plan, tmp_path / WORK, remotes) + assert finding_lines(given.stderr) == finding_lines(result.stderr) + assert [code for code, _ in found(on_github.stderr)] \ + == ([RELATIVE] if local else []) + if not local: + assert [code for code, _ in found(given.stderr)] == [RELATIVE] + assert (given.returncode, result.returncode) == (1, 2), \ + given.stderr + result.stderr + assert not (remotes / CODE_BARE).exists() + return + passes(given) + assert result.returncode == 0, result.stderr + result.stdout + assert VERIFIED in result.stdout + clone = tmp_path / "clone" + cloned = clone_the_split(source, clone) + assert cloned.returncode == 0, cloned.stderr + assert (clone / "code" / DEPENDENCY / VALUE).read_text() \ + == DEPENDENCY_TREE[VALUE] diff --git a/tests/test_adopt_submodule_url_answers.py b/tests/test_adopt_submodule_url_answers.py index 9db3e831..4440f75a 100644 --- a/tests/test_adopt_submodule_url_answers.py +++ b/tests/test_adopt_submodule_url_answers.py @@ -54,11 +54,11 @@ THERE_RE = re.compile(r", to `((?:[^`\\]|\\.)*)`, the repository it names " r"from the source, ") #: What `execute --local-remote-dir`'s refusal adds to its first line when a -#: finding in it resolved a relative url from the remotes it would create. +#: finding in it resolved a relative url from the remotes it would create, +#: as `check` given the same directory does (#417). FROM_THE_LOCAL_LEGS = ( - ", but with each relative submodule url resolved from the leg remotes " - "this run would create under --local-remote-dir, where `check` resolves " - "it from GitHub's:") + " given the same --local-remote-dir, with each relative submodule url " + "resolved from the leg remotes this run would create there:") #: Both platforms' answer for a url that names a drive. ON_WINDOWS = os.name == "nt" #: Where the tests' `insteadOf` sends a path, and the url the guard is @@ -178,6 +178,10 @@ def test_execute_refuses_a_relative_url_that_names_nothing_from_either_side( assert finding.endswith(CLONE_FAILS), finding assert refusal.remediation.endswith( "\n" + adopter.URL_ANSWERS_NOTHING_REMEDIATION) + # Where a relative url is resolved from is said of the run, not of the + # command: `check` given this directory resolves it here too (#417). + assert ("which is on GitHub, or, under `--local-remote-dir`, the bare " + "repository `execute` would create there.") in refusal.remediation assert "no leg repository was created" in refusal.remediation elsewhere = refusal_of(adopter, plan, tmp_path, tmp_path / "elsewhere") @@ -251,6 +255,24 @@ def test_check_notes_each_url_it_does_not_ask(adopter, tmp_path, monkeypatch): assert asked == [] +def test_check_given_local_remotes_still_notes_a_url_on_a_network(tmp_path): + """`check --local-remote-dir` resolves a relative url from the bare + repositories `execute` would create there, which are on this disk, and + asks it there (#417); a url on a network it never asks, as without the + flag, and it names that url in the NOTE all the same.""" + remotes = tmp_path / REMOTES + origin = str(remotes / THING_REMOTE) + second = "https://github.com/testorg/second.git" + plan = planned(source_with(tmp_path, SIBLING, origin, + remotes / "dependency", second=second), + tmp_path, its_org(origin), + answers={**BOTH_IN_CODE, EXTRA: "code"}) + result = check(plan, remotes) + passes(result) + assert unasked_notes(result.stdout) == [unasked_note( + f"`{second}` of `extra/second`")] + + def test_a_path_this_machine_rewrites_to_a_network_is_not_asked( tmp_path, monkeypatch): """A `url..insteadOf` can send a path on this disk to a network,