Skip to content

Instantly share code, notes, and snippets.

@raine
Created April 22, 2026 09:04
Show Gist options
  • Select an option

  • Save raine/8e35a4c390dcfa1cd642b2f22c40fec5 to your computer and use it in GitHub Desktop.

Select an option

Save raine/8e35a4c390dcfa1cd642b2f22c40fec5 to your computer and use it in GitHub Desktop.

PR #142

Summary

PR #142 reworks PR checkout (workmux add --pr N) to avoid creating fork-<owner> remotes. Instead of fetching from the fork and setting up a named remote for tracking, it fetches refs/pull/<N>/head from the base repo and stores the head repo URL directly in branch.<name>.remote.

This document explains why that approach breaks multiple core workmux and git workflows.


The Change

The PR replaces the existing flow:

1. Call gh API to get PR details
2. Create fork remote (e.g., fork-mic92)
3. Fetch from fork remote
4. Create branch tracking fork remote

With:

1. Call gh API to get PR details
2. Fetch refs/pull/N/head from origin
3. Create branch
4. Set branch.<name>.remote = [email protected]:head-owner/repo.git  (RAW URL)
5. Set branch.<name>.merge = refs/heads/<branch>

The refs/pull/<N>/head fetch is correct and solves a real problem (deleted head branches). But storing a raw URL in branch.remote violates Git's data model and breaks tooling across the stack.


Breakage 1: remove --gone Will Never Clean Up PR Branches

Severity: HIGH File: src/git/branch.rs:286-304

Workmux's remove --gone feature uses git for-each-ref --format="%(refname:short)|%(upstream:track)" to find branches whose upstream has been deleted. When branch.remote is a URL, Git returns an empty track string instead of [gone].

Test evidence:

# URL-valued remote (PR #142 behavior):
$ git for-each-ref --format="%(refname:short)|%(upstream:track)" refs/heads
main|
pr-branch|          <-- empty, not [gone]

# Named remote (existing behavior):
$ git for-each-ref --format="%(refname:short)|%(upstream:track)" refs/heads
main|
pr-branch|[gone]    <-- correctly detected

This means PR branches will accumulate forever. Users will have no automatic way to discover stale PR checkouts. The remove --gone command becomes useless for the exact branches it should be most useful for.


Breakage 2: HTTPS Users Cannot Push

Severity: HIGH

The PR hardcodes SSH transport for the tracking URL:

let head_url = format!("git@{}:{}/{}.git", host, head_owner, head_repo.name);

But the fetch path uses HTTPS as fallback. Users who:

  • Cloned the repo via HTTPS
  • Use credential helpers (not SSH keys)
  • Are behind corporate proxies that block SSH

will get broken push/pull after checkout:

$ git config branch.feature.remote "[email protected]:forkowner/testrepo.git"
$ GIT_SSH_COMMAND="ssh -o BatchMode=yes" git pull
ERROR: Repository not found.
fatal: Could not read from remote repository.

The PR does not infer the user's preferred transport from origin. It assumes everyone uses SSH for forks.


Breakage 3: Remote Matching Matches Wrong Forge

Severity: HIGH File: src/git/remote.rs (PR introduces find_remote_for)

The PR's find_remote_for() matches remotes only on owner and optionally repo:

if !provider.owner().eq_ignore_ascii_case(owner) { continue; }
if let Some(repo) = repo_name
    && !provider.repo().eq_ignore_ascii_case(repo)
{
    continue;
}

It never checks host. A remote pointing to gitlab.com/user/repo would match a lookup for github.com/user/repo. The owner:branch syntax passes None for repo_name, making it match any remote with that owner regardless of host or repository.

This could cause workmux add owner:branch to silently reuse a remote from the wrong forge, sending pushes to the wrong place.


Breakage 4: Silent Stale Branch Reuse

Severity: MEDIUM File: src/workflow/pr.rs (PR's resolve_pr_ref)

If a local branch already exists, the PR returns it without fetching or verifying:

if git::branch_exists(&local_branch)? {
    return Ok(PrCheckoutResult { local_branch });
}

This means workmux add --pr 123 can attach a worktree to a stale branch that has nothing to do with the PR. If the user previously created a feature branch locally, then runs workmux add --pr 456 where PR 456's head branch is also named feature, workmux silently uses the local branch without fetching PR 456's code.


Breakage 5: Config Lock Race Condition

Severity: MEDIUM Files: src/command/add.rs, src/workflow/create.rs

The PR writes to .git/config in resolve_pr_ref(), which is called from command/add.rs before plan.execute(). But the GitConfigLock is only acquired inside workflow/create.rs.

add.rs:320   resolve_pr_ref() -> writes config
...
add.rs:497   plan.execute() -> create.rs:349 GitConfigLock::acquire()

Concurrent workmux add --pr commands will race on git's config.lock file, causing one to fail with "could not lock config file."


Breakage 6: push.default=simple Fails on Cross-Repo PRs

Severity: MEDIUM

Cross-repo PRs prefix the local branch with the owner (e.g., forkowner-feature) while the upstream tracks feature. With push.default=simple (Git default since 2.0):

$ git push
fatal: The upstream branch of your current branch does not match
the name of your current branch.  To push to the upstream branch
on the remote, use

    git push fork HEAD:feature

This only appears after the user makes a commit. Before that, git says "Everything up-to-date," hiding the problem. The user discovers they cannot push only after they've done work.


Breakage 7: git branch -vv and git status Show No Upstream

Severity: MEDIUM (UX degradation)

Git porcelain commands assume branch.remote is a remote name. When it's a URL:

  • git branch -vv shows no upstream column for PR branches
  • git status --porcelain=v2 --branch omits upstream tracking info
  • Shell prompts that show upstream status break

This makes PR branches feel "second-class" in normal Git workflows.


Breakage 8: Unescaped Commas in Docker Mounts

Severity: LOW File: Sandbox excluded_files implementation

The mount spec type=bind,source=/dev/null,target=/path,readonly uses CSV-like parsing. A path containing a comma would be mis-parsed. While paths with commas are rare, this is a latent bug in the sandbox feature.


Architectural Inconsistency

The PR leaves two different remote management strategies in the codebase:

  • --pr: URL-based tracking, no named remote
  • owner:branch: Named remotes via ensure_fork_remote()

This means users get completely different behavior depending on which syntax they use. Both paths solve the same problem (getting code from a fork) but with incompatible implementations.


Why Not Just Patch get_gone_branches?

One might ask: why not just special-case URL remotes in get_gone_branches()?

Because the problem is not localized. URL-valued branch.remote breaks:

  • Git's upstream tracking (%(upstream:track))
  • git branch -vv
  • git status --porcelain=v2 --branch
  • push.default=simple
  • Any shell prompt that reads upstream state
  • Any future workmux feature that assumes branch.remote is a remote name

Patching each symptom individually creates a maintenance burden and still leaves Git in an invalid state. The fix is to not write URLs into branch.remote in the first place.


Conclusion

The refs/pull/<N>/head fetch mechanism in PR #142 is correct and valuable. But the URL-based tracking approach is architecturally wrong. It violates Git's data model, breaks workmux's own cleanup features, and degrades the standard Git UX for PR branches.

The right fix keeps the refs/pull/<N>/head fetch but uses named remotes for tracking, either:

  1. Per-PR scoped remotes (e.g., wm-pr-123) with proper refspecs, or
  2. Shared fork remotes with strict host+owner+repo matching, inheriting the user's transport protocol.

Both approaches preserve Git's upstream tracking semantics and fix the original problem without introducing new breakage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment