Repository navigation
update-shape.py, family.py and bump-leg.py: the branch HEAD is on is read with .strip(), so a branch ending in U+00A0, U+2028 or U+0085 is read as its neighbour, and bump-leg.py (and update-shape.py) decode it with errors="replace", so the push line names a branch that is not there #765
Description
Goal: each tool that reads the branch HEAD is on reads the branch's own name, byte for byte, and one it cannot spell is refused by name before it commits, so no push line names another branch.
Filed by lane openRepoShape-5, from rv672's review of #672's PR (#746, head a5d473ee; its checklist candidates 1 and 2). Candidate 3 of that review, {root} printed raw, is in the sibling issue for raw spellings.
Lands after: lane 5's #672 (PR #746, fix/672-bump-leg-branch-and-push-line), which gives bump-leg.py the full ref and rstrip("\n") (S2 of its fix round) and the push line refs/heads/<b>:refs/heads/<b>; lane 4's #674 (fix/674-update-shape-apply, head d0699029), which gives update-shape.py's current_branch the full ref and the same push line; and lane 4's #689 (fix/689-next-push-refspec, head fa513848), which gives family.py the same. Line numbers are at main 66b230ed; where a PR head holds the line, its sha is given.
Lines
- The branch read strips Unicode whitespace in
update-shape.pyandfamily.py.bump-leg.pyreadsymbolic-ref's answer with.strip(), which also removes a trailing U+00A0, U+2028 or U+0085, and bump-leg.py: the branch is read in full and pushed asrefs/heads/<b>:refs/heads/<b>, dirty names are spelled,committedlists what git holds, and the dry run says it runs no validator (#672) #746's fix round (S2) takes only the newline off (proc.stdout.rstrip("\n"),a5d473ee:scripts/bump-leg.py:1074-1100). Measured there by rv672: a root onfeatand U+00A0 was read asfeat, a branch of its own, and the push line pushed that instead of the bump. The other two tools use the same call shape (read from source, not measured):update-shape.py:2230-2234(current_branch:symbolic-ref --quiet --short HEAD, thenproc.stdout.strip(); still.strip()atd0699029:update-shape.py:2265after update-shape.pyapply: a rollback line fails over a read-onlycontracts/unsaid,committed on <branch>:lists a path the commit lacks,--branch v1.0.0cannot push beside a tagv1.0.0, and a quoted"files":header is refused #674 drops--short),update-shape.py:3171-3178(_symbolic_head:(answer or "").strip()), andscripts/family.py:698-704(_holder_branch:.decode("utf-8", "surrogateescape").strip(); still so atfa513848:scripts/family.py:687-695).scripts/family.py:4498reads a member's HEAD throughgit_out, which strips the same way (scripts/repo_shape.py:1065:proc.stdout.decode("utf-8").strip()). Direction:rstrip("\n")in each, as bump-leg.py: the branch is read in full and pushed asrefs/heads/<b>:refs/heads/<b>, dirty names are spelled,committedlists what git holds, and the dry run says it runs no validator (#672) #746 did;git_out's own strip is bootstrap.py and repo_shape.py: a leg on a branch whose name is not UTF-8 makes every latermake bootstrapend in a UnicodeDecodeError, and a character the filesystem encoding lacks still ends step (a) in a traceback #710's to decide (it changes that decode), andfamily.py:4498follows it. - A branch whose name is not UTF-8 gets a push line that cannot work.
bump-leg.py'scurrent_branchdecodes witherrors="replace"(mainscripts/bump-leg.py:1070-1075;:1094ata5d473ee), so a branch namedcafand byte 0xE9 reads ascafand U+FFFD, and the line namesrefs/heads/cafand U+FFFD on both sides; pasted, it failssrc refspec ... does not match any. Measured by rv672 on base (push -u origin 'caf...', bare) and on head ('refs/heads/caf...:refs/heads/caf...'): the same failure, nothing sent.update-shape.py:2230-2233decodes the same way (encoding="utf-8", errors="replace"; read from source, not measured), andfamily.pydecodes withsurrogateescape, which keeps the byte but which the printed line then has to spell. Direction: rv672's: refuse such a branch by name before committing, inbump-leg.pyandupdate-shape.py(the person can rename it:git branch -m), since a push line that names another branch is worse than none; or read the bytes, asfamily.pydoes, and print the name throughrepo_shape.spelledso a line a person pastes holds the byte. Decide once for the three tools; bootstrap.py and repo_shape.py: a leg on a branch whose name is not UTF-8 makes every latermake bootstrapend in a UnicodeDecodeError, and a character the filesystem encoding lacks still ends step (a) in a traceback #710's decision aboutgit_out(keep the name's bytes, since the name goes back to git) is the model.
Verify: tests/test_bump_leg_branch_and_push_line.py (new in #746), tests/test_non_utf8_git_reads.py, tests/test_update_shape_landing.py and tests/test_update_shape_paste_lines.py and the family files that print a NEXT line (tests/test_family_add_refusals.py and the tests/test_family*.py files #689 edits) pass on Linux. One new case per tool with a branch ending in U+00A0 beside the same name without it: the tool reads each as itself, and its refusal or push line names the branch it is on; and one per tool with a branch named with byte 0xE9 (POSIX only, skipped where the filesystem refuses the name, as skip_where_a_name_cannot_be_not_utf_8 does after #723), asserting the refusal or the spelled line. Mutate rstrip("\n") back to strip() and see the case fail, as #746's R-mutants did for bump-leg.py.
Overlaps. fix/672-bump-leg-branch-and-push-line (#746) edits scripts/bump-leg.py and tests/test_non_utf8_git_reads.py; fix/674-update-shape-apply edits update-shape.py (current_branch at :2246-2270 of its head); fix/689-next-push-refspec and six other open branches edit scripts/family.py (#646, #684, #686, #688, #692, #693), and fix/689-... also edits scripts/shape_materialize.py; l3-676-pins-merged-away and l3-683-partial-clone edit update-shape.py. update-shape.py, scripts/family.py and scripts/bump-leg.py are the standard's own tools, not pinned copies. #710 changes git_out, which is pinned in every project (scripts/repo_shape.py).
Parent: #458 (update-shape.py, bump-leg.py) and #459 (family.py).
Milestone: v1.1.1. A branch named with a trailing no-break space or a byte that is not UTF-8 is not a regression; the first needs a name nobody types, and the second ends in a push that fails and sends nothing.
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 17, 2026
CLAIMED — lane openRepoShape-1, session 300f8f46-004a-4e72-8587-4f192be2e108@Eagle, 2026-10-17T10:31:21Z, for #765
Logged in
lanes/log/openRepoShape-1.mdat09296d46b73d4d9f0bd537f85f857f064cd4b69d.The three reads (lane-collision-protocol Rule 1):
update-shape.py, family.py and bump-leg.py: a rollback hands git the …
Merge main d6071c7 into fix/765-branch-read-bytes: both sides' tests …