Skip to content

bootstrap.py: the merge --ff-only, --continue and --abort lines it prints word a leg's path and an upstream as a shell reads them back, so a byte that is not UTF-8 is a printf and no longer escape text that names no ref (#932) - #944

Merged
brettheap merged 1 commit into
mainfrom
fix/932-pasteable-upstream
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #932 (comment)

The assembly root's pinned bootstrap.py printed git -C <leg> merge --ff-only <upstream> (move_back_to_pin) and an operation's git -C <leg> <verb> --continue and --abort (_say_in_progress) through spelled(shlex.quote(...)). spelled writes a byte that is not UTF-8 as #391's \udcXX text, and a control character, a U+2028 and a bidirectional control as the escape Python spells it, and sh reads that text as the characters it is. For an upstream origin/a + byte 0xff + b, the line named no ref.

The helper and its form

_paste_word(name) words ONE path or ref, local to bootstrap.py (it imports nothing new from repo_shape.py: a pinned copy must not rely on a newer shape copy, #370, #610).

  • Where spelled has nothing to escape and this stdout can write every character, it is spelled(shlex.quote(name)) as it was. An ordinary name, a name with a space or an apostrophe and a printable name that is not ASCII (origin/café) are byte for byte what they were, so every line that was pasteable is unchanged.
  • Any other name is "$(printf -- '<octal escapes>')", the bytes os.fsencode gives the name, each byte outside [A-Za-z0-9_./:@+=,-] a three-digit octal escape. Why this form: dash has no $'...' before 0.5.13 (Ubuntu's 0.5.12 reads $'a\377b' as $a\377b), and the printf of dash, bash and zsh reads \NNN; -- keeps a name that begins with - from being read as an option; % and the backslash and the apostrophe are escapes, so the format has no conversion and no quote to end; the double quotes keep the result one word. The word is ASCII, so it is the same on any stdout. It also covers a C1 control, a U+2028 and a bidirectional control in a name, and a name the stdout cannot write (PYTHONIOENCODING=ascii and origin/café).
  • Two names have no such word and keep spelled's text, as they were: one that ends in a line feed ($(...) strips it, so the word would name another path) and one whose bytes do not read back as the name (os.fsdecode). Git refuses a control character in a ref's name, so both can only be a leg's path, and a path with a final line feed is only a hand-made manifest's.

The lines it changes

The 0xff probe

An upstream origin/a + byte 0xff + b, main past the pin with no commit of its own, the line pasted into /bin/sh -c in the root:

before: git -C spec merge --ff-only 'origin/a\udcffb'
        merge: origin/a\udcffb - not something we can merge     (exit 1)
after:  git -C spec merge --ff-only "$(printf -- 'origin/a\377b')"
        main is at the upstream's commit                         (exit 0)

Tests

tests/test_bootstrap_pasteable_lines.py, 45 tests. Each integration test runs the command from the printed line, as printed, through /bin/sh -c from the root (one of them also through dash, bash and zsh) and asks where the branch, or the merge, is afterwards, with the stdout lines asserted whole.

  • the move-back line for an upstream with the byte 0xff, a C1 control, a U+2028 and a bidirectional control;
  • the same for a leg mounted at a path with the byte 0xff, a leading - and a % (.gitmodules holds the byte, and project.yaml its \udcff escape);
  • _say_in_progress's --continue and --abort, each run, in a leg at a path with the byte 0xff;
  • unchanged byte for byte: ASCII, an apostrophe, origin/café, a CJK name, a space, a no-break space, the empty name, =x;
  • PYTHONIOENCODING=ascii with origin/café: the printf word;
  • the helper's words for each byte class and a digit after an escape (exact, and read back through /bin/sh), the two names with no word, the legacy Windows code page, a stdout that is None, and no new import from repo_shape.

Two existing tests pinned the old line and now pin the new one: test_bootstrap_after_bump.py::test_the_branch_and_its_upstream_are_spelled_in_the_move_back and the byte row and the ascii test of test_bootstrap_non_utf8_branch.py. The prose in the same line (the upstream as a person reads it) is still spelled.

Fail on base (50178305's bootstrap.py): 41 of the 45 fail (collected 45, no skip). The four that pass by design are the printable-upstream line's three rows (ASCII, an apostrophe, café: the line must be what it was) and the no-new-import test.

Pass at head: 45 passed (inside the runs below).

Runs

Load on this host made a full run impractical (a load of 85 to 270 on 32 cores), so the runs are scoped as the coordinator asked. One pytest process at a time, run9s.sh with SHARDS=1, the real gh unreachable.

git files result
2.43.0 (the host), at this head every tests/test_bootstrap*.py (11 files, the new one included), test_pinned_copy_text.py, test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py 702 passed, 3 skipped (test_bootstrap_ref_store.py's reftable rows: this git makes no reftable repository)
2.47.1 (a local build), at this head the new file, test_bootstrap_non_utf8_branch.py, test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py 223 passed
2.43.0, a scratch merge of this head with origin/main 64464d31 the new file, test_bootstrap_non_utf8_branch.py, test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py, test_pinned_copy_text.py 313 passed

Before the commit, on 2.43.0 with the tree uncommitted, the new file, eight other test_bootstrap* files, test_leg_path_yaml_scalars.py and test_non_utf8_name_helpers.py passed as well (the last two are the only ones of the list below that ran).

Left to CI's tests job, which runs the whole suite on the head (59 files that mention bootstrap.py): every test_adopt_* that does (13), test_bom_reads, the four test_bump_leg*, test_doctor_* (5), test_family* (8), test_leg_path_yaml_scalars, test_legs_readers_not_a_list, test_load_yaml_refusals, test_non_utf8_name_helpers, test_park_resume_targets, test_push_branch_refspec, test_scaffold_* (5), test_setup_project_py, test_setup_sh, test_shape_doctor, test_siblings_family_name, test_template_text_placeholders, test_update_shape* (7), test_validate_family_branch_step, test_validate_pins_lockstep_lines, test_validate_workflow_gitmodules_urls, test_validator_child_encoding and test_windows_paths. None of them reads a line this change moves: the lines are asserted in test_bootstrap_after_bump.py, test_bootstrap_non_utf8_branch.py and the new file, all run.

Mutants

Each applied alone to this head's bootstrap.py, with the new file run (-x); every one is killed, none survives.

mutant killed by
the helper returns spelled(shlex.quote(name)) always test_the_printf_word_prints_the_bytes_of_the_name
the octal escape off by one (byte - 1) test_the_printf_word_prints_the_bytes_of_the_name
the escape unpadded (:o, a digit after it is read into it) the same test, the row with %
a printable name takes the printf branch test_a_name_with_nothing_to_escape_is_the_word_it_was
_prints_as_it_is is str.isprintable the same, the no-break-space row
_prints_as_it_is is always true test_the_printf_word_prints_the_bytes_of_the_name
no -- after printf the same, the first row (the exact word)
no double quotes round the substitution the same, the first row
% is a bare byte the same, the % row
/ is not a bare byte test_the_move_back_command_is_pasteable_whatever_the_upstream_is_named[byte-0xff]
the final-line-feed guard dropped test_a_name_with_no_printf_word_keeps_the_text_it_had
the os.fsdecode round-trip guard dropped test_a_name_the_codec_would_not_read_back_keeps_the_text_it_had (it survived until that test was added)
the stdout's encoding not asked test_a_stdout_that_cannot_write_the_name_gets_the_printf_word
the helper dropped from _say_in_progress test_the_commands_that_end_an_operation_are_pasteable_whatever_the_path[--continue]
the helper dropped from move_back_to_pin's path test_the_move_back_command_is_pasteable_whatever_the_legs_path_is_named[byte-0xff]
the helper dropped from move_back_to_pin's upstream test_the_move_back_command_is_pasteable_whatever_the_upstream_is_named[byte-0xff]

Merge checks

git merge-tree --write-tree of this head, clean (exit 0, one tree id and no conflict line) against origin/main (64464d31, which moved from 50178305 while this was written; it touches the family root's bootstrap.py and three tests), against fix/430-d-assembly-root-guards and against fix/896-casefold-branch-f (their local heads df1bb186 and 80353dd1; neither branch is on origin yet, so these are an early look to be repeated when they are pushed).

The overlap with #896

#896's head adds _say_twin, whose way out is command = (f"git -C {spelled(shlex.quote(path))} switch " f"{spelled(shlex.quote(branch))}"). A branch can hold the byte 0xff past _argument_fault, so it has this defect. Whichever of the two lands second should make it command = f"git -C {_paste_word(path)} switch {_paste_word(branch)}". The helper is defined above _say_in_progress, so the order of the functions does not matter, and the two diffs touch different hunks.

A pinned copy: every project's bootstrap.py takes an upstream-changed row, and re-pins it.

Closes #932

🤖 Generated with Claude Code

Summary by Sourcery

Make bootstrap's printed merge and in-progress operation commands reliably pasteable for unusual leg paths and upstream names.

New Features:

  • Add pasteable shell-word generation for leg paths and upstream refs, using byte-preserving printf forms when normal quoting cannot round-trip the name.

Bug Fixes:

  • Ensure printed merge, continue, and abort commands remain executable for non-UTF-8 names, control characters, and other escaped paths or refs.

Enhancements:

  • Preserve existing command output for names that are safely printable while handling stdout encoding limitations and names without a safe command-substitution form.

Documentation:

  • Document the byte-preserving shell-word behavior and its limitations in the assembly-root bootstrap script.

Tests:

  • Add comprehensive coverage that executes printed commands through multiple POSIX shells and validates paths, refs, encoding edge cases, and helper boundaries.

… it prints word a leg's path and an upstream as a shell reads them back, so a byte that is not UTF-8 is a `printf` and no longer escape text that names no ref (#932)

step (a) printed `git -C <leg> merge --ff-only <upstream>`, and an operation's `--continue` and `--abort`, through `spelled(shlex.quote(...))`. `spelled` writes a byte that is not UTF-8 as #391's `\udcXX` text, and a control character, a line separator and a bidirectional control as the escape Python spells it, so for an upstream `origin/a<0xff>b` the line named no ref and git answered `not something we can merge`.

`_paste_word` words one path or ref: exactly what it was where `spelled` has nothing to escape and the stdout can write the name, and otherwise `"$(printf -- '<octal escapes>')"`, which dash, bash and zsh turn back into the bytes. A pinned copy, so it imports nothing new from `repo_shape.py`.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T16:34Z) @codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T16:38:01.033632Z ec2017c Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Makes bootstrap commands printed for manual pasting reliably round-trip leg paths and upstream refs through POSIX shells, especially for non-UTF-8 and control bytes, while preserving compatible output for already-safe names and adding comprehensive exact-output and integration coverage.

Sequence diagram for pasteable bootstrap command generation

sequenceDiagram
    participant Bootstrap
    participant PasteWord as _paste_word
    participant Stdout
    participant Shell
    participant Git

    Bootstrap->>PasteWord: _paste_word(path)
    PasteWord->>Stdout: _prints_as_it_is(name)
    alt name prints and reads back unchanged
        PasteWord-->>Bootstrap: spelled(shlex.quote(name))
    else name needs byte-safe representation
        PasteWord-->>Bootstrap: "$(printf -- '<octal bytes>')"
    end
    Bootstrap->>Stdout: print merge or --continue/--abort command
    Stdout-->>Shell: pasted command
    Shell->>Git: execute with original path/ref bytes
    Git-->>Shell: operate on intended leg or upstream
Loading

Flow diagram for choosing a pasteable shell word

flowchart TD
    A["_paste_word(name)"] --> B{spelled text writes and reads back unchanged?}
    B -->|Yes| C[Return spelled shlex quote]
    B -->|No| D{os.fsencode round trip succeeds and name does not end in LF?}
    D -->|No| E[Keep spelled text]
    D -->|Yes| F[Encode non-bare bytes as three-digit octal escapes]
    F --> G[Return double-quoted printf command substitution]
    C --> H[Use in printed bootstrap command]
    E --> H
    G --> H
Loading

File-Level Changes

Change Details Files
Adds a local helper that emits shell words which preserve path and ref bytes when pasted, while retaining the existing spelling for names that are already safe or cannot be represented losslessly.
  • Detects whether the existing quoted spelling is unchanged and writable to stdout.
  • Encodes unsafe names with an ASCII printf -- command substitution using fixed-width octal escapes.
  • Guards against trailing-newline stripping and filesystem codec round-trip failures.
  • Keeps the helper self-contained so pinned bootstrap copies require no new repo_shape.py imports.
templates/assembly-root/scripts/bootstrap.py
Uses the new paste-safe word formatting for commands printed during move-back and in-progress operations.
  • Formats the leg path and upstream in merge --ff-only instructions.
  • Formats the leg path in --continue and --abort instructions.
  • Updates module and function documentation to describe the shell-readable command forms while leaving human-readable names spelled as before.
templates/assembly-root/scripts/bootstrap.py
Expands regression coverage to validate exact output and execution of printed commands across unsafe names and shell environments.
  • Adds integration tests for non-UTF-8 bytes, controls, Unicode separators, bidi controls, special path characters, and in-progress commands.
  • Verifies unchanged output for ordinary, quoted, printable Unicode, empty, and whitespace-containing names.
  • Tests stdout encoding failures, filesystem codec edge cases, trailing newlines, shell compatibility, octal escaping, and pinned-copy import constraints.
  • Updates existing expected command lines for the new printf representation.
tests/test_bootstrap_pasteable_lines.py
tests/test_bootstrap_after_bump.py
tests/test_bootstrap_non_utf8_branch.py

Assessment against linked issues

Issue Objective Addressed Explanation
#932 Ensure the assembly root’s merge --ff-only command prints an upstream name containing non-UTF-8 bytes in a shell-readable form, so pasting the line names the actual Git ref (or clearly says it cannot be pasted). ✅

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: ec2017cf0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #944 (#932) at ec2017cf, for lane openRepoShape-5: READY

No finding is a MUST. The printf word is exact under dash, bash and zsh for every byte from 0x01 to 0xff in seven positions. Printable names print byte for byte what 50178305 printed. Every requested run passes, and every merge is clean. There are three SHOULDs (one in this PR's own test, two that need follow-ups or landing care) and five NITs.

Findings

SHOULD 1. A word that begins with = cannot be pasted into zsh, and the new test treats that as intended.

SHOULD 2 (landing). The clean merge with #896 keeps the old form in a new line.

SHOULD 3 (follow-up). The NOT INITIALIZED paste line is still neither quoted nor worded.

NIT 1 (check 4, the names with no word): keeping today's text is right, but the body should say so against the Goal.

NIT 2. _prints_as_it_is (:774-782) asks the stdout's codec, but git is handed os.fsencode's bytes.

  • Under a latin-1 or cp1252 stdout, origin/café gets the quoted word and is printed as the byte e9. The ref is c3 a9.
  • 50178305 does the same, so it is not a regression.
  • The docstring's "a paste of it names the same" is not true there. Either compare the stdout codec with sys.getfilesystemencoding() for non-ASCII text, or soften the docstring.

NIT 3. On POSIX the round-trip guard (:833) is stricter than it needs to be.

  • A manifest path "sp\udcc3\udca9c" encodes to c3 a9, the directory spéc.
  • The guard returns 'sp\udcc3\udca9c', yet "$(printf -- 'sp\303\251c')" reads back exactly those bytes.
  • The docstring (:811-813) names only the lone surrogate and the Windows code page. Only a hand-escaped manifest reaches this.

NIT 4. Two mutants survive. All 124 tests in the new file and the two changed test files pass with each.

  • R6: except (LookupError, UnicodeEncodeError) reduced to except UnicodeEncodeError. No test uses a stdout with an unknown codec.
  • R8: the bytes taken from name.encode("utf-8", "surrogateescape") instead of os.fsencode. Under a latin-1 filesystem codec (an in-process shim), the head gives "$(printf -- 'caf\351\011')" and R8 falls back to the old text 'café\t'. The guard stops R8 from naming another path, but the word is lost.

NIT 5. Stale wording.

Checked and true

  1. The word is exact. I built 2,169 names: each byte 0x01 to 0xff alone, inside a name, leading, trailing, after -, after = and before 0xff; plus about 160 special names (%, %s, a backslash, ', ", $, $(id), a backtick, -x, -n, =x, spaces, a tab, ~, glob characters, !, U+0080 to U+009F, U+2028, the bidi controls, U+FEFF), each also with 0xff before and after. Every word came from the head's real _paste_word.

    • Words made: 2,165 (1,545 printf, 620 quoted). The other 4 names have no word, and all of them end in LF. Every printf word is ASCII.
    • dash and bash: /bin/sh, /bin/dash and /bin/bash, under LC_ALL=C and C.UTF-8: 0 mismatches, and each word is one argument ($# is 1).
    • zsh: 0 mismatches over the 2,089 words that do not begin with =, in both locales. The leading = failures are SHOULD 1.
    • The brief's literal form sh -c 'printf %s "$1"' x <word>, for 600 short names: 0 mismatches under sh and bash.
    • Versions: dash 0.5.12-6ubuntu5, bash 5.2.21, zsh 5.9. A digit after an escape works (\0111 reads as tab then 1), and -- works in all three shells.
  2. Unchanged output. I compared 30 printable names (ASCII, café, CJK, an apostrophe, U+00A0, a ZWJ, an emoji, RTL scripts and others) across 22 line sets each:

    • _say_in_progress: 7 rows × 2 marker places;
    • move_back_to_pin: 4 outcomes × 2 placements.

    All 660 are byte-identical to 50178305 on a UTF-8 stdout. The diff touches only the module docstring, PRINTF_BARE, the two new functions and those two callers. argparse reads only the docstring's first line, which is unchanged.

  3. The stdout rule. Under UTF-8, printable names keep the quoted word. Under ascii, café, 日本 and U+00A0 get the printf word, which is ASCII. With sys.stdout None the rule treats the stdout as UTF-8. With an unknown codec every name gets the printf word, which is harmless.

  4. Old-form words. None is left in head outside _paste_word.

  5. Pinned copy. The repo_shape import set is unchanged (Refusal, escape_unencodable_stdout, find_repo_root, git_out, load_yaml, recorded_gitlink, spelled), and test_pinned_copy_text.py passed in the 2.43 run.

  6. The three changed tests. Each change was needed, because base no longer prints what they expected. Each still tests what its name says, apart from the docstring in NIT 5.

  7. Mutants: 12 run, 10 killed, the 2 survivors are in NIT 4.

    Mutant Killed by
    w932's M2b, octal unpadded test_the_printf_word_prints_the_bytes_of_the_name, the 50% row
    w932's M9, no round-trip guard the codec test
    w932's M4c, upstream without the helper the upstream test, byte-0xff row
    R1, hex escapes for bytes above 0x7f the printf-word test
    R2, the apostrophe left bare the printf-word test
    R3, the stdout check inverted the printf-word test
    R4, --abort keeps the old word the in-progress test
    R5, the LF guard anywhere in the name the printf-word test, the a\nb row
    R7, no guard for a stdout that is None the stdout-None test
    R9, no -- for the in-progress path the in-progress test (the line is asserted whole)
  8. Runs at ec2017cf through run9.sh, one process at a time:

    • git 2.43.0: the new file, test_bootstrap_non_utf8_branch.py, test_bootstrap_after_bump.py, test_pinned_copy_text.py and the trio: 359 passed, 0 skipped, in 245 s.
    • git 2.47.1: the new file: 45 passed in 18.4 s.
    • Base check: the new file against 50178305's bootstrap.py: 41 failed and 4 passed, the four w932 named.
  9. Merges (git merge-tree --write-tree):

    • with origin/main 64464d31: tree d175c8f9, clean;
    • fix/430-d-* and fix/896-* are not on origin (ls-remote showed only main and fix/932-pasteable-upstream). As an early look at the local heads: df1bb186 gives 268922b2 and 80353dd1 gives e2b6802d, both clean.
  10. Sonar-style checks:

    • Cognitive complexity, by hand: _prints_as_it_is 2, _paste_word 5, _say_in_progress 0, move_back_to_pin 4 (unchanged); all under 15.
    • Duplication: no run of 4 or more identical non-comment lines repeats in the new file.
    • Asserts: none in the new file joins conditions with and, and none was added elsewhere.
    • Line length: the 91 lines added to bootstrap.py are at most 79 columns. The 4 longer lines (7, 1289, 1379, 1538) were already there at base.

The commit is authored Brett Heap <service@opensoft.dev>, and body-guard.py passes on the body draft.

Could not verify

  • macOS's /bin/sh (bash 3.2) and its printf --.
  • Git Bash on Windows: how MSYS converts the printf bytes, and a path with a lone surrogate.
  • git 2.55 (CI's version).
  • Sonar's own analyser; the counts above are by hand.
  • The 59 files that mention bootstrap.py that w932 left to CI. I ran only the brief's set.
  • The merges with 430-d and 896 once those branches are pushed.

Checklist candidates

  1. Quote a leading = in _paste_word (zsh), and in the NOT INITIALIZED line.
  2. Word the NOT INITIALIZED line with shlex.quote plus _paste_word (slice D or a follow-up).
  3. Switch bootstrap.py: on a disk that folds case, a leg on Main whose tracking branch main is behind the pin is moved under its own HEAD, and bootstrap says it is on main now over an index that undoes the pin #896's _say_twin to _paste_word when the second of the two PRs lands.
  4. Compare the stdout codec with the filesystem codec in _prints_as_it_is.
  5. Loosen the POSIX round-trip guard.
  6. A trailing-LF leg path: a "cannot be pasted" note, or have validate-manifest.py refuse a control character in a leg path (today it refuses none).
  7. Unit tests that kill R6 and R8.

brettheap pushed a commit that referenced this pull request Oct 18, 2026
…ame the commits "a branch spelled like main" holds, and the docstrings say what making a branch hides, so no line over an unborn twin is false (#896)

Part 1 of the fix round on #948, from rv896's review (issuecomment-6252823490),
which found the change READY with four NITs that need nothing from #944.

NIT 1. `TWIN_RISKS["make"]` said making the branch could hide "the commits
Main holds"; an unborn twin holds none. It now says "the commits a branch
spelled like main holds", which is true of a packed twin, an unborn twin and a
third spelling beside either.

NIT 2. The missing-branch refusal told the person to check out the pin and
never printed it. Its first line now reads `branch main does not exist (the
pin is <sha12>), and the checkout is on Main, ...`. No refusal #896 did not
add is changed. The packed-twin test takes the sha from the printed line to
run the way out.

NIT 3. Two `_folded_twin` rows, `refs/notes/Main` and `refs/tags/xMain`
against `main`, both false. Each has 11 characters before `Main`, as
`refs/heads/` does, so a mutant that drops the `refs/heads/` guard and keeps
the 11-character slice answered true for both and survived the 24 tests; the
two rows kill it.

NIT 4. The module docstring says what each act could do: a branch MOVED as
`main` could be the ref HEAD stands on, with the index left at the old commit,
and one MADE as `main` could hide the commits the twin holds behind the new
ref. `_say_twin`'s docstring no longer says "packed or not": a loose `Main` on
a disk that folds case makes `refs/heads/main` resolve, so the missing-branch
path never runs for it.

The `switch` line and the docstrings that compare it with `move_back_to_pin`
are not touched: part 2 switches them to #944's `_paste_word` once #944 lands.
Cognitive complexity is unchanged: place_at_pin 6, fast_forward_to_pin 15,
create_at_pin 13, _folded_twin 3, _say_twin 1. (The first commit's message says
14, 6 and 12; those were a hand count that missed `why or []`.)

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap
brettheap marked this pull request as ready for review October 18, 2026 17:59
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 1 hour by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 450279c into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/932-pasteable-upstream branch October 18, 2026 18:01
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…h and the branch with `_paste_word`, so a branch that is not UTF-8 pastes into a shell as the bytes it is (#896)

Part 2 of the fix round on #948, from rv896's SHOULD 1, now that #944 (#932)
has landed as 450279c and `_paste_word` is on main. The first commit's merge
of origin/main is the commit before this one.

`_say_twin` built its command with `spelled(shlex.quote(...))`, the form #932
replaced. For a branch named with a byte that is not UTF-8 that printed
`git -C spec switch 'a\udcffb'`, and `/bin/sh` reads that word as the eight
characters it holds, so the pasted command named no branch. The command is now
`git -C {_paste_word(path)} switch {_paste_word(branch)}`, as #944's hand-over
gave it: a name that prints as it is stays `shlex.quote`d, and one with a byte
that is not UTF-8 is a `printf` that prints the bytes. `_say_twin`'s docstring
and `test_the_way_out_is_quoted_for_a_shell_...` say so, where they said the
command was printed as `move_back_to_pin` printed it before #944.

Tests: one end-to-end case with a tracking branch `a<0xff>b` and HEAD on its
twin `A<0xff>b` (T7, `--branch`): bootstrap leaves the leg, the printed
command is `git -C spec switch "$(printf -- 'a\377b')"`, run through
`/bin/sh -c` as printed it puts HEAD on `refs/heads/a<0xff>b` byte for byte,
and the next run fast-forwards it. Five unit rows read the two words of the
command back through `/bin/sh` and compare them with the bytes of the names
(an ordinary pair, a path with the byte, a branch with the byte, both with a
leading dash, a path with a percent). Dropping `_paste_word` from the path,
from the branch, or restoring the old form each fails 3, 3 and 5 of the 32.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…racking branch only in case (`Main` for `main`) is left where it is, so on a disk that folds case `branch -f main` no longer moves the ref HEAD stands on (#896) (#948)

**Head 4541529** (fix round 1: part 1 `48222b5e`, then the merge `60df34fc` of origin/main `450279ca` (#944), then part 2), on the first head `80353dd1`, on origin/main `50178305` (the v1.1.1 release commit). origin/main is now `450279ca`: `git merge-tree --write-tree` of this head against it is clean, tree `aabc059b`. One file of `templates/` changes: `templates/assembly-root/scripts/bootstrap.py`. One new test file.

**Fix round 1, part 2 (rv896's SHOULD 1, one commit after the merge of main, `45415296`).** #944 (#932) landed as `450279ca`, so `_paste_word` is on main. `_say_twin`'s command is now `git -C {_paste_word(path)} switch {_paste_word(branch)}`: for a branch named with a byte that is not UTF-8 the old `spelled(shlex.quote(...))` form printed `switch 'a\udcffb'`, which `/bin/sh` reads as the eight characters it holds, so the pasted command named no branch. `_say_twin`'s docstring and the docstring of `test_the_way_out_is_quoted_for_a_shell_...` now say `_paste_word`, where they said the line was printed as `move_back_to_pin` printed its own before #944. A non-UTF-8 twin test runs the printed `switch` through `/bin/sh -c` as printed, and five unit rows read each word back through `/bin/sh`.

**Fix round 1, part 1 (rv896's NITs 1 to 4, one commit, `48222b5e`).** rv896 found this change READY (issuecomment-6252823490). Part 1 takes the four NITs that need nothing from #944: the `make` risk now says "the commits a branch spelled like main holds" (an unborn twin holds none); the missing-branch lines print the pin's sha, `branch main does not exist (the pin is <sha12>)`; two `_folded_twin` rows, `refs/notes/Main` and `refs/tags/xMain`, kill the one surviving mutant; the module and `_say_twin` docstrings say what MAKING a branch hides. The `switch` line was left for part 2, below, which switches it to #944's `_paste_word` now that #944 has landed.

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

Where HEAD's ref and `refs/heads/<branch>` are equal under `str.casefold()` but differ in bytes, bootstrap leaves the leg. No ref moves and none is made. It says where the leg is, why it is left, and the way out.

- `_folded_twin(current, branch)` says whether HEAD's ref is such a twin. A detached HEAD (`None`) and a ref outside `refs/heads/` are never twins. It compares the names after `refs/heads/`, so a name held as surrogate escapes (#704) raises nothing.
- `_say_twin(label, path, branch, current, state, act)` prints the three lines. Each of the three path functions asks the helper first, before any move, and grows by one `if`. They take the leg's `path` for the command, as `move_back_to_pin` does; `checkout_tracking_branch` passes it at its three call sites only (its `(sub / ".git").exists()` lines are untouched).
- The words hold on a disk that keeps case too, where `Main` and `main` are two branches. They never say the two are one ref: they say that a disk that folds case (macOS, Windows) reads them as one branch, and what that could do.

Before (all three said the leg was on `Main`, which keeps its commits (a branch with no commit yet, for the unborn twin below), and moved or made `main`):

    T7 (main behind the pin)   branch main fast-forwarded <old> -> <pin>, the pin; ...
                               the leg was on Main, which keeps its commits; it is on main now
    P9 (main at the pin)       on main at <pin> (branch tip == pin)
                               the leg was on Main, which keeps its commits; it is on main now
    missing branch             branch main created at <pin> - at the pin
                               the leg was on Main, which keeps its commits; it is on main now

Now (spaces and quoting as printed; the leg is `spec`):

    T7:
      [spec] spec: branch main tip <old> is behind the PIN <pin>, and the checkout is on Main, which differs from main only in case
              a disk that folds case (macOS, Windows) reads Main and main as one branch, so moving main could move the ref HEAD stands on and leave the index at the old commit; the leg is left as it is
              run `git -C spec switch main`, which moves no branch, and then run bootstrap again.
    P9:
      [spec] spec: branch main is at the pin <pin>, and the checkout is on Main, which differs from main only in case
              a disk that folds case (macOS, Windows) reads Main and main as one branch, so the leg is already on main there, and bootstrap would say it left a branch that keeps its commits; the leg is left as it is
              run `git -C spec switch main`, which moves no branch, and then run bootstrap again.
    missing branch:
      [spec] spec: branch main does not exist (the pin is <pin>), and the checkout is on Main, which differs from main only in case
              a disk that folds case (macOS, Windows) reads Main and main as one branch, so making main could hide the commits a branch spelled like main holds behind the new ref; the leg is left as it is
              not moving the checkout off it. Keep any commit made there on a branch whose name differs from main by more than case, check out the pin in the leg, and run bootstrap again.

T7 and P9 give `git -C <leg> switch <branch>`, the path and the branch each one `_paste_word` (#932), as `move_back_to_pin`'s path and upstream are: a name that prints as it is stays `shlex.quote`d, and one with a byte that is not UTF-8 is a `printf` that prints the bytes. On a disk that folds case it re-spells HEAD and moves no file, and the next run takes T7's on-branch `merge --ff-only` path or P9's no-op checkout. Where the two are branches, it puts the leg on the real `main`, and `Main` keeps its commits. Every other state prints what it printed: HEAD on `refs/heads/main` itself, on `feature`, on `mains`, detached, on a ref outside `refs/heads/`.

CI is Linux only, so a disk that folds case is emulated, as the issue's own probe did: a symlink `.git/refs/heads/Main -> main`, which git's files backend reads through, and HEAD on `refs/heads/Main`. T7 is the issue's state: `main` loose and behind the pin, the pin's commit adding `NEW-CONTRACT.md` and editing `README.md`.

At `50178305`:

    before: HEAD -> refs/heads/Main | Main = main = 536671ad3925 | pin = f27b6a92a433 | status ''
    bootstrap: branch main fast-forwarded 536671ad3925 -> f27b6a92a433, the pin; origin/main is 536671ad3925, which the pin deliberately does NOT follow
               the leg was on Main, which keeps its commits; it is on main now
    after:  HEAD -> refs/heads/main | HEAD = main = f27b6a92a433 | status 'D  NEW-CONTRACT.md;M  README.md'

At this head (a second scaffold, so other shas):

    before: HEAD -> refs/heads/Main | Main = main = bf6b7be3d5fa | pin = 19051708a6c9 | status ''
    bootstrap: the three T7 lines above
    after:  HEAD -> refs/heads/Main | Main = main = bf6b7be3d5fa | status ''

P9 on the base printed `on main at <pin>` and the `the leg was on Main` line; at this head it prints the three P9 lines, and HEAD, `main` and the status are as before. The way out, `git -C spec switch main` run as printed, puts HEAD on `refs/heads/main`, moves no branch, and the next run prints `fast-forwarded` (T7) or `on main at <pin> (branch tip == pin)` (P9), with `git status --short` empty.

At `50178305`, git 2.43.0 and 2.47.1, the same result. `checkout -q --no-overwrite-ignore -b main <pin>`, from HEAD on `Main`, `main` not there:

- **A packed twin with commits** (`git pack-refs --all`, so no loose `Main`, no `main`). Before: HEAD on `Main`, HEAD = `Main` = the twin's commit, `main` missing, status clean. The checkout exits 0 and writes the loose file `main`. On a disk that folds case that file is the file `Main` reads, so `Main` now names the pin, and the twin's commit is in no branch and no ref (`git branch -a --contains`, `git for-each-ref --contains`: nothing); only HEAD's reflog lists it. **This is the harm.** The emulation makes the link after the checkout: a link made before a packed `Main` is a DANGLING link, git reads `Main` as missing through it (it falls back to packed-refs only when `lstat` says ENOENT, and a folding disk would say that), HEAD reads as unborn, and the staged `g.txt` is left. That before-state is not a folding disk's, so the probe has no link before the checkout, and the committed packed test has none at all: a real `Main` with a commit, packed, is the state of a disk that keeps case, and the lines are the same on both.
- **An unborn twin** (HEAD on `Main` with no commit, the dangling link `Main -> main`, which a folding disk reads as one missing ref). The checkout moves HEAD to `main` at the pin and the index is consistent. Nothing is hidden: the twin holds no commit. **It is harmless.** It is refused all the same: one rule, one helper, and no state of HEAD to branch on; `head is not None` in the missing-branch path would exempt it, if the coordinator prefers that.

The words follow the packed probe: `switch main` is no way out of a leg with no `main` on a disk that keeps case, so the line says to keep the commits on a branch whose name differs from `main` by more than case, check out the pin (whose sha the first line prints), and run bootstrap again. The risk says "the commits a branch spelled like main holds", which is true of a packed twin, of an unborn twin (it holds none, so nothing is hidden) and of a third spelling beside either (rv896's probe: an unborn `Main` beside a packed `MAIN` hides that commit the same way). The committed packed test runs the way out: `git branch kept-from-Main`, `git checkout --detach <the printed sha>`, bootstrap prints `branch main created at <pin> - at the pin`, `kept-from-Main` and `Main` both hold the commit.

`tests/test_bootstrap_casefold_twin.py`, 32 tests. The leg set-up is `test_bootstrap_after_bump.py`'s fixtures, `bumped` and `fast_forwarded`, and `test_bootstrap_leg_moves.py`'s `typed` and `main_of`; the helper `bootstrap_module` is `test_bootstrap_branch_bytes.py`'s. Every test uses the module's own local bare remotes. Each symlink is `os.symlink`, and the test skips on Windows, and where it raises `OSError` (a disk that folds case refuses the link: `Main` is `main`), with the reason. A test that makes a real `Main` skips on a disk that folds case. Every assert on the output compares step (a)'s spec-leg lines exactly.

1. T7 under the emulation: `main` unchanged, HEAD still `refs/heads/Main`, `git status --short` empty, the three lines exact.
2. The way out runs as printed, then bootstrap again: `fast-forwarded`, HEAD `refs/heads/main` at the pin, status empty.
3. P9 under the emulation, the lines exact, and its way out.
4. The missing branch: an unborn twin under the emulation, and a packed twin with a commit, with the way out run.
5. A disk that keeps case: a real loose `Main` with a commit of its own and HEAD on it, T7's state. The same lines. The way out puts the leg on the real `main`, bootstrap fast-forwards it, `Main` keeps its commit.
6. Unchanged byte for byte: HEAD on `main` itself, on `feature` and on `mains` (3 tests).
7. `_folded_twin`'s table (15 rows): `Main`, `MAIN`, the twin the other way, the branch itself, `Main` against `Main`, detached, `refs/remotes/origin/main`, another branch, a longer name, `refs/Heads/main`, `refs/notes/Main` and `refs/tags/xMain` (each 11 characters before `Main`, as `refs/heads/` is), a final sigma against a sigma (`casefold` folds it, `lower()` does not), and a name that is not UTF-8, itself and as a twin. Plus three tests of `_say_twin` itself: the command is shell-quoted as before for an ordinary pair, a name that is not UTF-8 or holds an ESC is spelled (output is ASCII, no raise), and (part 2) five rows read the two words of the command back through `/bin/sh` and compare them with the bytes of the names: an ordinary pair, a path with the byte, a branch with the byte, both with a leading dash, and a path with a percent.
8. (Part 2) A tracking branch `a<0xff>b` and HEAD on its twin `A<0xff>b` (T7, `--branch`, POSIX, on a disk that takes the byte; skipped on Windows, and on macOS, whose disk refuses the byte): the lines name both spelled, the printed command is `git -C spec switch "$(printf -- 'a\377b')"`, run through `/bin/sh -c` as printed it puts HEAD on `refs/heads/a<0xff>b` byte for byte, and the next run fast-forwards the branch.

**On the base (`50178305`, the first 26 tests copied in): 23 failed, 3 passed.** The three that pass are case 6, by design. Part 2's new test cannot run on the base (it imports `test_bootstrap_pasteable_lines.py`, which #944 added); on the merged tree before the part 2 change it failed, at the command: `- git -C spec switch "$(printf -- 'a\377b')"` expected, `+ git -C spec switch 'a\udcffb'` printed. **At this head: 32 passed**, on git 2.43.0 and 2.47.1.

| mutant | result |
|---|---|
| compare byte for byte again (`_folded_twin` never true) | 11 failed |
| the check dropped from T7 / from P9 / from `create_at_pin` | 3 / 1 / 2 failed |
| `casefold()` -> `lower()` | 1 failed (the sigma row). Equivalent for ASCII; the sigma row tells them apart, so no `ss` pair was needed |
| the `None` guard dropped (`None.startswith`) | `make bootstrap`'s first run raises in the fixture: 9 errors and 1 failed |
| a twin with `current` `None` (returns true for it) | the same 9 errors and 1 failed |
| `switch` -> `checkout` in the printed way out | 4 failed (the exact-lines tests). Case 2, which only runs the printed command, still passes: `git checkout main` does the same here |
| the `refs/heads/` guard dropped (whole refs compared) | 1 failed (`refs/Heads/main`) |
| `held != branch` dropped (the branch itself a twin) | 7 failed |
| the path / the branch not shell-quoted | 1 failed each |
| the twin / the branch not spelled | 1 failed each |
| the missing-branch path printing the `switch` command | 3 failed |
| the missing-branch way out saying `a branch` for `a branch whose name differs ... by more than case` | 3 failed |
| **R2** (round 1): the `refs/heads/` guard dropped, the 11-character slice kept | **24 passed on the first 24 tests: it survived. 2 failed on the 26 (`refs/notes/Main`, `refs/tags/xMain`): killed.** |
| round 1's re-runs on the 26: the check dropped from `create_at_pin`; `casefold()` -> `lower()` | 2 failed; 1 failed |
| **part 2:** `_paste_word` dropped from the path / from the branch / the old `spelled(shlex.quote(...))` form restored | 3 / 3 / 5 failed of the 32 (the byte-path rows; the end-to-end test and the byte-branch rows; all of them but the ordinary pair). None survives |

The host's load was 85 to 270, so the coordinator scoped them (one pytest process at a time, 15:53Z and 16:15Z).

- **git 2.43.0**, `tests/test_bootstrap_casefold_twin.py` and every `tests/test_bootstrap*.py` (11 files, the issue's `test_bootstrap_after_bump.py`, `test_bootstrap_leg_moves.py` and `test_bootstrap_non_utf8_branch.py` among them), `test_family_add_spelling_leftovers.py`, `test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): **751 passed, 3 skipped** (the three skip `test_bootstrap_ref_store.py:102`: 2.43 makes no reftable repository) in 14:47. Then `test_bom_reads.py` and `test_validator_child_encoding.py`: **188 passed**.
- **git 2.47.1**, the new file, `test_bootstrap_leg_moves.py`, `test_bootstrap_after_bump.py` and the trio: **248 passed** in 3:40.
- **Round 1, part 2, at the merged tree** (the load was about 50, two processes at a time): git 2.43.0, the new file, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_after_bump.py` and the trio: **329 passed** in 7:04. git 2.47.1, the new file and `test_bootstrap_pasteable_lines.py`: **72 passed** in 2:09. Both ran before the five unit rows were added (a change to the test file alone); the new file was then run again on both: **32 passed** each.
- **Round 1, at the part 1 tree** (the load was about 40, two processes at a time): git 2.43.0, the new file, `test_bootstrap_leg_moves.py`, `test_bootstrap_after_bump.py` and the trio: **250 passed** in 3:44. git 2.47.1, the new file: **26 passed** in 20 s.
- **The predicted merge with origin/main `64464d31`** (a commit of tree `d7d8ca11`, never pushed): the new file, the trio and `test_release_workflow.py`: **231 passed** (git 2.43.0).
- A first run of 115 files in 3 shards (git 2.43.0) was stopped at 35%, 28% and 45% when the load reached 270: about 3,100 tests, none failed. It is not counted.

**Left to CI's `tests` job, which runs the whole suite on the head:** every file not named above. Of the files `git grep -l -E 'assembly-root/scripts/bootstrap|make bootstrap|bootstrap\.py' -- tests` names, these 66 were not run: test_adopt_attributes_eol_block.py, test_adopt_clone_template.py, test_adopt_e2e.py, test_adopt_ignored_shape_files.py, test_adopt_mount_ignored.py, test_adopt_nested_attributes.py, test_adopt_pins.py, test_adopt_plan_yaml_quoting.py, test_adopt_reencoded_files.py, test_adopt_relative_submodule_url.py, test_adopt_root_manifest.py, test_adopt_root_symlinks.py, test_adopt_shape_pin_and_work_dir.py, test_adopt_submodules.py, test_adopt_verify.py, test_bump_leg.py, test_bump_leg_dry_run_truth.py, test_bump_leg_rollback.py, test_bump_leg_unreadable.py, test_doctor_legs_members_next.py, test_doctor_machine_row.py, test_doctor_next_is_a_command.py, test_doctor_paste_through_git.py, test_doctor_pin_restore_merges.py, test_family.py, test_family_bootstrap_gate_order.py, test_family_bump_remove_rollback.py, test_family_gitmodules_values.py, test_family_head_lost_gitmodules.py, test_family_holder_bootstrap_texts.py, test_family_makefile_siblings.py, test_family_members_not_a_list.py, test_family_siblings_placement.py, test_leg_path_yaml_scalars.py, test_legs_readers_not_a_list.py, test_load_yaml_refusals.py, test_naming_policy.py, test_non_utf8_name_helpers.py, test_park_resume_targets.py, test_push_branch_refspec.py, test_scaffold_e2e.py, test_scaffold_ignored_shape_files.py, test_scaffold_leg_path_collisions.py, test_scaffold_manifest_values.py, test_scaffold_mount_ignored.py, test_setup_by_hand_commands.py, test_setup_project_py.py, test_setup_sh.py, test_shape_doctor.py, test_siblings_family_name.py, test_siblings_inputs.py, test_template_text_placeholders.py, test_update_shape.py, test_update_shape_add.py, test_update_shape_apply_lines_and_headers.py, test_update_shape_kept_helper.py, test_update_shape_landing.py, test_update_shape_merged_away.py, test_update_shape_pr_base.py, test_validate_family_branch_step.py, test_validate_family_member_path_glob.py, test_validate_family_remedies_typed.py, test_validate_family_typed_states.py, test_validate_pins_lockstep_lines.py, test_validate_workflow_gitmodules_urls.py, test_windows_paths.py. On git 2.47.1 the 2.43 list's other files were not run either.

- origin/main `450279ca` (#944 landed): this head merges clean, tree `aabc059b`. The merge commit `60df34fc` itself is a clean merge, tree `664513ff`, as predicted.
- Slice D's head `0ef875c0` (#958): clean, tree `af0a54b7`.
- The other lanes' open branches, `fix/415-legs-members-next-tests`, `fix/430-a-bump-leg-unsearchable`, `fix/879-holder-remedies-compose`, `fix/880-check-plan-name-docstring`, `fix/884-ignored-depth`, `fix/885-one-line-install-release-notes`, `fix/897-kept-copy-reraise` and `fix/898-bump-leg-reachability`: none touches `bootstrap.py` or this test file, and each merges clean with this head. `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip` conflict, in `shape-doctor.py`, `validate-pins.py` and tests this change does not touch, and conflict the same way with origin/main itself.
- Slice D's earlier local head, `df1bb186`, conflicted with the first head (both added a block after `_on_a_branch`); it was moved at `0ef875c0`, which merges clean.

- **Docs.** `bootstrap.py`'s module docstring (step (a)) and the docstrings of the three path functions and `_move_and_check_out` say the rule. I grepped `AGENTS.md`, `README.md`, `docs/`, the assembly root's `AGENTS-shape.md` and README, and the family root's, for a sentence about step (a)'s paths. None is false: each says a leg is placed `only where that loses no commit, and otherwise ... left where it is, saying why`, or `or says why not`, and a twin leg is left, saying why. Nothing else was edited.
- **Cognitive complexity (S3776, 15 allowed), counted by hand and with an AST counter that follows Sonar's rules** (each `if`, `elif`, `else`, `for`, `except` and ternary, plus nesting; one per run of one boolean operator): `place_at_pin` 5 -> 6, **`fast_forward_to_pin` 14 -> 15, at the limit**, `create_at_pin` 12 -> 13, `_folded_twin` 3, `_say_twin` 1, `checkout_tracking_branch` 13 (unchanged). If Sonar counts one more in `fast_forward_to_pin`, the cheapest cure is a helper for its `note` (two points). Round 1 changes only words, so none changes. The commit message of 80353dd says `14, 6 and 12`; that was my hand count before the counter found the `why or []` argument in `fast_forward_to_pin` and `create_at_pin`. The numbers here are right, and the second commit's message says so; the squash message should not repeat the first commit's sentence.
- **Width.** Every new line is within 79 columns in both files. No duplicated block; no `assert` joins conditions with `and`.

- **One rule for all three paths,** the unborn twin included (see the probe). rv896 ruled to keep refusing it and not to add `head is not None`: an unborn twin can stand beside a packed third spelling whose commit `checkout -b` hides, and the refusal costs one step (`git -C spec checkout --detach <pin>`, then bootstrap).
- **`casefold()` can over-match a real disk:** `ss` and the sharp s fold under it, which a disk's own simple folding may not do. The leg is then left, with true words, where the disk would not need it.
- **The rule is by name, so a reftable leg is left too,** where `Main` and `main` are two refs on any disk. The way out is true there as well.
- **NFC and NFD twins are out of scope** (below).

1. **An NFC/NFD twin does the same harm, and is not caught.** APFS also reads a name's NFC and NFD forms as one file. Emulated as the issue's probe was (a symlink from the NFD name to the NFC branch `caf` + U+00E9, HEAD on the NFD name, `--branch` the NFC name, git 2.43.0), at `50178305` AND at this head: `branch caf<e9> fast-forwarded <old> -> <pin>`, `the leg was on cafe<U+0301>, which keeps its commits; it is on caf<e9> now`, status `D  NEW-CONTRACT.md` and `M  README.md`. `casefold()` does not normalize; `unicodedata.normalize("NFC", ...)` in `_folded_twin` before the compare is the one-line cure, with a test.
2. **`bump-leg.py` and `family.py` compare the root's or the holder's branch with `tracking_branch` byte for byte** (`branch == project.tracking`, `branch != family.tracking()`): on a disk that folds case a root on `Main` is on `main`, and the pull-request-only refusal (`bump-leg-on-tracking-branch`, `family-on-tracking-branch`) is not given. Reasoned from the code; not probed.
3. **A HEAD on a ref whose directory is spelled otherwise** (`git symbolic-ref HEAD refs/Heads/main`) is a twin on a disk that folds case, and is not one here: the rule leaves refs outside `refs/heads/` alone, as the brief says. Reasoned; very rare.
4. **From rv896, for later:** a follow-up for `create_at_pin`: the same lost-commit harm happens from a HEAD detached at the pin over a packed `Main` and no `main`, which the rule does not see (it asks only about HEAD's ref); before `checkout -b`, ask `git for-each-ref refs/heads/` for any name that casefolds to the branch, packed or loose.

Review: rv896, lane 5's independent reviewer, not the writer (w896): READY at `80353dd1` (issuecomment-6252823490), and its re-check of fix round 1 READY at `45415296`, the head that lands (issuecomment-6253509985), with no MUST and no SHOULD. Its one NIT, a leading `=`, is #961's, open as #973. CI (Brett Heap's ruling 27): H0 is the head 4541529 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 450279c; and the local gate passed on its predicted squash tree 67a22e1 over main 3db58c4: the bootstrap_casefold_twin, repo_hygiene, agents_md_codes, cli_reference sets, 202 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #896

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
… of the bytes git is handed (UTF-8 on Windows), and the NOT INITIALIZED command names a leg's path as one word after a `--`, with `./` before a `:` (#961) (#973)

#944 (#932) made each command step (a) prints for a person to paste name its leg path and its upstream as ONE shell word, through `_paste_word`. rv932's independent review of it (#944 issuecomment-6252637720) left a SHOULD and four NITs in that function, and #961 filed them, with a SHOULD for the one printed command #944 did not word: the NOT INITIALIZED line. This takes them all, in `templates/assembly-root/scripts/bootstrap.py`, `tests/test_bootstrap_pasteable_lines.py`, `tests/test_assembly_root_unreadable_paths.py` (#958's file), `tests/test_bootstrap_spelling.py` and the docstring of `tests/test_bootstrap_after_bump.py`.

The branch is restacked on main `af0e60f2`, which holds #958 (#430 slice D, which moved the NOT INITIALIZED line into `_initialized`): #958's final head, then its squash, then main (nothing newer). The squash's tree is identical to #958's final head, so the merges brought in no code of their own. Against main this PR changes those five files and no other, and `git diff -U0` against main has the same changed lines as this PR's own commits on #958's head `7e87e05f` (checked from objects, see "What this adds to main").

**1. A leading `=` (SHOULD 1).** `_paste_word` quoted with `shlex.quote`, which leaves `=x` bare, and zsh expands a bare `=word` to the path of a command.
- Before, read back by `/usr/bin/zsh` 5.9 (`printf %s <word>`): `=x`, `=ls`, `==x` and `=%` did not print themselves; `=ls` was `/usr/bin/ls` and `=x` was `zsh: x not found`. dash and bash read them all.
- After: the word is `'=x'`, written locally with `quoted.startswith("=")`, as `repo_shape.shell_quoted` quotes it (#486). `bootstrap.py` imports nothing new from `repo_shape.py`, and a test holds the import set. A word `shlex.quote` quotes already (`=a b`) is not quoted again, and `a=b` stays bare.
- The test row `("=x", "=x")` is now `("=x", "'=x'")`, in its own test, because the one it was in asserts "byte for byte what it was".
- rv932's probe (every byte 0x01 to 0xff in seven places, about 160 special names, 3,194 names, each word read by dash, bash and `/usr/bin/zsh` under `LC_ALL=C` and `C.UTF-8`, as `set -- <word>`, one argument and the bytes git is handed): 450 mismatches at `450279ca`, all zsh and all a leading `=` (225 names in each locale), and **0 at the first head**, with 603 names that begin with `=` among those read. The 4 names that end in a line feed have no word (item 3).

**2. The codec (NIT 2).** `_prints_as_it_is` asked the stdout's codec only, but git is handed another set of bytes.
- Before: under a latin-1 or cp1252 stdout, `origin/café` was the quoted word, printed as the byte `e9`, where the ref is `c3 a9`. Under a latin-1 filesystem and a UTF-8 stdout it was the other way round.
- After: a non-ASCII word is kept quoted only where `text.encode(stdout's codec)` equals the bytes git is handed (`_handed_to_git`: `os.fsencode`'s on POSIX, UTF-8 on Windows, item 8). Otherwise it is the `printf` word, which is ASCII. An ASCII word skips the comparison, so `origin/main` stays bare on any stdout. `spelled`'s check comes first.
- **A decision:** the brief says to compare with `sys.getfilesystemencoding()`. `os.fsencode` is that codec with its error handler, and it is what `subprocess` hands git on POSIX, so comparing the two byte strings asks both at once and cannot disagree with what git is given. It is also the call a test shims in process (`LegacyWindowsOs` does, #944).

**3. The round-trip guard (NIT 3).** The guard `os.fsdecode(raw) != name` kept the escape text for `sp\udcc3\udca9c`, though `"$(printf -- 'sp\303\251c')"` reads back exactly the bytes git is handed (the directory `spéc`).
- After: no name is read back. git IS handed those bytes, so a name `os.fsdecode` would read as another has its word. A name that ends in a line feed keeps today's text, as the brief says. (On Windows the guard was replaced by item 8.)
- **An addition inside "what can be pasted":** a name with a NUL has no word either. `$(...)` drops a NUL, so the old word `"$(printf -- 'a\000b')"` printed `ab` in dash and bash (zsh keeps it). It keeps today's text now. It cannot be reached from a real leg path or ref, so this is a guard and a unit row, nothing more.

**4. R6 and R8 (NIT 4).** Two unit rows, in process:
- R6, `except UnicodeEncodeError` alone: a `sys.stdout` stub whose `encoding` is `no-such-codec` (a `LookupError`, not a `UnicodeEncodeError`); the word is the `printf` word and nothing raises.
- R8, `name.encode("utf-8", "surrogateescape")` for `os.fsencode`: a shim `os` whose `fsencode` is latin-1, and `café` + tab; the word is `"$(printf -- 'caf\351\011')"`, and `/bin/sh` prints the shim's bytes.

**5. Stale words (NIT 5).** `tests/test_bootstrap_after_bump.py`'s docstring no longer says the command is "spelled" (#391): the names are, the command is `_paste_word`'s. The pasteable test file's docstring now says what the rule is, and which tests run what was printed and which ask the helper. The module paragraph says where the stdout cannot write the name, the leading `=`, the new command, and the names with no word. `_prints_as_it_is` and `_paste_word` have new docstrings.

**6. The NOT INITIALIZED command (SHOULD 3).** `_initialized` printed `git submodule update --init {spelled(path)}`, bare, for a leg that is not there.
- Before, pasted from the root (rv973 measured 40 awkward paths in sh, dash, bash and zsh): `my spec` was two pathspecs; a path with an ESC, a U+2028, the byte 0xff, `%s` or a tab was its escape text, which names no path; `sp'ec` was a syntax error in every shell; `=spec` failed in zsh (`zsh:1: spec not found`); `-spec` was an option.
- After: `git submodule update --init -- <word>`, the path as `_paste_word` words it, after a `--`. The leg is checked out at its pin again, in every shell and on git 2.43 and 2.47.1 (rv973: 40 of 40). The label `[spec] <path>:` is still spelled.
- A path that is not a name `printf` can print (a lone surrogate, a NUL, a final line feed) keeps its text, as in item 3.
- rv973 asked for a landing condition: the PR does not land without this commit.

**7. A path that begins with `:` (rv973's NIT 1 on the staged commit).** Git reads a word that begins with `:` as pathspec magic, even after the `--`, so `--init -- :spec` matched nothing and the leg stayed empty (all four shells, git 2.43 and 2.47.1). `_initialized` words `./:spec`: `./` goes inside the one word, so a path that needs quoting is `'./:my spec'` and a byte path is `"$(printf -- './:sp\377ec')"`. Only this command has it: `git -C :spec` is a directory, not a pathspec.

**8. Windows: the bytes are UTF-8 (rv973's NIT 1 and NIT 2).** git for Windows is handed UTF-16 and stores UTF-8, and Git Bash reads a paste as UTF-8.
- `_handed_to_git(text)` is `os.fsencode(text)` on POSIX and strict `text.encode("utf-8")` where `os.name == "nt"`. `_prints_as_it_is` compares the stdout with it, and `_paste_word` makes its `printf` bytes from it.
- Before, under `PYTHONLEGACYWINDOWSFSENCODING=1` (where `os.fsencode` is the code page's): on main a UTF-8 stdout got `"$(printf -- 'origin/caf\351')"` for `origin/café` and `\2005` for `€5`, which Git Bash holds as other names, where the quoted word was right; and a codec rule made of `os.fsencode` would have given a cp1252 stdout the same kind of word (it was the staged commit's case, rv973's NIT 2). A lone surrogate had a word of three junk bytes in the default mode.
- After: the quoted word stays on a UTF-8 stdout, the `printf` word is UTF-8 in either mode, and a lone surrogate, which has no UTF-8 bytes, keeps its text. The Windows read-back guard `os.fsdecode(raw) != name` is gone, and with it the one `os.name` test in `_paste_word`. POSIX is unchanged.
- I have no Windows host: the evidence is in-process shims, as in #944.

`tests/test_bootstrap_pasteable_lines.py` is now 128 tests (45 in #944). `tests/test_assembly_root_unreadable_paths.py` has 11 more rows, and `tests/test_bootstrap_spelling.py`'s NOT INITIALIZED test asks for the word.
- **Items 1 to 5, first head, against `450279ca`:** 19 of 97 pasteable tests failed there, 33 of the 52 new cases passed there by design (a word `shlex.quote` quotes already, every `=` name in sh, dash and bash, the cases where both codecs agree, R6's and R8's rows, which kill mutants that base's code does not contain), and 97 of 97 passed at that head.
- **Item 6, on #958's head, against the merged tree with the old line:** 20 failed, 377 passed in the three changed files. 15 new cases failed: five paste rows, the `-spec` row, the zsh row of the `=` test, six rows in `test_assembly_root_unreadable_paths.py`, the legacy-mode UTF-8 row and the euro row. 5 changed existing cases failed, since the line they assert changed. The helper `not_initialized` in #958's file takes the word. Each paste test empties the leg (`git submodule deinit`), runs bootstrap, checks the line whole, runs the printed command from the root in a shell, and checks the leg's `.git` is back at the pin. Git ignores a submodule path that begins with `-`, so `-spec` is checked another way: the command is exactly `git submodule update --init -- -spec`, and git's stderr names the path (without the `--` it prints its usage).
- **Items 7 and 8, against the staged commit:** 11 failed, 405 passed: the `:spec` paste in sh, dash, bash and zsh (4), three text rows for a leading `:` (3), the default-mode lone surrogate (1) and three legacy-mode `printf` rows (3). 8 of the 19 new cases pass there by design (the rows for a space at either end, which kill `_paste_word(path.strip())` and are right in the old code).
- **Pass at this head:** all of them.

Each applied alone to the head's `bootstrap.py`, the changed test files run with `-x`.
- **First head:** 30 mutants (R6, R8, four for item 1, seven for item 2, seven for item 3, and #944's ten), 30 killed.
- **On #958's head, first round:** 38 killed, among them rv973's O3 (the comparison asked outside its `try`).
- **Last round:** 45 applied, 45 killed: five for the `./` (none, `:/` only, always, also before `-`, glued in front of the word), R-H (`.strip()`) and R-F (`_paste_word(spelled(path))`), the command (`spelled(path)`, no `--`, `shlex.quote` only), six for Windows (the `nt` branch dropped, UTF-8 everywhere, `nt` inverted, `surrogatepass`, `os.fsencode` in the word, the read-back guard back), O3, R6, R8 and the rest of #961's and #944's. **One survived the first pass:** the `./` glued in front of the word. It is the same name to a shell, so no paste could tell; two text rows (`:my spec`, `:sp<0xff>ec`) kill it.

All through `run9.sh` (the real `gh` unreachable, `EMAIL`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null` set), at the restacked head `d286b289`.

| git | files | result |
|---|---|---|
| 2.43.0 | the pasteable file, `test_assembly_root_unreadable_paths.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_spelling.py`, `test_pinned_copy_text.py`, `test_adopt_reencoded_files.py` and the trio | 805 passed |
| 2.47.1 | the same | 805 passed |

Earlier heads of this branch also ran the other eight `test_bootstrap*.py` files (343 passed, 3 skipped for the reftable rows), and a predicted merge with main that added the casefold and create-twins files (662 passed).

`git diff origin/main HEAD` touches five files: `templates/assembly-root/scripts/bootstrap.py`, `tests/test_assembly_root_unreadable_paths.py`, `tests/test_bootstrap_after_bump.py`, `tests/test_bootstrap_pasteable_lines.py` and `tests/test_bootstrap_spelling.py`. Their changed lines, in order, are the same as `git diff -U0 7e87e05 e23137c` gives (this PR's own commits on #958's head): 109, 54, 19, 483 and 25 lines, no difference. The three merges are: #958's final head `7c06c578` (clean), its squash `af0e60f2` (two conflicts, `bootstrap.py` and `tests/test_assembly_root_unreadable_paths.py`, both taken from this branch's side, which holds #958's content plus this change; the squash's tree equals the final head's) and main (already merged).

- `_say_twin`, `fast_forward_to_pin`, `place_at_pin` and `create_at_pin`, which #948 (#896) edits. #948 has landed, with its `switch` line and its rename line through `_paste_word`, so they take this PR's leading-`=` quote with no change of their own (rv932's SHOULD 2).
- A leg path that ends in a line feed keeps today's text, a deliberate exception to the Goal's "or the line says it cannot be pasted" (rv932's NIT 1).
- Git ignores a `submodule.<name>.path` that begins with `-`, so no command can initialize a leg there; the printed command is right, and `validate-manifest.py` is not asked about such a path here.
- Not checked: a real Windows host, Git Bash's argument conversion, macOS's `/bin/sh`, git 2.55 and Sonar's own analysis (cognitive complexity by hand: `_prints_as_it_is` 4, `_paste_word` 6, `_handed_to_git` 1, `_initialized` 4).

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

Review: rv973, lane 5's independent reviewer, not the writer (w961): READY at `e23137c7` (issuecomment-6282595318), after its READY at `2a2bc8f9` (issuecomment-6282590057) and at the first head `b2887cf5` (issuecomment-6253889729); `d286b289`, the head that lands, restacks that change unchanged onto #958's squash (lane 5's check from objects, issuecomment-6282600447). CI (Brett Heap's ruling 27): H0 is the head d286b28 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base af0e60f; and the local gate passed on its predicted squash tree 993946c over main ee6f553: the assembly_root_unreadable_paths, bootstrap_after_bump, bootstrap_pasteable_lines, bootstrap_spelling, repo_hygiene, agents_md_codes, cli_reference sets, 635 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #961

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant