mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 00:03:55 +00:00
Fall back to a merge when a divergent pull has no reconciliation strateg
- Git 2.27+ refuses `git pull` on divergent branches unless pull.rebase or pull.ff is configured. Retry with `--no-rebase` (Git's historical default) so pulls succeed out of the box on fresh hosts. - Skip the fallback whenever the caller already specified a reconciliation strategy (e.g. --ff-only, --rebase) so explicit policies still fail as expected on divergence. - Applied identically in the local git pull path and the relay/SSH git handler so both surfaces behave the same way.
This commit is contained in:
@@ -363,6 +363,45 @@ describe('git remote operations', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('retries a divergent pull as a merge when no strategy is configured', async () => {
|
||||
const divergentError = new Error(
|
||||
'Command failed: git pull\n' + 'fatal: Need to specify how to reconcile divergent branches.'
|
||||
)
|
||||
gitExecFileAsyncMock
|
||||
// First attempt: plain pull rejects with git's reconciliation error.
|
||||
.mockResolvedValueOnce({ stdout: 'feature\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'origin/feature\n', stderr: '' })
|
||||
.mockRejectedValueOnce(divergentError)
|
||||
// Fallback attempt: pull --no-rebase (merge) succeeds.
|
||||
.mockResolvedValueOnce({ stdout: 'feature\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'origin/feature\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
||||
|
||||
await gitPull('/repo')
|
||||
|
||||
expect(gitExecFileAsyncMock.mock.calls).toEqual([
|
||||
[['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd: '/repo' }],
|
||||
[['rev-parse', '--abbrev-ref', 'HEAD@{u}'], { cwd: '/repo' }],
|
||||
[['pull'], { cwd: '/repo' }],
|
||||
[['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd: '/repo' }],
|
||||
[['rev-parse', '--abbrev-ref', 'HEAD@{u}'], { cwd: '/repo' }],
|
||||
[['pull', '--no-rebase'], { cwd: '/repo' }]
|
||||
])
|
||||
})
|
||||
|
||||
it('does not retry a fast-forward-only pull that fails on divergence', async () => {
|
||||
gitExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: 'feature\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'origin/feature\n', stderr: '' })
|
||||
.mockRejectedValueOnce(
|
||||
new Error('Command failed: git pull\nfatal: Not possible to fast-forward, aborting.')
|
||||
)
|
||||
|
||||
await expect(gitFastForward('/repo')).rejects.toThrow('Not possible to fast-forward')
|
||||
// No fallback attempt: only the three probe/pull calls ran.
|
||||
expect(gitExecFileAsyncMock.mock.calls).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('pulls the same-name origin branch for legacy base-tracking worktrees', async () => {
|
||||
gitExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: 'feature\n', stderr: '' })
|
||||
|
||||
+26
-5
@@ -1,4 +1,9 @@
|
||||
import { normalizeGitErrorMessage } from '../../shared/git-remote-error'
|
||||
import {
|
||||
MERGE_RECONCILIATION_PULL_ARGS,
|
||||
isDivergentPullReconciliationError,
|
||||
normalizeGitErrorMessage,
|
||||
pullArgsSpecifyReconciliation
|
||||
} from '../../shared/git-remote-error'
|
||||
import { resolveEffectiveGitUpstream } from '../../shared/git-effective-upstream'
|
||||
import { gitRefTargetsBranchOnRemote } from '../../shared/git-remote-branch-name'
|
||||
import { resolveGitRemoteRebaseSource } from '../../shared/git-rebase-source'
|
||||
@@ -214,11 +219,11 @@ async function gitPullWithArgs(
|
||||
pushTarget?: GitPushTarget,
|
||||
options: GitRuntimeOptions = {}
|
||||
): Promise<void> {
|
||||
try {
|
||||
const runPull = async (effectiveArgs: string[]): Promise<void> => {
|
||||
if (pushTarget) {
|
||||
const target = await validateGitPushTarget(worktreePath, pushTarget, options)
|
||||
await gitExecFileAsync(
|
||||
['pull', ...pullArgs, target.remoteName, target.branchName],
|
||||
['pull', ...effectiveArgs, target.remoteName, target.branchName],
|
||||
gitOptionsForWorktree(worktreePath, options)
|
||||
)
|
||||
return
|
||||
@@ -230,13 +235,29 @@ async function gitPullWithArgs(
|
||||
// Why: legacy Orca branches may still track origin/main while pushes
|
||||
// target origin/<branch>. Pull the same effective branch the UI reports.
|
||||
await gitExecFileAsync(
|
||||
['pull', ...pullArgs, upstream.remoteName, upstream.branchName],
|
||||
['pull', ...effectiveArgs, upstream.remoteName, upstream.branchName],
|
||||
gitOptionsForWorktree(worktreePath, options)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await gitExecFileAsync(['pull', ...pullArgs], gitOptionsForWorktree(worktreePath, options))
|
||||
await gitExecFileAsync(['pull', ...effectiveArgs], gitOptionsForWorktree(worktreePath, options))
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
await runPull(pullArgs)
|
||||
} catch (error) {
|
||||
// Why: on hosts with no pull.rebase/pull.ff policy, Git 2.27+ refuses to
|
||||
// reconcile divergent branches. Retry as a merge (Git's historical
|
||||
// default) so pulls succeed out of the box; callers that already forced a
|
||||
// strategy — or users who configured rebase — never reach this fallback.
|
||||
if (!pullArgsSpecifyReconciliation(pullArgs) && isDivergentPullReconciliationError(error)) {
|
||||
await runPull([...MERGE_RECONCILIATION_PULL_ARGS, ...pullArgs])
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(normalizeGitErrorMessage(error, 'pull'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user