Skip to content

fix: base new worktrees on fetched origin/<default>, not stale local main#5

Merged
joch merged 4 commits into
mainfrom
fetch
Jun 25, 2026
Merged

fix: base new worktrees on fetched origin/<default>, not stale local main#5
joch merged 4 commits into
mainfrom
fetch

Conversation

@joch

@joch joch commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Problem

ygg new runs git fetch origin (advancing origin/<default>) but then created the worktree from the local default branch via git worktree add -b <name> <path> main — the bare name resolves to the local branch. If you hadn't pulled main, the new worktree was based on a stale commit. The Pull() method the help text and a // Pull default branch comment implied was running was never called anywhere.

Reproduced: after fetch, origin/main at commit B but the new worktree came out at the stale local commit A.

Fix

Create() now resolves the base via a new baseRef() helper:

Situation Base used
local behind origin (forgot to pull) origin/<default>
local ahead of origin (unpushed / offline) local — never silently drops local commits
diverged local (preserves prior behavior)
no remote local default branch

It selects origin only when the local branch is an ancestor of origin/<default> (git merge-base --is-ancestor).

Also in this PR:

  • --no-track so the new branch doesn't adopt origin/<default> as its upstream
  • existing-branch detection uses an up-front ref check instead of a locale-fragile "already exists" string match
  • the success line reports the actual resolved base (Created worktree at … (based on origin/main))
  • removed the dead Pull() method; corrected the misleading help text/comment

Tests

New tests cover both base directions (stale→origin, ahead→local), --no-track, no-remote fallback, and existing-branch checkout. Verified locally with go test -race ./..., go vet, and staticcheck (matches CI), plus an end-to-end run of the built binary against the stale-main scenario.

https://claude.ai/code/session_01SMjzH78VMHpvo7LdtqRbPE

…main

`ygg new` fetched origin (advancing origin/<default>) but then created the
worktree from the local default branch via `git worktree add -b <name> <path>
main`, where the bare name resolves to the *local* branch. When the local
default branch was un-pulled, the new worktree was based on a stale commit. The
`Pull()` method the help text implied was run was never called.

Create() now resolves the base via baseRef():
- local behind origin (forgot to pull)  -> base on origin/<default>
- local ahead of origin (unpushed/offline) or diverged -> keep local, so
  local-only commits are never silently dropped
- no remote -> local default branch

Also:
- `--no-track` so the new branch doesn't adopt origin/<default> as upstream
- existing-branch detection uses an up-front ref check instead of a
  locale-fragile "already exists" string match
- the success line reports the actual resolved base
- removed the dead Pull() method; corrected the misleading help text/comment

Tests cover both base directions, no-track, no-remote fallback, and existing-
branch checkout.

Claude-Session: https://claude.ai/code/session_01SMjzH78VMHpvo7LdtqRbPE

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1b1293c5b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/worktree/worktree.go Outdated
Comment on lines +111 to +112
if m.isAncestor(defaultBranch, remoteRef) {
return remoteRef

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the same upstream base for cleanup

When this returns origin/<default> because local main is behind, the local default branch is intentionally left stale. ygg clean still computes merged branches with the bare local name via wm.MergedBranches(defaultBranch) in internal/cli/clean.go:60-61, so a no-op worktree created at origin/main (or one later merged upstream) is not considered merged until the user manually pulls local main. That regresses cleanup in the stale-main scenario this change is fixing; the merge check needs to use the same resolved base/ref or otherwise refresh local main before cleaning.

Useful? React with 👍 / 👎.

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.

Good catch — fixed in c3ac260. Exported baseRefBaseRef and routed both merge-detection paths (ygg clean and ygg remove via IsBranchMerged) through it, so they measure against the same resolved default tip (origin/<default> when local main is stale) that ygg new now bases worktrees on. A branch merged upstream is now detected before a local pull; behavior is unchanged when local is ahead, diverged, or there is no remote. Added TestIsBranchMergedUsesResolvedBaseForStaleLocalMain covering it.

Caveat: git branch --merged against any ref still does not detect squash-merged branches (the squashed commit is not an ancestor of the branch tip), so this does not change squash-merge cleanup — gone-branch cleanup remains the mechanism for that.

`ygg clean` and `ygg remove` (via IsBranchMerged) detected merged branches
against the bare local default branch, so when `ygg new` bases worktrees on
origin/<default> (stale local main), a branch merged upstream was not seen as
merged until the user pulled local main.

Export baseRef -> BaseRef and route both merge-detection paths through it, so
clean/remove measure against the same resolved tip new worktrees are based on.
For true/fast-forward merges this detects upstream merges before a local pull;
behavior is unchanged when local is ahead, diverged, or there is no remote.

Note: squash merges are not detected by `git branch --merged` against any ref,
so this does not change squash-merge cleanup (use gone-branch cleanup for that).

Claude-Session: https://claude.ai/code/session_01SMjzH78VMHpvo7LdtqRbPE
@joch

joch commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3ac2603c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/worktree/worktree.go Outdated
// diverged, the local branch wins so those commits are never silently dropped.
// Falls back to the local branch name when no remote-tracking ref exists.
func (m *Manager) BaseRef(defaultBranch string) string {
remoteRef := "origin/" + defaultBranch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Qualify the remote base ref

When a repository has a local branch named origin/<default> (for example refs/heads/origin/main) in addition to the remote-tracking ref, returning the short origin/main makes the ref passed to git worktree add and git branch --merged ambiguous; I verified git worktree add ... origin/main exits with fatal: ambiguous object name: 'origin/main' in that case. Since this helper has already checked refs/remotes/..., return the fully qualified remote ref internally and keep any shorter name only for display so ygg new and cleanup still work in repos with valid slash-containing branch names.

Useful? React with 👍 / 👎.

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.

Confirmed and fixed in df0de3d. Reproduced your exact failure (fatal: ambiguous object name: 'origin/main') in a test with a local refs/heads/origin/main. BaseRef now returns the fully-qualified ref (refs/remotes/origin/<default> / refs/heads/<default>) so git worktree add and git branch --merged stay unambiguous, and a new DisplayRef helper shortens it back to origin/main for the user-facing "(based on …)" message. Covered by TestBaseRefQualifiesRemoteRef.

BaseRef returned the short "origin/<default>", which is ambiguous when a local
branch literally named "origin/<default>" exists (refs/heads/origin/main vs
refs/remotes/origin/main). git then fails with "fatal: ambiguous object name".

Return the fully-qualified ref (refs/remotes/origin/<default> or
refs/heads/<default>) so git commands stay unambiguous, and add DisplayRef to
shorten it for the user-facing "(based on …)" message.

Claude-Session: https://claude.ai/code/session_01SMjzH78VMHpvo7LdtqRbPE
@joch

joch commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df0de3df3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cli/clean.go
// Find merged branches, measured against the resolved default tip
// (origin/<default> when local is stale) so branches merged upstream are
// detected consistently with how `ygg new` bases new worktrees.
mergedBranches, err := wm.MergedBranches(wm.BaseRef(defaultBranch))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the default branch out of clean candidates

When BaseRef returns a fully-qualified ref here, MergedBranches no longer filters out the default branch itself: for example, running ygg clean from a non-primary worktree checked out on main makes git branch --merged refs/heads/main print * main, which MergedBranches normalizes to main and compares against refs/heads/main. That puts the default branch in mergedSet, so the current default-branch worktree is scheduled for removal; the previous MergedBranches(defaultBranch) call filtered main out.

Useful? React with 👍 / 👎.

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.

Fixed in 61a7d2c. You're right — once merge detection uses a fully-qualified ref, MergedBranches no longer self-filters the default branch. Extracted the candidate selection into selectMergedWorktrees and now skip both the primary worktree and any worktree on the default branch, independent of the merged set. Added TestSelectMergedWorktreesExcludesDefaultBranch (which fails on the old logic, selecting the main worktree, and passes now). IsBranchMerged was unaffected (it returns early for branch == defaultBranch).

Now that merge detection measures against a fully-qualified ref, MergedBranches
no longer self-filters the default branch, so `git branch --merged` reporting
`main` could put a non-primary default-branch worktree into the clean
candidates and schedule it for removal.

Extract the candidate selection into selectMergedWorktrees and skip both the
primary worktree and any worktree on the default branch, regardless of what the
merged set contains. Add a unit test (internal/cli now has coverage).

Claude-Session: https://claude.ai/code/session_01SMjzH78VMHpvo7LdtqRbPE
@joch

joch commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 61a7d2c7e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@joch joch merged commit 2d23fb5 into main Jun 25, 2026
8 checks passed
@joch joch deleted the fetch branch June 25, 2026 20:37
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.

1 participant