Skip to content
Merged
289 changes: 271 additions & 18 deletions adopt-project.py

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,27 @@ def env_commit(work: Path, message: str) -> None:
A scaffold that fails on a machine with no `user.email` configured fails
for a reason that has nothing to do with the project being scaffolded, so
a fallback identity is supplied rather than assumed.

THE MESSAGE GOES TO GIT AS UTF-8, whatever the locale (#196). Text-mode
stdin encodes with the locale's codec unless Python runs in UTF-8 mode
-- cp1252 on a Windows without `PYTHONUTF8=1`, ASCII under a POSIX `C`
locale with that mode off -- and git keeps the bytes it is given. An
adoption's split message already carried an em dash, which reached git
there as a cp1252 byte, and it names each entry path as the source spells
it: `Übersicht.md` would land as another byte that is not UTF-8, and
`文書.md` would raise `UnicodeEncodeError` after both legs are pushed.
Every caller's message goes the same way: `scaffold-project.py`'s
assembly commit, `scripts/family.py`'s holder commit, and each first
commit `git_init_commit` makes (a scaffolded leg, a seeded one).

`errors="replace"` works BOTH WAYS. Decoding, git's own output cannot
raise. Encoding, a character UTF-8 cannot carry is written as `?`: the
one such character a message can hold is a lone surrogate, the byte of a
name that is not UTF-8 as `Source.tree` keeps it. `adopt-project.py`
refuses one in a path it extracts (`entry_path_problem`), but a `root`
or `drop` entry holding one is listed in the split message, so there the
message names it with a `?` in place of that byte, a path that does not
exist, rather than raising after both legs are pushed.
"""
env = dict(os.environ)
for key, fallback in (("GIT_AUTHOR_NAME", "openRepoShape scaffold"),
Expand All @@ -665,7 +686,8 @@ def env_commit(work: Path, message: str) -> None:
env[key] = fallback
check_program(COMMIT_COMMAND)
proc = subprocess.run(COMMIT_COMMAND, cwd=str(work), input=message,
capture_output=True, text=True, check=False, env=env)
capture_output=True, text=True, encoding="utf-8",
errors="replace", check=False, env=env)
if proc.returncode != 0:
raise CommandFailed(COMMIT_COMMAND, work, proc.returncode,
proc.stderr + proc.stdout)
Expand Down
6 changes: 4 additions & 2 deletions tests/test_adopt_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,9 @@ def test_the_tool_never_touches_the_source_working_tree(adopted):

def test_a_plan_path_that_is_a_git_option_is_refused(source_repo, tmp_path):
"""THE PLAN IS UNTRUSTED INPUT: it is edited between `plan` and `execute`,
by a human or by an AI, and every path in it becomes a git argument."""
by a human or by an AI, and every path in it reaches git. An entry's path
that begins with `-` is `check`'s finding `plan-unsafe-path` (#196), and
`execute` refuses it before any leg exists."""
plan = tmp_path / "plan.yaml"
assert write_plan(source_repo, plan, project=PROJECT).returncode == 0
for path, leg in ANSWERS:
Expand All @@ -328,5 +330,5 @@ def test_a_plan_path_that_is_a_git_option_is_refused(source_repo, tmp_path):
"--local-remote-dir", str(tmp_path / "remotes"),
"--work-dir", str(tmp_path / "work"))
assert result.returncode == 2
assert "unsafe-value" in result.stderr
assert "REFUSED plan-unsafe-path" in result.stderr
assert not (tmp_path / "remotes").exists()
Loading
Loading