Skip to content

bootstrap.py: the name the rename line gives a twin skips one with a branch stored under it, Main-kept over Main-kept/x, which git refuses, so the pasted line works and the next run makes the tracking branch (#1000) - #1006

Merged
brettheap merged 1 commit into
mainfrom
fix/1000-kept-name-under
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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

Follow-up of #962 (landed as #984, 2a78ae1a): the two NITs of rv984b's READY re-check (#984 issuecomment-6266764236), filed as #1000. One commit, 9b8c643f, on 2a78ae1a. Two files change: templates/assembly-root/scripts/bootstrap.py and tests/test_bootstrap_create_twins.py (+177, -10).

A pinned copy: every project's bootstrap.py takes an upstream-changed row, and re-pins it. No digest of the template is kept anywhere, and #984 handled it the same way.

The Goal

The line step (a) prints to rename a twin, git -C <leg> branch -m <twin> <name>, can be pasted as it is. _kept_name's docstring says so (#962), and #984 chose the concrete <twin>-kept name to keep that promise. It broke when a branch is stored UNDER the chosen name.

What was wrong (#1000 item 1)

_kept_name skipped <twin>-kept only when a branch of the leg FOLDED to it. With the leg detached at the pin, Main packed and holding a commit, and a branch Main-kept/x, the line was git -C spec branch -m Main Main-kept, which git refuses: exit 128, error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept', fatal: branch rename failed. The next run printed the same line again, so the person had to find a name alone. On a disk that folds case, main-kept/x blocks Main-kept the same way.

What changed

  • _taken(held, name) (new, bootstrap.py): the name is taken when a held branch, folded, is _fold(name) or starts with _fold(name) + "/". _kept_name loops while _taken(held, name): <twin>-kept, then -kept-2 and on. Main-kept-2 is printed, the pasted line works, and the next run makes the tracking branch.
  • The docstrings of _taken and _kept_name state the rule, why a branch that is a directory PREFIX of the name needs no check, and what the helper cannot see (below).
  • Item 2 (bootstrap.py: _kept_name skips a -kept name with a branch stored under it, and a test row for a twin with a slash (follow-up of #962) #1000): a test row for a twin whose name has a slash. --branch team/main with a packed Team/main, detached over it and ON it. rv984b's mutant O3 (_folded_spelling returning ref.rsplit("/", 1)[-1] instead of ref[len("refs/heads/"):]) survived 63 of 63 tests; the rows kill it, and kill the same cut at the HEAD-twin site (O3b).
  • 13 tests added, the file is 44 tests: seven _kept_name rows (a branch under the name; under it in another case; deeper under it; under each of the first two; a name that only starts with it is no bar; a branch under another name that holds it is no bar; a twin with a slash), two Unicode rows (NFD under an NFC name, NFC under an NFD name), two end-to-end rows (Main-kept/x loose and packed: Main-kept-2 printed, pasted through /bin/sh, the next run makes main) and the two slash rows. Helpers: missing and created take a branch, and twin_with_a_commit. Cognitive complexity: _taken 1, _kept_name 1, create_at_pin 14 and fast_forward_to_pin 15 (both unchanged). New lines are within 79 columns, and the new bootstrap.py lines are ASCII.

What git refuses, and what is left alone

Probed on git 2.43.0 and 2.47.1 in throw-away repositories (gp/gitfacts*.sh):

  • Handled, exit 128: a branch stored under the name, loose or packed, at any depth, for a twin with a slash too, and with the reftable backend.
  • Not refused, so not skipped: a name that only starts with the candidate (Main-kept-x), a tag, a remote-tracking ref or the stash of that name, and an unborn HEAD twin, whose rename only rewrites HEAD and checks nothing.
  • A branch that is a directory prefix of the name (team for Team/main-kept) is not handled, on purpose: the name has every directory its twin has, and the twin folds to the tracking branch, so git will not make that prefix beside the twin (cannot lock ref 'refs/heads/Team'), and a held team stops checkout -b team/main itself, packed or loose. No -kept name helps.
  • Not handled, and said in _kept_name's docstring: a stale reflog directory under the name, which git refuses (directory not empty, exit 128), for-each-ref does not list, and branch -D removes with the branch, so only a ref file removed by hand leaves one; and a twin whose last path component is 246 bytes or more, whose -kept name is refused at the file-name limit.

The probe, before and after

A real repository from the test scaffold, local bare remotes, the printed line pasted through /bin/sh. The leg is detached at the pin, Main packed with a commit, Main-kept/x loose or packed, main missing.

at 2a78ae1a:
  run 1 prints  rename Main to a name that differs from main by more than case, its commits staying on it: `git -C spec branch -m Main Main-kept`, and run bootstrap again.
  pasted        exit 128  error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept' / fatal: branch rename failed
  runs 2 and 3 print the same line again; heads stay Main, Main-kept/x.
with this change:
  run 1 prints  ... `git -C spec branch -m Main Main-kept-2`, and run bootstrap again.
  pasted        exit 0
  run 2 prints  [spec] spec: branch main created at <pin> - at the pin
  heads after   Main-kept-2 (the commit), Main-kept/x, main (the pin)

HEAD unborn on Main, Main-kept/x held:
  at 2a78ae1a   prints Main-kept and the rename works (exit 0, HEAD rewritten); run 2 makes main.
  with this     prints Main-kept-2, also works; run 2 makes main, "the leg was on Main-kept-2, a branch with no commit yet".

The tests that fail at main

The final test file copied onto 2a78ae1a: 9 failed, 35 passed (44 tests). The 9: the five _kept_name rows with a branch under the name (under it; under it in another case; deeper; under each of the first two; a twin with a slash), the two Unicode rows and the two end-to-end rows. The 4 new tests that pass there pass by design: the two no-bar rows, and the two slash rows, which pin code that was already right and fail only for O3 and O3b.

Mutants (each in a scratch tree, against the create-twins and casefold files, 76 tests; all 13 killed)

mutant failed
O3: _folded_spelling returns ref.rsplit("/", 1)[-1] 1 (the slash row, detached)
O3b: the same cut at the HEAD-twin site 1 (the slash row, on the twin)
the new check removed (return folded in held) 9
the "/" dropped (a plain startswith(folded)) 1 (a name that only starts with it)
folded + "/" in h for startswith 1 (a branch under another name that holds it)
the check on the unfolded name 8
all for any (guarded against an empty set; unguarded it hangs) 9
the equality dropped 4
the immediate parent only 1 (deeper)
the first path component only 1 (a twin with a slash)
the name casefolded, not _folded 1 (NFC under, NFD name)
the held names not folded 9
the loop dropped (while False) 13

Runs

At the committed head 9b8c643f, through run9.sh (gh stub first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null):

  • git 2.43.0 (/usr/bin/git): the create-twins, casefold, test_bootstrap_pasteable_lines.py, test_bootstrap_leg_moves.py and test_pinned_copy_text.py files and the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py): 405 passed.
  • git 2.47.1 (exactly $Q/v115/w764/git247/bin/git, first on PATH): the create-twins and casefold files: 76 passed.
  • The rest of the suite is left to CI's tests job.

Merge checks

git merge-tree --write-tree of the head against: origin/main 04923f10 clean (tree b2486824); fix/430-d-assembly-root-guards (#958, cf8d5667) clean (tree 12cb36fb); fix/961-paste-words (#973, b2887cf5) clean (tree de0bb093). Main has moved on since 2a78ae1a and has not touched bootstrap.py or the twin tests, so the branch needs no merge and no new run.

Closes #1000

Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

Summary by Sourcery

Make bootstrap choose pasteable twin-rename commands by skipping candidate names occupied by nested branches.

Bug Fixes:

  • Prevent bootstrap from suggesting a branch rename that Git rejects when another branch exists beneath the proposed name.
  • Ensure branch twins containing slashes are renamed using their complete names.

Enhancements:

  • Extend kept-name selection to account for case- and Unicode-folded branch names stored beneath candidate names.

Tests:

  • Add coverage for nested, packed, case-variant, Unicode-variant, and slash-containing branch names, including end-to-end pasted rename flows.

…branch stored under it, `Main-kept` over `Main-kept/x`, which git refuses, so the pasted line works and the next run makes the tracking branch (#1000)

Follow-up of #962 (landed as #984, 2a78ae1): rv984b's two NITs.

`_kept_name` chose `<twin>-kept` when no branch of the leg FOLDED to it, and
did not look for a branch stored UNDER it. With the leg detached at the pin,
`Main` packed with a commit and a branch `Main-kept/x`, the refusal printed
`git -C spec branch -m Main Main-kept`, which git refuses with exit 128
(`'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept'`; a
loose ref or a packed one, at any depth; git 2.43.0 and 2.47.1, the reftable
backend too), and the next run printed the same line, so the person had to
choose a name. `_taken(held, name)` is now the question `_kept_name` asks: one
of the branches, folded, is the name, or starts with the name and a slash.
`Main-kept-2` is printed, the pasted line works, and the next run makes the
tracking branch. A disk that folds case reads `main-kept/` as `Main-kept` too,
so the fold applies as it does for a name that is there.

What git does not refuse is left alone: a branch that only starts with the name
(`Main-kept-x`), a tag, a remote-tracking ref or the stash of that name, and an
unborn HEAD twin, whose rename only rewrites HEAD. A branch that is a directory
prefix of the name cannot be held beside a twin below it, and would stop the
tracking branch itself. A reflog directory left under the name by hand, and a
name at the file-name limit, are refused by git and cannot be seen from the
refs; `_kept_name`'s docstring says so.

A test row for a twin whose name has a slash (`--branch team/main`, a packed
`Team/main`, detached over it and on it): rv984b's mutant O3, `_folded_spelling`
returning the last path part, survived 63 of 63 tests, and is killed, as is the
same cut at the HEAD-twin site.

tests/test_bootstrap_create_twins.py: 13 tests added, 9 of which fail at
2a78ae1; the other 4 pass there by design (a name that only starts with the
name, or holds it in the middle, is no bar; the two slash rows pin code that was
right). bootstrap.py is a pinned copy: every project's `bootstrap.py` takes an
`upstream-changed` row, and re-pins it.

Closes #1000

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-19T20:38Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes bootstrap-generated twin rename commands reliably pasteable by skipping candidate names blocked by folded branch descendants, then validates the behavior across ref storage formats, case and Unicode variants, slash-containing names, and subsequent bootstrap runs.

Sequence diagram for pasteable twin branch rename

sequenceDiagram
    participant Bootstrap
    participant Git
    participant User

    Bootstrap->>Bootstrap: _heads(sub)
    Bootstrap->>Bootstrap: _kept_name(sub, spelling)
    Bootstrap->>Bootstrap: _taken(held, name)
    alt candidate is occupied or has a held descendant
        Bootstrap->>Bootstrap: Try next -kept candidate
    else candidate is available
        Bootstrap-->>User: Print git -C <leg> branch -m <twin> <name>
        User->>Git: Paste branch rename command
        Git-->>User: Rename succeeds
        User->>Bootstrap: Run bootstrap again
        Bootstrap->>Git: Create tracking branch at pin
    end
Loading

Flow diagram for selecting a safe kept branch name

flowchart TD
    A[Candidate: spelling-kept] --> B["_taken(held, name)"]
    B -->|folded name exists| C[Try spelling-kept-2]
    B -->|held branch starts with folded name plus slash| C
    B -->|available| D[Return candidate]
    C --> B
    D --> E[Print pasteable git branch -m command]
Loading

File-Level Changes

Change Details Files
Prevent generated twin-rename commands from selecting Git-invalid branch names when an existing branch is stored beneath the candidate name.
  • Added _taken to compare folded branch names and detect exact matches or descendants using a slash-boundary check.
  • Updated _kept_name to skip successive -kept candidates until neither a conflicting branch nor descendant exists.
  • Documented intentional limitations involving directory-prefix branches, stale reflogs, and filesystem name-length constraints.
templates/assembly-root/scripts/bootstrap.py
Expanded coverage for nested, case/unicode-equivalent, packed, and slash-containing branch names, including the full paste-and-rerun workflow.
  • Added unit cases for descendants at multiple depths, false-positive prefixes, Unicode forms, and twins containing slashes.
  • Added end-to-end tests for loose and packed refs, verifying the printed rename works and the next bootstrap creates the tracking branch.
  • Added helpers supporting configurable branch names and packed twin setup.
tests/test_bootstrap_create_twins.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1000 Make _kept_name skip a candidate when a held branch is stored beneath it, including under case-folded spellings, so the printed rename command succeeds. ✅
#1000 Add test coverage for a twin whose name contains a slash, ensuring the full branch name is handled rather than only its final path component. ✅

Possibly linked issues


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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 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-19T20:40:50.847831Z 9b8c643 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.

@sonarqubecloud

Copy link
Copy Markdown

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 9b8c643f29

ℹ️ 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 #1006 (#1000) at 9b8c643f, for lane openRepoShape-5: READY

The fix does what #1000 asks. In a real repository, the line printed over a branch stored under <twin>-kept now pastes through /bin/sh and the next run makes the tracking branch. A twin with a slash is named whole. All six mutants I ran are killed, O3 and the dropped-"/" mutant among them. The writer's test counts and merge trees match what I measured. I found no MUST and no SHOULD; there are three NITs, about the words only.

MUST

None.

SHOULD

None.

NIT

  1. The reason _taken's docstring gives for not checking a prefix does not hold in every case (templates/assembly-root/scripts/bootstrap.py:886-890).
    • It says a branch that is a directory prefix of the name "stops checkout -b <tracking branch> just the same".
    • Probe: --branch team/main, a packed Team/main with a commit, and a branch TEAM, on a disk that keeps case. TEAM stops nothing: the rename to Team/main-kept worked, and run 2 printed branch team/main created at <pin>.
    • The example team for team/main-kept can never be there beside its twin team/main. Git will not make team beside team/main; I saw the same refusal for team/main beside team, on the files backend and on reftable.
    • The decision itself is right, for two reasons the docstring could state instead:
      • Every candidate has the twin's directories, so a prefix check would bar every candidate, and _kept_name would never return.
      • A branch that is a directory of the tracking branch blocks checkout -b whatever the twin is renamed to. Probe with a branch team: run 2 said could not create branch 'team/main' ... 'refs/heads/team' exists.
  2. "WHAT IT CANNOT SEE" leaves out a broken ref file under the name (bootstrap.py:904).
    • Probe: a file refs/heads/Main-kept/x that holds garbage. for-each-ref does not list it, so Main-kept is printed.
    • The paste exits 128 with error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept', and every run prints the same line again. This is the same on 2.43.0 and 2.47.1, at the base and at the head.
    • It is a hand-made state, like the stale reflog directory the docstring already names, so one more clause is enough.
  3. The body's "a twin of 246 bytes or more" should be "a twin whose last path component is 246 bytes or more". The file-name limit applies to one path component, so a name like Team/<a 245-byte part> is longer than 246 bytes and still works.

Checked and found true

  1. Before and after, in real repositories. The test scaffold with local bare remotes; each printed line pasted through /bin/sh; checked on git 2.43.0 and 2.47.1.

    • At 2a78ae1a, a branch under the chosen name broke the rename. With Main-kept/x loose, packed, or under HEAD's own twin, and with Team/main-kept/x under the twin Team/main, every paste exited 128 with git's "cannot create" error. Three runs printed the same line.
    • At 9b8c643f, the same states print Main-kept-2 or Team/main-kept-2. The paste exits 0 and run 2 makes the branch. HEAD's twin case adds "the leg was on Main-kept-2, which keeps its commits".
    • The heads after the fix are Main-kept-2, Main-kept/x and main, so the branch under the name is left where it was.
    • On a disk that keeps case, a branch under the name in another case (main-kept/x) also gets Main-kept-2. That skip is not needed there, but it is needed on a disk that folds case, and it matches the assembly root's bootstrap.py: create_at_pin still hides a branch's commits behind a new loose ref when HEAD is detached or a third spelling is packed, and _folded_twin does not fold NFC and NFD #962's rule.
    • An empty directory refs/heads/Main-kept/x/ is no bar: git clears it, and the paste exits 0.
    • A stale reflog directory makes git refuse (exit 128), as the docstring says.
    • With git 2.47.1 in plain repositories, both the files backend and reftable refuse branch -m Main Main-kept over Main-kept/x, and accept Main-kept-2.
  2. What _taken refuses, and whether it can loop.

    • It refuses a name equal under _fold, or a held name that starts with the folded name plus /. Each held name can bar at most one candidate, since the candidates all have the twin's directories and differ only in their suffix. So the loop ends within len(held) + 1 turns.
    • NFC against NFD and case are folded on both sides.
    • A name that only starts with the candidate (Main-kept-x), or holds it in the middle (xMain-kept/y), is no bar.
  3. Tests at the base. The new test file copied onto 2a78ae1a: 9 failed and 35 passed, the 9 the body lists.

  4. Mutants: all six killed.

    mutant failed
    O3 1 (slash row, detached)
    O3b 1 (slash row, on the twin)
    dropped "/" 1 (the "only starts with it" row)
    the new check only on the first candidate (mine) 1
    a suffix that accumulates, Main-kept-2-3 (mine) 2
    packed refs left out of held (mine) 1 (the x packed row)
  5. Runs at 9b8c643f. With the gh stub, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL set:

    • git 2.43.0: the body's eight files, 405 passed.
    • git 2.47.1: the create-twins and casefold files, 76 passed.
  6. Merges. merge-tree --write-tree is clean against main 04923f10 (tree b2486824), the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958 cf8d5667 (12cb36fb) and bootstrap.py: the paste words it prints quote a leading =, are made 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 b2887cf5 (de0bb093); all three match the body. No commit on main since 2a78ae1a touches bootstrap.py or the twin tests.

  7. Style and Sonar-style checks.

    • No added assert joins conditions with and or or.
    • The added lines are at most 79 columns, and the added bootstrap.py lines are ASCII.
    • The imports from repo_shape are unchanged.
    • Cognitive complexity: _taken 1, _kept_name 1, create_at_pin 14 and fast_forward_to_pin 15, the last two unchanged.

Could not verify

Nothing on a real APFS or NTFS disk. What a folding disk does with a directory main-kept/ is reasoned, not measured.

Reviewer: rv984b, lane 5's independent reviewer, not the writer (w1000).

Lane: openreposhape-5 (openRepoShape-5)

@sourcery-ai

sourcery-ai Bot commented Oct 19, 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 1 hour and 34 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 3969409 into main Oct 19, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1000-kept-name-under branch October 19, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bootstrap.py: _kept_name skips a -kept name with a branch stored under it, and a test row for a twin with a slash (follow-up of #962)

1 participant