Skip to content

worktree_base_path rejects stock gh repo fork clones: canonical repo must sit at the remote named "origin" #681

Description

@mjacobs

Summary

worktree_base_path requires the clone's remote literally named origin to be the tracked repo. That is the inverse of the layout gh repo fork produces, so a contributor with a stock fork clone cannot point middleman at it without re-plumbing their remotes first.

Repro

A stock fork clone (gh repo fork kenn-io/agentsview --clone):

origin    https://github.com/<you>/agentsview.git
upstream  https://github.com/kenn-io/agentsview.git
[[repos]]
owner = "kenn-io"
name = "agentsview"
worktree_base_path = "/path/to/agentsview"
$ middleman api -i -d '{"worktree_base_path":"/path/to/agentsview"}' \
    PUT /api/v1/repo/github/kenn-io/agentsview/worktree-base
HTTP/1.1 400 Bad Request

{"title":"Bad Request","status":400,"detail":"origin remote does not match repository:
 clone remote repo \"<you>/agentsview\" does not match configured repo \"kenn-io/agentsview\"",
 "code":"validationError","details":{"field":"body.worktree_base_path"}}

A hand-edited config.toml gets the same rejection later instead, at workspace-creation time (workspaceSetupGitDir -> ValidateWorktreeBasePath).

Why this is worth changing

From gh repo fork --help:

By default, the new fork is set to be your origin remote and any existing origin remote is renamed to upstream.

So the default GitHub fork workflow produces exactly the layout we reject. The workaround is to invert the remotes:

git remote rename origin fork
git remote rename upstream origin
git config remote.pushDefault fork   # keep bare `git push` going to the fork
git config push.default current      # `simple` breaks @{push} once triangular

That works fine (and git rewrites pushDefault, branch.*.remote, fetch refspecs and gh-resolved for you), but it asks users to adopt a non-default convention to satisfy a config field, and the error message never hints that the name origin is what's load-bearing.

Root cause

The requirement is functional, not merely defensive: we fetch by the literal remote name.

  • internal/workspace/manager.go:3177 fetchWorkspaceBaseWithGit -> fetch --prune --no-tags --negotiation-tip=refs/remotes/origin/* origin +refs/heads/*:refs/remotes/origin/*
  • internal/workspace/manager.go:3162 fetchWorkspaceMergeRequestHeadRefWithGit -> fetch ... origin +<ref>:<ref>

If origin were the fork, that would populate the tracked repo's branches with fork data — so the validation is right to refuse:

  • internal/workspace/manager.go:1091 ValidateWorktreeBasePath
  • internal/workspace/manager.go:1204 / :1224 validateOriginRemoteURLs / validateOriginRemoteURL -> gitremote.ValidateRemoteIdentity
  • internal/workspace/manager.go:1293 validateOriginFetchRefspec (asserts remote.origin.fetch updates refs/remotes/origin/*)

The real constraint is "we fetch by name", which is an implementation choice rather than an inherent one.

Proposed fix

Identify the remote by URL identity rather than by name:

  1. Resolve the base remote as the one whose URL matches the tracked repo identity. gitremote.ValidateRemoteIdentity is already exactly that predicate — iterate git remote instead of assuming origin. Optionally accept an explicit remote = "..." on the repo config entry to override, defaulting to auto-detect.
  2. Thread the resolved name through the places that hardcode origin:
    • validateOriginFetchRefspec -> assert remote.<name>.fetch maps to refs/remotes/<name>/*
    • both fetch helpers -> fetch <name>, --negotiation-tip=refs/remotes/<name>/*
    • the origin-HEAD refresh path (refreshWorkspaceBaseOriginHeadWithGit)
  3. Keep rejecting the genuinely ambiguous cases: no remote matches the identity, or two or more do.

If that's more surface than we want, a cheap alternative: keep the origin-only rule but make the 400 actionable — name the offending remote and suggest the rename, so users aren't left guessing that origin specifically is the requirement.

Related (possibly split out)

Even once configured, worktree_base_path is skipped for cross-fork PR workspaces: internal/workspace/manager.go:1001 workspaceSetupGitDir only consults it when ws.MRHeadRepo == nil, and internal/db/types.go:735 documents that field as "nil for same-repo PRs".

So under a fork-based PR flow the field only benefits issue workspaces and same-repo PRs. That compounds the above — the users most likely to have a fork clone get the least out of the feature. Happy to split this into its own issue if it's a separate concern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions