Skip to content

Pin git's message locale at the last site that classified translated stderr - #211

Merged
blooop merged 4 commits into
mainfrom
wayfinder/devlaunch-187
Aug 15, 2026
Merged

blooop merged 4 commits into
mainfrom
wayfinder/devlaunch-187

Conversation

@blooop

@blooop blooop commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

Closes #187.

Both halves the ticket names, in one PR — they came out of one review pass, neither is large, and the ticket allowed either shape.

1. The translated-stderr hazard, one file over

Creating a branch in the bare cache tells the harmless "it is already there" (swallowed, and the launch carries on) from a real failure (raised) by looking for already exists in git's stderr. git marks that string for translation, and unlike the fetch path it had no locale pin.

Measured, not assumed — against git's own German catalog, on this host:

$ LC_ALL=en_US.UTF-8 LANGUAGE=de git branch dup
Schwerwiegend: Branch 'dup' existiert bereits

No already exists in it, so the benign arm is missed. Driving the real method against a real git in that locale, with the fix reverted:

RESULT: RAISED -> Failed to create branch: Schwerwiegend: Branch 'dup' existiert bereits

and with the fix in place, RESULT: swallowed (correct). So this was a live defect on a non-English host, not a theoretical one: every launch of a branch already in the cache failed — the ordinary re-launch, the common case.

The fix is the one the fetch path already carries: hand git an explicit env pinning LC_ALL=C and LANGUAGE=C, layered on the inherited environment rather than replacing it so ssh auth and the rest survive. Both are pinned rather than just LC_ALL, so the guarantee does not rest on the single glibc rule that lets LC_ALL=C outrank LANGUAGE — the run above confirms that rule is real (C.UTF-8 + LANGUAGE=de stays English), and also confirms it is the only thing that would have saved a one-variable pin.

This was the last site classifying translated git output without the pin.

2. The base-branch fetch's outcome, every arm named

When the requested branch does not exist upstream, a second fetch gets the default branch so the new branch is cut from something current. That fetch's outcome was tested only for the failure arm; the other two fell off the end. The dispatch immediately around it names all three and rejects an unrecognised one, and the sum type exists precisely so that guarantee holds wherever its values are read — absorbed silently, a fourth outcome would be indistinguishable from a clean fetch at exactly the moment a brand-new branch is being based on the cache.

Every arm is now named, with an explicit rejection for anything else. No behaviour changes for the three outcomes that exist today — the two newly-named arms both do what falling through did.

Tests

Three, each mutation-checked (fix reverted → test goes red):

Test Pins Mutation check
test_already_exists_answer_survives_a_translated_git With a German host locale set, the env handed to git pins LC_ALL=C and LANGUAGE=C, and the surrounding environment (SSH_AUTH_SOCK) survives Red before the fix; red again when the env replaces os.environ instead of extending it; red again when LANGUAGE is left unpinned
test_an_unrecognised_base_fetch_outcome_is_rejected_not_absorbed An outcome outside the sum, returned by the base-branch fetch, is rejected rather than read as a clean fetch Red before the fix. Separately, collapsing the new RefMissingOnRemote arm turns an existing test red, so the named arms are load-bearing rather than decorative
test_an_unrecognised_requested_ref_outcome_is_rejected_not_absorbed The same rejection on the requested ref's own fetch Red when that guard is removed

The first mirrors the locale test #185 added, at the seam the ticket named. The third is a declared small extension: the dispatch the fix was held up against was itself unexercised — its rejecting arm was the one uncovered line in the method — so pinning only the new one would have left the pair asymmetric. Test only; that guard already worked.

Not fixed here

Pushing a branch to the remote builds its env as a bare {"GIT_SSH_COMMAND": ...}, replacing the inherited environment rather than extending it — so when an ssh key is passed, git runs with no PATH, no HOME and no SSH_AUTH_SOCK. Same shape of mistake as the one fixed above and noticed while fixing it, but a different failure and outside this ticket's scope; left for a ticket of its own rather than smuggled in.

Checks

pixi run ci exits 0 — pylint 10.00/10, 1659 passed, 23 deselected. CHANGELOG updated under ## [Unreleased]; no README change, as no CLI surface moves.

🤖 Generated with Claude Code

Summary by Sourcery

Pin git branch creation to the C locale and tighten handling of base-branch fetch outcomes when ensuring workspace branches.

Bug Fixes:

  • Ensure re-launching a branch that already exists works correctly on non-English hosts by forcing git branch creation to run with a C message locale.
  • Prevent unrecognised base-branch and requested-ref fetch outcomes from being silently treated as successful by asserting on unexpected fetch result types.

Enhancements:

  • Extend ensure-branch logic to explicitly name all defined fetch outcome variants and delegate unexpected ones to a common unhandled handler.

Documentation:

  • Document the locale pinning and fetch outcome handling changes in the unreleased section of the changelog.

Tests:

  • Add tests covering git branch creation under a German host locale to verify message locale pinning and environment preservation.
  • Add tests confirming that unexpected fetch outcomes for both base-branch and requested-ref fetches are rejected rather than absorbed.

blooop added 3 commits August 15, 2026 01:26
create_local_branch tells "the branch is already there" (swallowed) from a
real failure (raised) by looking for "already exists" in git's stderr, and
git marks that string for translation. Measured on this host with git's own
de catalog: LC_ALL=en_US.UTF-8 LANGUAGE=de turns it into "Schwerwiegend:
Branch 'dup' existiert bereits", so the benign arm is missed and an ordinary
re-launch of an existing branch raises.

Same pin the fetch path already carries, layered on os.environ rather than
replacing it so ssh auth and the rest of the environment survive.
The default-branch fetch -- the one that gives a brand-new branch something
current to be cut from -- tested only for FetchFailed and let the other
outcomes fall off the end. The dispatch immediately around it names all three
and rejects an unrecognised one, and the sum type exists precisely so that
guarantee holds everywhere its values are read; absorbed silently, a fourth
outcome would be indistinguishable from a clean fetch at exactly the moment a
new branch is being based on the cache.

No behaviour changes for the three outcomes that exist today.
The dispatch the base-branch fix was held up against was itself unexercised --
its rejecting arm was the one uncovered line in the method. Test only; the
guard already worked.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Pins git’s message locale for branch creation to avoid misclassifying translated stderr, and makes base-branch fetch handling exhaustively name all known outcomes and loudly reject unknown ones, with tests and documentation updated to lock in the guarantees.

Sequence diagram for git branch creation with pinned C locale

sequenceDiagram
    participant WorkspaceBranchManager
    participant Subprocess
    participant Git

    WorkspaceBranchManager->>Subprocess: create_local_branch(..., env={**os.environ, LC_ALL=C, LANGUAGE=C})
    Subprocess->>Git: git branch <branch>
    Git-->>Subprocess: stderr "fatal: branch '<branch>' already exists" (untranslated)
    Subprocess-->>WorkspaceBranchManager: CompletedProcess(stderr)
    WorkspaceBranchManager->>WorkspaceBranchManager: classify stderr contains "already exists"
    WorkspaceBranchManager-->>WorkspaceBranchManager: swallow harmless already_exists case
Loading

File-Level Changes

Change Details Files
Pin git’s message locale for branch creation so stderr-based classification is reliable on non-English hosts while preserving the inherited environment (e.g., SSH auth).
  • Update branch creation subprocess.run call to provide an explicit env built from os.environ and overriding LC_ALL and LANGUAGE to "C"
  • Import os in the branch manager module to construct the merged environment
  • Document the locale pinning and rationale in code comments near the subprocess invocation
devlaunch/worktree/branch_manager.py
Make base-branch fetch outcome handling exhaustive, matching requested-ref dispatch, and reject any unhandled fetch outcome via a shared helper.
  • Replace single FetchFailed check on base_outcome with explicit isinstance branches for Updated, RefMissingOnRemote, and FetchFailed
  • Add an else branch that calls unhandled_fetch_outcome for unexpected fetch outcomes
  • Ensure behaviour for the three known outcomes is unchanged (Updated/RefMissingOnRemote fall through; FetchFailed logs a warning)
devlaunch/worktree/workspace_clone.py
Add tests to enforce locale pinning for git branch creation and exhaustive handling of fetch outcomes for both base and requested refs.
  • Add test_already_exists_answer_survives_a_translated_git to verify LC_ALL and LANGUAGE are pinned to C and that other env vars like SSH_AUTH_SOCK are preserved
  • Add test_an_unrecognised_base_fetch_outcome_is_rejected_not_absorbed to assert that a fourth outcome in the base-branch fetch raises an AssertionError via unhandled_fetch_outcome
  • Add test_an_unrecognised_requested_ref_outcome_is_rejected_not_absorbed to assert the same rejection behaviour for the requested-ref fetch
test/test_worktree_branch_manager.py
test/test_workspace_clone.py
Update CHANGELOG to describe the locale pinning fix and the strengthened fetch outcome handling guarantees.
  • Add an Unreleased entry explaining that non-English host locales no longer break re-launching existing branches because git is invoked with LC_ALL=C and LANGUAGE=C
  • Describe the fix ensuring all fetch outcomes are named and that unknown ones are rejected instead of being silently treated as clean fetches
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#187 Ensure the git call in branch_manager.create_local_branch (branch_manager.py:83) pins git's message locale (e.g., LC_ALL=C and LANGUAGE=C) so that textual classification of stderr is stable across translated git installations, and add a regression test mirroring the existing locale test to verify this behavior. ✅
#187 Update the handling of the base_outcome from the base-branch fetch in workspace_clone.ensure_branch so that all known fetch outcome arms are explicitly named/handled and any unrecognized outcome is rejected loudly rather than silently absorbed, with tests ensuring this exhaustiveness. ✅

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

@codecov

codecov Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.80%. Comparing base (9013d20) to head (c18891d).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #211      +/-   ##
==========================================
+ Coverage   92.76%   92.80%   +0.03%     
==========================================
  Files          24       24              
  Lines        3649     3653       +4     
==========================================
+ Hits         3385     3390       +5     
+ Misses        264      263       -1     
Files with missing lines Coverage Δ
devlaunch/worktree/branch_manager.py 100.00% <100.00%> (ø)
devlaunch/worktree/workspace_clone.py 94.20% <100.00%> (+0.57%) ⬆️

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The spec review drove the real call path (ensure_branch_exists, both
callers guarded by not local_exists under the repo lock) and showed the
already-exists arm is reachable only in a race — not on ordinary
re-launches as the entry claimed. State the latent hazard, not an
everyday failure. Follow-up for the flagged push_branch_to_remote env
replacement filed as #212.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during review.

Two-axis review of #211 against ticket #187 at merge-base 9013d20. Preflight: all checks green, non-empty diff (5 files, +110/−1), MERGEABLE.

Standards

Nothing blocking. The change conforms to the repo's documented and de facto conventions.

  • Env-pin duplication at n=2 — don't extract the code; do dedupe the rationale (nit). env={**os.environ, "LC_ALL": "C", "LANGUAGE": "C"} is one line, and Fowler's Duplicated Code doesn't bite at one line across two modules. What is duplicated is the justification: the same ~7-line "LANGUAGE outranks a non-C LC_ALL" comment now appears at branch_manager.py:79-86 and repo_manager.py:385-390, again in both tests, and again in the CHANGELOG. A named module-level constant (e.g. C_MESSAGE_LOCALE_ENV beside FetchOutcome in repo_manager.py) carrying the reason once would buy the dedupe without inventing a wrapper. A run_git() helper would be premature; a shared constant is not.
  • Shotgun Surgery exposure is real but bounded (observation). The invariant "any git call whose stderr is classified textually must pin the message locale" is enforced only by prose. Grep confirms exactly two such sites codebase-wide — branch_manager.py:92 and repo_manager.py:396 — and after this PR both are pinned. A third site is where extraction becomes mandatory.
  • The base_outcome dispatch is right, and the pass arms are load-bearing (observation). unhandled_fetch_outcome(outcome: NoReturn) is an exhaustiveness check ty verifies by narrowing to Never; the explicit arms are what make the check fire. Matches the sibling dispatch exactly.
  • Nit: the two aligned sites still disagree on the None-guard. repo_manager.py:395 uses e.stderr or ""; branch_manager.py:92 is unguarded. Pre-existing, not introduced here. (Folded into follow-up #212.)
  • Nit: ensure_branch is now ~90 lines at three nesting levels — but the two dispatches carry different payloads, so leaving it beats forcing symmetry.
  • Test duplication is the conforming choice — no shared-assertion helper convention exists to hook into. CHANGELOG in the correct section; README correctly untouched.

Spec

Verified independently on this host:

  • Live defect reproduced at the classifier. LC_ALL=en_US.UTF-8 LANGUAGE=de git branch dup → Schwerwiegend: Branch 'dup' existiert bereits; LC_ALL=C.UTF-8 LANGUAGE=de stays English. Both the translation claim and the "one glibc rule" justification for pinning LANGUAGE too are true, not assumed.
  • Tests: all three pass; the two touched suites total 92 passed. Mutation spot-check: replacing (rather than extending) os.environ turns the test red on KeyError: 'SSH_AUTH_SOCK' — the assertion is load-bearing.
  • No behaviour change for the three existing outcomes; FetchFailed warning byte-identical.
  • "Last site" claim holds: only branch_manager.py:92 and repo_manager.py:395 classify git stderr textually; both now pinned.

Findings:

  • BLOCKING (doc accuracy only). The CHANGELOG asserted "every launch of an existing branch raised Failed to create branch". Not true: create_local_branch is reached only from ensure_branch_exists, both call sites guarded by not local_exists under the RepoLock — so the already-exists arm is reachable only in a race. Driving the real ensure_branch_exists against a German-locale git with the pin stripped produced no raise. The fix and the ticket's diagnosis are right; the release note overstated realised user impact.
  • Non-blocking. push_branch_to_remote's env replacement is correctly out of scope per the ticket, but the promised follow-up ticket had not been filed.

Deviations judged: extra requested-ref test — sound; both halves in one PR — sound (ticket: "Fix both together"); CHANGELOG yes / README no — sound; push hazard flagged not fixed — sound in scope.

Verdict

Request changes → resolved during review. The single blocking finding was documentation accuracy; the orchestrator reworded the CHANGELOG entry to the verified reachability (commit c18891d, "latent misclassification at the seam, not an everyday launch failure") and filed the promised follow-up as #212 (which also carries the None-guard alignment nit). With those in, no findings block. GitHub refuses author-account review states, so this is posted as a comment; this written verdict governs. Merging with a merge commit once checks re-green on c18891d.

@blooop
blooop merged commit 795e393 into main Aug 15, 2026
12 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-187 branch August 15, 2026 00:46
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.

The same translated-stderr hazard remains at branch_manager.py:83

1 participant