mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(worktree): keep the claimed-branch fast-forward silent and off sparse creates
Only surface a successful fast-forward from the claim path: a claimed branch with local-only commits is the normal case, and the "not refreshed" toast is infinite-duration and factually wrong there. Skip the probe entirely for --no-checkout (sparse) creates, whose empty index reports staged deletions and would false-positive as a dirty worktree. Refs #15645 Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
This commit is contained in:
@@ -42,7 +42,6 @@ describe('addWorktree', () => {
|
||||
translateWslOutputPathsMock.mockClear()
|
||||
})
|
||||
|
||||
|
||||
it('fast-forwards a checked-out existing branch inside the new worktree (#15645)', async () => {
|
||||
// The branch pre-exists locally, is behind origin, and the new worktree just
|
||||
// checked it out — so the branch is OWNED by /repo-feature now.
|
||||
@@ -71,8 +70,6 @@ describe('addWorktree', () => {
|
||||
{ checkoutExistingBranch: true }
|
||||
)
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('CALLS', JSON.stringify(gitExecFileAsyncMock.mock.calls.map((c) => c[0])))
|
||||
expect(result.localBaseRefRefresh).toMatchObject({ status: 'updated' })
|
||||
const reset = gitExecFileAsyncMock.mock.calls.find(
|
||||
(call) => call[0][0] === 'reset'
|
||||
@@ -81,6 +78,50 @@ describe('addWorktree', () => {
|
||||
expect(reset[1].cwd).toBe('/repo-feature')
|
||||
})
|
||||
|
||||
it('leaves a claimed branch with local-only commits silent (#15645)', async () => {
|
||||
gitExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: '' }) // worktree add /repo-feature feature
|
||||
.mockResolvedValueOnce({ stdout: 'remote-tip\n' }) // resolveWorktreeAddBaseRef existence probe
|
||||
.mockResolvedValueOnce({ stdout: '1\t2\n' }) // rev-list --left-right --count (ahead 1 -> not fast-forwardable)
|
||||
|
||||
const result = await addWorktree(
|
||||
'/repo',
|
||||
'/repo-feature',
|
||||
'feature',
|
||||
'origin/feature',
|
||||
true,
|
||||
false,
|
||||
{ checkoutExistingBranch: true }
|
||||
)
|
||||
|
||||
// Claiming a branch you already have commits on is the normal case, not a warning worth an infinite toast.
|
||||
expect(result.localBaseRefRefresh).toBeUndefined()
|
||||
expect(gitExecFileAsyncMock.mock.calls.find((call) => call[0][0] === 'reset')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips the claim-path refresh for sparse (--no-checkout) creates', async () => {
|
||||
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' }) // worktree add --no-checkout
|
||||
|
||||
const result = await addWorktree(
|
||||
'/repo',
|
||||
'/repo-feature',
|
||||
'feature',
|
||||
'origin/feature',
|
||||
true,
|
||||
true,
|
||||
{ checkoutExistingBranch: true }
|
||||
)
|
||||
|
||||
// A --no-checkout worktree has an empty index, so probing it would report a bogus dirty status.
|
||||
expect(result.localBaseRefRefresh).toBeUndefined()
|
||||
expect(gitExecFileAsyncMock.mock.calls).toEqual([
|
||||
[
|
||||
['worktree', 'add', '--no-checkout', '/repo-feature', 'feature'],
|
||||
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
|
||||
]
|
||||
])
|
||||
})
|
||||
|
||||
it('fast-forwards with reset --hard when localBranch is checked out in primary worktree', async () => {
|
||||
const worktreeListOutput =
|
||||
'worktree /repo\nHEAD abc123\nbranch refs/heads/main\n\nworktree /repo-other\nHEAD def456\nbranch refs/heads/feature\n'
|
||||
|
||||
@@ -936,7 +936,9 @@ async function refreshLocalBaseRefForWorktreeCreate(
|
||||
* @param baseBranch - Optional base branch to create from (defaults to HEAD)
|
||||
* @remarks Side effects (best-effort, warn-only): passes `--no-track`, writes
|
||||
* `branch.<branch>.base` for new-branch worktrees with a base ref, and may
|
||||
* write `push.autoSetupRemote=true` to the repo's shared config.
|
||||
* write `push.autoSetupRemote=true` to the repo's shared config. When
|
||||
* `checkoutExistingBranch` and `refreshLocalBaseRef` are both set, the claimed
|
||||
* branch itself may be fast-forwarded inside the new worktree after the add.
|
||||
*/
|
||||
export async function addWorktree(
|
||||
repoPath: string,
|
||||
@@ -1018,22 +1020,21 @@ async function performAddWorktree(
|
||||
})
|
||||
|
||||
if (options.checkoutExistingBranch) {
|
||||
// Why (#15645): the just-created worktree owns the branch now, so the
|
||||
// refresh's owner path fast-forwards it in place (reset --hard inside the
|
||||
// owning worktree is the one mutation git permits here). Skipping this
|
||||
// left a pre-existing local branch checked out at its own stale commit —
|
||||
// with no upstream, nothing ever signalled the staleness.
|
||||
if (refreshLocalBaseRef && baseBranch) {
|
||||
// Why (#15645): run after the add — the new worktree owns the branch now, so the refresh's owner path can reset --hard it in place.
|
||||
// Why !noCheckout: a sparse create's index is still empty here, so the clean-status probe would false-positive dirty.
|
||||
if (refreshLocalBaseRef && baseBranch && !noCheckout) {
|
||||
const effectiveExistingBase = await resolveWorktreeAddBaseRef(baseBranch, (qualifiedRef) =>
|
||||
hasWorktreeBaseCommitRef(repoPath, qualifiedRef, options)
|
||||
)
|
||||
localBaseRefRefresh = await refreshLocalBaseRefForWorktreeCreate(
|
||||
const claimedRefresh = await refreshLocalBaseRefForWorktreeCreate(
|
||||
repoPath,
|
||||
baseBranch,
|
||||
effectiveExistingBase,
|
||||
options.remoteTrackingBase,
|
||||
options
|
||||
)
|
||||
// Why: the user deliberately claimed this branch — local-only commits are expected, so only report a successful fast-forward instead of an infinite "not refreshed" warning this path never used to raise.
|
||||
localBaseRefRefresh = claimedRefresh?.status === 'updated' ? claimedRefresh : undefined
|
||||
}
|
||||
return localBaseRefRefresh ? { localBaseRefRefresh } : {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user