fix(git): recognize colored stale lease rejections (#36041)

This commit is contained in:
Jinjing
2026-10-29 11:50:37 -07:00
committed by GitHub
parent d12a4b1c5c
commit a4e600a663
2 changed files with 39 additions and 2 deletions
+33
View File
@@ -13,6 +13,39 @@ afterEach(() => {
})
describe('normalizeGitErrorMessage', () => {
it.each([
' ! [rejected] main -> main (stale info)',
'\t!\t[rejected] main -> main (stale info)\t',
' \x1b[31m! [rejected] main -> main (stale info)\x1b[m',
'\x1b[31m ! [rejected] main -> main (stale info)\x1b[0m'
])('preserves stale lease rejection details: %s', (rejection) => {
for (const newline of ['\n', '\r\n']) {
const output = [
'Command failed: git push https://user:secret@example.com/repo.git',
rejection,
"error: failed to push some refs to 'https://token@example.com/repo.git'"
].join(newline)
expect(normalizeGitErrorMessage(new Error(output), 'push')).toBe(
output.replace('user:secret@', '').replace('token@', '')
)
}
})
it('does not classify a rejection marker on a separate line as a stale lease rejection', () => {
const tail = "error: failed to push some refs to 'origin'"
const error = new Error(`Command failed: git push\n !\n [rejected] main (stale info)\n${tail}`)
expect(normalizeGitErrorMessage(error, 'push')).toBe(tail)
})
it('handles push errors containing many blank lines', () => {
const tail = "error: failed to push some refs to 'origin'"
const error = new Error(`Command failed: git push\n${'\n'.repeat(64_000)}${tail}`)
expect(normalizeGitErrorMessage(error, 'push')).toBe(tail)
})
it.each(['remote: stale info', 'hook: stale info'])(
'does not classify unrelated output as a lease rejection: %s',
(output) => {
+6 -2
View File
@@ -1,3 +1,4 @@
import { stripAnsiEscapeSequences } from './ansi-escape-sequences'
import { isPushHookFailure } from './source-control-push-failure'
// Why: strip `user:password@` on any scheme, but a lone `user@` only on HTTP(S) — SSH's git@host user-info is required, so stripping breaks the URL.
@@ -6,7 +7,7 @@ const HTTPS_TOKEN_URL_PATTERN = /(https?:\/\/)[^\s/@:]+@/gi
const SUBMODULE_PUSH_FAILURE_PATTERN = /Unable to push submodule ['"](.+?)['"]/i
const SUBMODULE_PUSH_FAILURE_SENTINEL_PATTERN =
/failed to push all needed submodules|Unable to push submodule/i
const STALE_LEASE_REJECTION_PATTERN = /^\s*!\s+\[rejected\][^\r\n]*\(stale info\)\s*$/m
const STALE_LEASE_REJECTION_PATTERN = /^[ \t]*![ \t]+\[rejected\][^\r\n]*\(stale info\)[ \t]*$/m
const SUBMODULE_REMOTE_CHANGED_PATTERN =
/non-fast-forward|fetch first|updates were rejected|remote contains work that you do not have/i
const NORMALIZED_SUBMODULE_PUSH_FAILURE_PATTERN =
@@ -166,7 +167,10 @@ export function normalizeGitErrorMessage(error: unknown, operation?: GitRemoteOp
return 'Push rejected: remote has newer commits (non-fast-forward). Please pull or sync first.'
}
if (operation === 'push' && (STALE_LEASE_REJECTION_PATTERN.test(raw) || isPushHookFailure(raw))) {
if (
operation === 'push' &&
(STALE_LEASE_REJECTION_PATTERN.test(stripAnsiEscapeSequences(raw)) || isPushHookFailure(raw))
) {
// Lease rejection and pre-push hook details precede Git's generic "failed to push some refs" tail line.
return raw.trim()
}