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:
Jinjing
2026-07-13 01:39:31 -07:00
parent e98bfd67c1
commit 3d557fc939
6 changed files with 202 additions and 31 deletions
+25
View File
@@ -20,6 +20,15 @@ const NORMALIZED_SUBMODULE_PUSH_FAILURE_PATTERN =
/(?:^|:\s)((?:Submodule '[^'\n]+'|A submodule) (?:has remote changes\. Pull inside the submodule, then try again\.|could not be pushed\. Resolve the submodule push error, then try again\.))(?:$|\s)/i
const DIVERGENT_PULL_RECONCILIATION_PATTERN =
/Need to specify how to reconcile divergent branches|divergent branches and need to specify how to reconcile them/i
// Why: any of these already pin a reconciliation strategy, so the merge
// fallback must not override an explicit caller/user choice (e.g. --ff-only).
const RECONCILIATION_PULL_ARG_PATTERN =
/^(--rebase|--no-rebase|--ff-only|--ff|--no-ff|--merge|-r)(=|$)/
// Why: merge is Git's historical pre-2.27 default. Falling back to it lets
// divergent pulls reconcile on fresh hosts that never configured pull.rebase
// or pull.ff, instead of failing outright. `--no-rebase` predates the 2.25
// baseline, so it is safe across all supported Git binaries.
export const MERGE_RECONCILIATION_PULL_ARGS = ['--no-rebase']
export function stripCredentialsFromMessage(message: string): string {
return message.replace(USERPASS_URL_PATTERN, '$1').replace(HTTPS_TOKEN_URL_PATTERN, '$1')
@@ -81,6 +90,22 @@ function* iterateLinesFromEnd(value: string): Generator<string> {
yield value.slice(0, lineEnd)
}
// Why: a fresh host may lack any pull.rebase/pull.ff policy, so Git 2.27+
// refuses to reconcile divergent branches. Detect that specific failure so the
// caller can retry with an explicit merge instead of surfacing a config error.
export function isDivergentPullReconciliationError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false
}
return DIVERGENT_PULL_RECONCILIATION_PATTERN.test(stripCredentialsFromMessage(error.message))
}
// Whether the pull already specifies how to reconcile (rebase/merge/ff-only),
// in which case the caller's choice must win over the merge fallback.
export function pullArgsSpecifyReconciliation(pullArgs: string[]): boolean {
return pullArgs.some((arg) => RECONCILIATION_PULL_ARG_PATTERN.test(arg))
}
export type GitRemoteOperation = 'push' | 'pull' | 'fetch' | 'upstream'
export function normalizeGitErrorMessage(error: unknown, operation?: GitRemoteOperation): string {