Skip to content

git wt <new-branch> <start-point> silently creates the wrong branch when start-point is a remote-only branch name #203

Description

@nanyanen87

Thanks for git-wt — it's become a core part of my daily multi-worktree workflow.

Summary

When creating a new worktree with git wt <new-branch> <start-point>, if <start-point> is a bare branch name that does not exist locally but exists on exactly one remote (e.g. develop right after a clone, or in gitflow repos where develop is rarely checked out locally), the underlying git worktree add -b <new-branch> <path> <start-point> triggers git's branch DWIM: the -b <new-branch> is silently ignored, and a local branch named after <start-point> (e.g. develop) is created and checked out instead.

The worktree directory is still named after <new-branch>, so the mismatch is easy to miss — you keep working and committing directly onto a branch named develop while believing you are on your feature branch.

flowchart TD
    A["git wt my-feature develop<br/>(→ git worktree add -b my-feature &lt;path&gt; develop)"] --> B{"'develop' resolves<br/>locally?"}
    B -- "yes" --> C["✅ new branch 'my-feature'<br/>at develop"]
    B -- "no" --> D{"matches exactly one<br/>remote branch?"}
    D -- "no" --> E["✅ error:<br/>invalid reference"]
    D -- "yes" --> F["⚠️ DWIM: new_branch = 'develop'<br/>-b my-feature is silently clobbered"]
    F --> G["worktree dir: …/my-feature/ (as requested)<br/>checked-out branch: develop (not requested)"]
Loading

The end state that makes this so easy to miss:

../work-wt/my-feature/          <- directory named as requested
    HEAD -> refs/heads/develop  <- but the branch is not ('my-feature' was never created)

Reproduction

tmp=$(mktemp -d) && cd "$tmp"
git init --bare remote.git
git init work && cd work
git config user.email t@t && git config user.name t
git commit --allow-empty -m init
git remote add origin ../remote.git && git push -q origin main
git checkout -qb develop && git commit --allow-empty -m dev && git push -q origin develop
git checkout -q main && git branch -qD develop   # develop now exists only on the remote

git wt my-feature develop
git -C ../work-wt/my-feature branch --show-current

Actual

Preparing worktree (new branch 'develop')
branch 'develop' set up to track 'origin/develop'.
...
develop        <- checked-out branch, despite asking for my-feature

Expected

Either a worktree on a new branch my-feature starting from origin/develop, or an error telling me the start-point is ambiguous / remote-only.

Environment

  • git-wt v0.29.0 (also reproduced on v0.15.0)
  • git 2.50.1 (Apple Git-155), macOS
  • Plain git worktree add -b my-feature <path> develop reproduces the same behavior, and --no-guess-remote does not prevent it — so this is git's behavior, not a git-wt bug per se. But since git wt <branch> <start-point> is a first-class git-wt interface, git-wt is in a good position to guard against it.

Root cause (git side)

This actually appears to be a bug in git itself, where the implementation contradicts its own documentation. git-worktree(1) says the remote DWIM applies only when "neither -b nor -B nor --detach are used", but the two-positional-args code path in builtin/worktree.c only excludes --detach and unconditionally overwrites the -b value:

} else if (ac == 2) {
        ...
        commit = lookup_commit_reference_by_name(branch);
        if (!commit) {
                remote = unique_tracking_name(branch, &oid, NULL);
                if (remote) {
                        new_branch = branch;   /* clobbers the -b argument */
                        branch = new_branch_to_free = remote;
                }
        }

The same code is present at least as far back as v2.20.0 (2018), so even if it gets fixed upstream, git-wt users will be running affected git versions for a long time — which I think justifies a guard in git-wt regardless.

Why this is easy to miss

I think a few things conspired to keep this hidden (in git core for 7+ years, and here):

  • The docs say it can't happen. Reading git-worktree(1) actively convinces you that -b makes this safe, so nobody suspects this path — neither users nor wrapper authors.
  • The precondition is narrow. The name must be missing locally and exist on exactly one remote. Developers who work in a repo daily usually have develop checked out locally, so it mostly bites fresh clones, CI, and agent-driven workflows that create worktrees from a clean state.
  • The failure is self-masking. The directory is named after <new-branch>, everything downstream works, and the only clue is a single stderr line (new branch 'develop'). You typically notice at push/PR time — and then it's easy to blame yourself rather than the tool.
  • For the same reason, the current e2e suite can't hit it: start-points are exercised in remote-qualified form (origin/main~1), and test repos are created by cloning, so the branch always exists locally.

Proposal

This feels like a sibling of #179 ("error when start-point is specified for existing branch or worktree", i.e. don't let a start-point be silently ignored). Options, in my order of preference:

  1. Error out: before running worktree add, verify <start-point> resolves as-is (e.g. git rev-parse --verify --quiet "<start-point>^{commit}" plus a check that it isn't a remote-only bare branch name). If it only matches refs/remotes/*/<start-point>, fail with a hint: did you mean "origin/<start-point>"?
  2. Auto-qualify: if <start-point> matches exactly one remote branch and no local ref, rewrite it to the remote-qualified form and proceed (perhaps with a notice on stderr).

Happy to send a PR for whichever direction you prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions