Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 57 additions & 18 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -10265,7 +10265,7 @@ def _unsafe_path_findings(plan: Plan) -> tuple[list[str], list[str]]:
# executables; and with the rule above, in 2 runs each, files `A` and `a`
# and files `D/A` and `D/a` in one folder: `check` printed the NOTE and
# `plan ok`, and `execute` verified. Other sets of one blob in the root met
# git's `will not add file alias` there, which the #654 rehearsal reports
# git's `will not add file alias` there, which the #654 rehearsal reported
# as `plan-file-reencoded`, in 1 run each for the second review: folder
# twins (`D/x`, `d/x`), alone and beside folders that only merge, in
# `check`; three paths (`AB`, `Ab`, `ab`) in `execute` alone, before any
Expand All @@ -10275,6 +10275,23 @@ def _unsafe_path_findings(plan: Plan) -> tuple[list[str], list[str]]:
# stay this finding, and its remediation, a rename. A file against a
# folder is always the finding: such a disk cannot hold a file and a folder
# under one name, whatever their bytes.
#
# ONE FINDING FOR ONE SET (#1184). The rehearsal still asked about the
# twins this finding names, and git's `will not add file alias` was said
# again, as `plan-file-reencoded`, a finding about bytes. Measured on
# f981468b with git 2.43, on a disk that keeps case under a global
# `core.ignorecase=true`, in 3 of 3 runs each: folder twins (`D/x`, `d/x`)
# of one blob or two, three paths of one blob (`AB`, `Ab`, `ab`) and two
# `.gitattributes` in folder twins, all in the root; and as git stops the
# split's `git add` there, a CRLF `notes.txt` the root keeps beside the
# folder twins went unnamed, which the same source names without that
# setting. Twins git would store as other bytes, CRLF under `text`, were
# named again too, with the setting or without it, and under it in the
# root as one twin of the two. So the rehearsal asks nothing about the
# paths this finding names (`_case_twins_kept`), as it asks nothing of a
# source holding a path git refuses, and asks the rest; the sets the NOTE
# takes are still asked. The rename this finding asks for comes first, and
# the `plan` and `check` after it ask about the renamed files.

#: `check`'s finding, and the code of `execute`'s refusal, for paths the
#: plan keeps in one repository that a disk ignoring case holds as one.
Expand Down Expand Up @@ -10336,6 +10353,29 @@ def _one_blob_twins(named: list[str], kinds: dict, leg: str) -> bool:
and kinds[named[0]][0] != GITLINK_MODE)


def _case_twins_kept(plan: Plan, tree: list,
refused: list[str] | tuple = ()
) -> list[tuple[str, list[list[str]], list[list[str]]]]:
"""`(where, the sets `plan-case-twins` names, the sets left to its NOTE)`
for each repository the split makes, in `TWIN_REPOSITORIES`' order, as
`_case_twin_findings` says them and the #654 rehearsal leaves the first
out (#1184). Only the paths ONE answered entry sends there are asked
(`_answered_legs`), and none git refuses, `refused`."""
held = set(refused)
kinds = {path: (mode, oid) for path, mode, oid, _ in tree}
answered = _answered_legs(plan.entries, [path for path, _, _, _ in tree
if path not in held])
kept = []
for leg, where in TWIN_REPOSITORIES.items():
sets = _case_twin_sets([path for path, (answer, _) in answered.items()
if answer == leg])
alike = [named for named in sets
if _one_blob_twins(named, kinds, leg)]
kept.append((where, [named for named in sets if named not in alike],
alike))
return kept


def _case_twin_findings(plan: Plan, tree: list,
refused: list[str] | tuple = ()
) -> tuple[list[str], list[str], list[str]]:
Expand All @@ -10348,17 +10388,8 @@ def _case_twin_findings(plan: Plan, tree: list,
(`_one_blob_twins`), which the finding leaves out. Only the paths ONE
answered entry sends there are asked (`_answered_legs`), and none git
refuses, `refused`: `plan-path-git-refuses` names those."""
held = set(refused)
kinds = {path: (mode, oid) for path, mode, oid, _ in tree}
answered = _answered_legs(plan.entries, [path for path, _, _, _ in tree
if path not in held])
found, notes = [], []
for leg, where in TWIN_REPOSITORIES.items():
sets = _case_twin_sets([path for path, (answer, _) in answered.items()
if answer == leg])
alike = [named for named in sets
if _one_blob_twins(named, kinds, leg)]
sets = [named for named in sets if named not in alike]
for where, sets, alike in _case_twins_kept(plan, tree, refused):
if alike:
same = [path for named in alike for path in named]
notes.append(
Expand Down Expand Up @@ -13681,9 +13712,12 @@ def _reencoded_attributes_findings(plan: Plan, source: Source,
# and counting the rest: what the root keeps, what goes to a leg or is
# dropped, the pinned copies, and then each of git's refusals. The root's own
# `.gitattributes` is #530's D5(a) question above, and is not asked again; a
# symlink and a gitlink get no rule applied. The files the split renders
# from values it knows only once the legs are pushed (the manifest, the pin
# files, the README) and the mounts' `.gitmodules` are not asked.
# symlink and a gitlink get no rule applied. The case twins
# `plan-case-twins` names are that finding's alone, and asked nothing here
# (#1184): not as files, nor as rules the split's `git add` stores again,
# though their rules still reach the files beside them. The files the split
# renders from values it knows only once the legs are pushed (the manifest,
# the pin files, the README) and the mounts' `.gitmodules` are not asked.

#: `check`'s finding, and the code of `execute`'s refusal, for a file the
#: plan keeps or moves, or a pinned copy, that this machine's git would store
Expand Down Expand Up @@ -14124,16 +14158,21 @@ def _reencoded_files_findings(plan: Plan, source: Source, tree: list,
asked nothing more, and `_Unasked` is raised where git cannot be asked
(#910). `refused` is `_paths_git_refuses`' answer, where `_entry_findings`
has it, so the rehearsal asks no path question of its own; given none,
it asks."""
it asks. The case twins `plan-case-twins` names are asked nothing
(#1184): git's `will not add file alias` for them, under
`core.ignorecase=true`, said that finding again as this one, and
stopped the split's `git add` before it stored any other file."""
kept_rows = _kept_in_root(plan.entries, tree)
kept = {path for path, _, _ in kept_rows}
disk = _KeptTree(kept_rows, False)
top = disk.at(ATTRIBUTES)
own = top[0] if top is not None and top[1] in FILE_MODES else None
at_the_top = top[0] if top is not None else None
order = {path: at for at, (path, _, _, _) in enumerate(tree)}
twins = {path for _, sets, _ in _case_twins_kept(plan, tree, refused or ())
for named in sets for path in named}
files = [(path, mode, oid) for path, mode, oid, _ in tree
if mode in FILE_MODES and path != own]
if mode in FILE_MODES and path != own and path not in twins]
rules = [(path, mode, oid) for path, mode, oid, _ in tree
if mode in (*FILE_MODES, SYMLINK_MODE)
and path.rsplit("/", 1)[-1].casefold() == ATTRIBUTES]
Expand Down Expand Up @@ -14186,8 +14225,8 @@ def _reencoded_files_findings(plan: Plan, source: Source, tree: list,
return [_refused_finding(COMPARE_REFUSED, compare_said)]
return [_moved_finding(taken)] if taken else []
held = {*[row for row in wanted if row[0] in kept],
*[row for row in rules
if row[0] in kept and row[0] != at_the_top]}
*[row for row in rules if row[0] in kept
and row[0] != at_the_top and row[0] not in twins]}
asked.hold(STORED, held)
stored_said = asked.refused(["add", "-u"], STORED)
blobs = {path: oid for path, _, oid in held}
Expand Down
164 changes: 162 additions & 2 deletions tests/test_adopt_check_throwaway_repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@
git's words. So a `.gitattributes` the plan keeps under `.github/`, whose
filter fails, is the split's store's refusal alone, its folder named in
UTF-8 or not (r1253's SHOULD-2).

#1184 followed, measured on f981468b with git 2.43 on a disk that keeps
case, under a global `core.ignorecase=true`. Where `plan-case-twins` named
folder twins (`D/x`, `d/x`), three paths of one blob (`AB`, `Ab`, `ab`) or
two `.gitattributes` in folder twins, all kept in the root, the
rehearsal's `git add` refused them, `will not add file alias`, and `check`
said so again as `plan-file-reencoded`, in 3 of 3 runs each; a CRLF
`notes.txt` kept beside the folder twins went unnamed, as git stopped
there. Now the rehearsal asks nothing about the twins `plan-case-twins`
names, and asks the rest as it does without that setting.
"""

from __future__ import annotations
Expand Down Expand Up @@ -522,8 +532,9 @@ def test_check_names_each_twin_git_refuses_and_each_set_it_holds(tmp_path):
repository, so the global setting is the one each throwaway reads.
`plan` asks about each twin as a path of its own (`planned` answers
each by name). Under a global `core.ignorecase=true` the #654
rehearsal's `git add` refuses the folder twins `D/x` and `d/x` as
`plan-file-reencoded`; this row does not see it, because the rehearsal
rehearsal's `git add` refused the folder twins `D/x` and `d/x` as
`plan-file-reencoded` until #1184, which leaves out the twins
`plan-case-twins` names; this row never saw it, because the rehearsal
asks nothing of a source that holds a path git refuses
(`_reencoded_files_findings`, `if refused: return []`).
`refused_paths` meets every twin: `plan-path-git-refuses` names, in the
Expand Down Expand Up @@ -986,6 +997,155 @@ def test_on_a_disk_that_ignores_case_check_names_the_twins_too(tmp_path):
twins_kept("1 set", ROOT_TWINS, "2 paths", "`A` and `a`")]


# ---------------------------------------------------------------------------
# #1184: one finding for one set, under `core.ignorecase=true`
# ---------------------------------------------------------------------------

#: What git says where its `git add` will not hold a twin beside another.
FILE_ALIAS = "fatal: will not add file alias"

#: `(the twins a source commit gains, each with a blob of its own or all
#: with one, the plan's answers, how `plan-case-twins` names them)`. On
#: f981468b, with git 2.43 under a global `core.ignorecase=true`, `check`
#: named each set again, in 3 of 3 runs, as `plan-file-reencoded`, git
#: saying `will not add file alias` at the rehearsal's `git add`.
ALIASED_TWINS = [
pytest.param(("D/x", "d/x"), True, {"D/": ROOT, "d/": ROOT},
"`D/x` and `d/x`", id="folder-twins"),
pytest.param(("D/x", "d/x"), False, {"D/": ROOT, "d/": ROOT},
"`D/x` and `d/x`", id="folder-twins-of-one-blob"),
pytest.param(("AB", "Ab", "ab"), False,
dict.fromkeys(("AB", "Ab", "ab"), ROOT),
"`AB`, `Ab` and `ab`", id="three-of-one-blob"),
pytest.param(("D/.gitattributes", "d/.gitattributes"), True,
{"D/": ROOT, "d/": ROOT},
"`D/.gitattributes` and `d/.gitattributes`",
id="gitattributes-in-folder-twins"),
]


def ignoring_case(base: Path) -> dict:
"""A global `core.ignorecase=true`, which a throwaway repository on a
disk that keeps case reads, as one on a disk that ignores case reads
the `true` its `git init` writes."""
return config_with(base, "[core]", "\tignorecase = true")


@pytest.mark.parametrize(("twins", "own_blobs", "answers", "named"),
ALIASED_TWINS)
def test_a_set_git_will_not_add_beside_its_twin_is_named_once(
tmp_path, twins, own_blobs, answers, named):
"""#1184: the #654 rehearsal asks nothing about the twins
`plan-case-twins` names, so git's `will not add file alias` for them is
not said again as `plan-file-reencoded`, a finding about bytes."""
source = a_source(tmp_path)
with_paths(source, twins, own_blobs=own_blobs)
result = checked(planned(source, tmp_path, answers),
ignoring_case(tmp_path))
assert "REFUSED" not in result.stderr, result.stderr
assert result.returncode == 1, result.stderr
assert finding_lines(result) == [
twins_kept("1 set", ROOT_TWINS, f"{len(twins)} paths", named)]
assert FILE_ALIAS not in result.stderr, result.stderr


def test_execute_refuses_such_a_set_as_its_one_finding(adopter, monkeypatch,
tmp_path):
"""`execute`'s refusal lists `check`'s findings: the set, once."""
for name, value in ignoring_case(tmp_path).items():
monkeypatch.setenv(name, value)
source = a_source(tmp_path)
with_paths(source, ("D/x", "d/x"), own_blobs=True)
refusal = refused_before_any_leg(
adopter, monkeypatch,
planned(source, tmp_path, {"D/": ROOT, "d/": ROOT}), tmp_path)
assert refusal.code == PLAN_CASE_TWINS
assert refusal.detail.splitlines()[1:] == [
" " + twins_kept("1 set", ROOT_TWINS, "2 paths", "`D/x` and `d/x`")]


def test_a_file_beside_the_twins_is_asked_as_without_the_setting(tmp_path):
"""git stopped the rehearsal's `git add` at the alias, and so the CRLF
`notes.txt` the root keeps beside the twins, under `* text eol=lf`, went
unnamed under `core.ignorecase=true` on f981468b, 3 of 3 runs. Now the
rest is asked, and `check` says what it says without the setting, the
file's `plan-file-reencoded` included."""
source = a_source(tmp_path, files={ROOT_ATTRIBUTES: TEXT_EOL_LF,
NOTES: CRLF})
with_paths(source, ("D/x", "d/x"), own_blobs=True)
plan = planned(source, tmp_path, {"D/": ROOT, "d/": ROOT, NOTES: ROOT})
without, ignored = checked(plan), checked(plan, ignoring_case(tmp_path))
assert codes(without) == [PLAN_CASE_TWINS, REENCODED_FILES], \
without.stderr
assert KEPT_CRLF in finding_lines(without)[1]
assert "`notes.txt`" in finding_lines(without)[1]
assert finding_lines(ignored) == finding_lines(without), ignored.stderr


@pytest.mark.parametrize(("prefix", "answers", "where"), [
pytest.param("", {"A.md": ROOT, "a.md": ROOT}, ROOT_TWINS, id="root"),
pytest.param("src/", {}, CODE_TWINS, id="code-leg"),
])
def test_twins_git_would_store_as_other_bytes_are_the_set_alone(
tmp_path, prefix, answers, where):
"""`A.md` and `a.md`, each CRLF under `* text eol=lf`, and each its own
blob, in the root or in the code leg: on f981468b `check` named them as
`plan-file-reencoded` too, after `plan-case-twins`, the root's by their
names and the leg's as files the plan moves. The rename
`plan-case-twins` asks for comes first, and the `plan` and `check` after
it ask about the renamed files."""
source = a_source(tmp_path, files={
ROOT_ATTRIBUTES: TEXT_EOL_LF, f"{prefix}A.md": CRLF,
f"{prefix}a.md": b"three\r\n"})
plan = planned(source, tmp_path, answers)
for env in (None, ignoring_case(tmp_path)):
result = checked(plan, env)
assert finding_lines(result) == [twins_kept(
"1 set", where, "2 paths",
f"`{prefix}A.md` and `{prefix}a.md`")], result.stderr


def test_the_twins_rules_still_reach_the_files_beside_them(tmp_path):
"""Twin `.gitattributes` files stay checked out, and the files in the
folders that hold twins stay asked: `D/` and `d/` each keep the rule
`*.txt text eol=lf` and a CRLF `.txt`, and `check` names the two files
as `plan-file-reencoded` after `plan-case-twins`, with the setting as
without it."""
rule = b"*.txt text eol=lf\n"
paths = ("D/.gitattributes", "d/.gitattributes", "D/n.txt", "d/m.txt")
source = a_source(tmp_path, files=dict(zip(paths,
(rule, rule, CRLF, CRLF))))
plan = planned(source, tmp_path, dict.fromkeys(paths, ROOT))
without, ignored = checked(plan), checked(plan, ignoring_case(tmp_path))
assert codes(without) == [PLAN_CASE_TWINS, REENCODED_FILES], \
without.stderr
assert "`D/n.txt` and `d/m.txt`" in finding_lines(without)[1]
assert finding_lines(ignored) == finding_lines(without), ignored.stderr


def test_a_set_the_note_takes_is_still_asked(tmp_path):
"""The control: the rehearsal leaves out only the sets the finding
names. `A.md` and `a.md`, one CRLF blob under `* text eol=lf`, are the
NOTE's, and are still `plan-file-reencoded`. Under the setting git's
index holds the set under one name, and the finding names that one, as
it did on f981468b."""
source = a_source(tmp_path, files={ROOT_ATTRIBUTES: TEXT_EOL_LF,
"A.md": CRLF, "a.md": CRLF})
plan = planned(source, tmp_path, {"A.md": ROOT, "a.md": ROOT})
for env in (None, ignoring_case(tmp_path)):
result = checked(plan, env)
assert codes(result) == [REENCODED_FILES], result.stderr
assert [line for line in result.stdout.splitlines()
if line.startswith("NOTE the plan keeps")] == [
one_blob_twins("1 set", ROOT_TWINS, "2 paths",
"`A.md` and `a.md`")]
if env is None:
assert ("the plan keeps 2 files in the assembly root, and this "
"machine's `git add` would store them as other bytes "
"than their blobs once the split's checkout has written "
"them: `A.md` and `a.md`.") in finding_lines(result)[0]


# ---------------------------------------------------------------------------
# L1-92: in the work directory
# ---------------------------------------------------------------------------
Expand Down
Loading