From aef904649346f4b9fc7354a3c4fd80504db716b7 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 29 Oct 2023 15:58:11 -0400 Subject: [PATCH] Honors VS Code git force push settings --- src/commands/git/push.ts | 2 +- src/constants.ts | 1 + src/env/node/git/git.ts | 37 ++++++++++++++++++++++++++++++++---- src/env/node/git/localGitProvider.ts | 20 +++++++++++++++++-- src/git/errors.ts | 27 +++++++++++++++++--------- 5 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/commands/git/push.ts b/src/commands/git/push.ts index 9056860..7b9f570 100644 --- a/src/commands/git/push.ts +++ b/src/commands/git/push.ts @@ -157,7 +157,7 @@ export class PushGitCommand extends QuickCommand { private async *confirmStep(state: PushStepState, context: Context): AsyncStepResultGenerator { const useForceWithLease = - configuration.getAny('git.useForcePushWithLease') ?? false; + configuration.getAny('git.useForcePushWithLease') ?? true; let step: QuickPickStep>; diff --git a/src/constants.ts b/src/constants.ts index 8fcfa72..67859a2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -664,6 +664,7 @@ export type CoreGitConfiguration = | 'git.pullTags' | 'git.repositoryScanIgnoredFolders' | 'git.repositoryScanMaxDepth' + | 'git.useForcePushIfIncludes' | 'git.useForcePushWithLease'; export const enum GlyphChars { diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index 26afaf1..fc212df 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -143,6 +143,7 @@ function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [nu } type ExitCodeOnlyGitCommandOptions = GitCommandOptions & { exitCodeOnly: true }; +export type PushForceOptions = { withLease: true; ifIncludes?: boolean } | { withLease: false; ifIncludes?: never }; export class Git { /** Map of running git commands -- avoids running duplicate overlaping commands */ @@ -881,12 +882,27 @@ export class Git { async push( repoPath: string, - options: { branch?: string; force?: boolean; publish?: boolean; remote?: string; upstream?: string }, + options: { + branch?: string; + force?: PushForceOptions; + publish?: boolean; + remote?: string; + upstream?: string; + }, ): Promise { const params = ['push']; - if (options.force) { - params.push('--force'); + if (options.force != null) { + if (options.force.withLease) { + params.push('--force-with-lease'); + if (options.force.ifIncludes) { + if (await this.isAtLeastVersion('2.30.0')) { + params.push('--force-if-includes'); + } + } + } else { + params.push('--force'); + } } if (options.branch && options.remote) { @@ -911,7 +927,20 @@ export class Git { } else if (GitWarnings.tipBehind.test(msg) || GitWarnings.tipBehind.test(ex.stderr ?? '')) { reason = PushErrorReason.TipBehind; } else if (GitErrors.pushRejected.test(msg) || GitErrors.pushRejected.test(ex.stderr ?? '')) { - reason = PushErrorReason.PushRejected; + if (options?.force?.withLease) { + if (/! \[rejected\].*\(stale info\)/m.test(ex.stderr || '')) { + reason = PushErrorReason.PushRejected; + } else if ( + options.force.ifIncludes && + /! \[rejected\].*\(remote ref updated since checkout\)/m.test(ex.stderr || '') + ) { + reason = PushErrorReason.PushRejected; + } else { + reason = PushErrorReason.PushRejected; + } + } else { + reason = PushErrorReason.PushRejected; + } } else if (GitErrors.permissionDenied.test(msg) || GitErrors.permissionDenied.test(ex.stderr ?? '')) { reason = PushErrorReason.PermissionDenied; } else if (GitErrors.remoteConnection.test(msg) || GitErrors.remoteConnection.test(ex.stderr ?? '')) { diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 467314a..d8150a1 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -182,7 +182,7 @@ import { serializeWebviewItemContext } from '../../../system/webview'; import type { CachedBlame, CachedDiff, CachedLog } from '../../../trackers/gitDocumentTracker'; import { GitDocumentState } from '../../../trackers/gitDocumentTracker'; import type { TrackedDocument } from '../../../trackers/trackedDocument'; -import type { Git } from './git'; +import type { Git, PushForceOptions } from './git'; import { getShaInLogRegex, GitErrors, @@ -1246,12 +1246,28 @@ export class LocalGitProvider implements GitProvider, Disposable { return undefined; } + let forceOpts: PushForceOptions | undefined; + if (options?.force) { + const withLease = configuration.getAny('git.useForcePushWithLease') ?? true; + if (withLease) { + forceOpts = { + withLease: withLease, + ifIncludes: + configuration.getAny('git.useForcePushIfIncludes') ?? true, + }; + } else { + forceOpts = { + withLease: withLease, + }; + } + } + try { await this.git.push(repoPath, { branch: branchName, remote: options?.publish ? options.publish.remote : remoteName, upstream: getBranchTrackingWithoutRemote(branch), - force: options?.force, + force: forceOpts, publish: options?.publish != null, }); diff --git a/src/git/errors.ts b/src/git/errors.ts index de9639c..6b61162 100644 --- a/src/git/errors.ts +++ b/src/git/errors.ts @@ -102,12 +102,14 @@ export class StashPushError extends Error { export const enum PushErrorReason { RemoteAhead = 1, - TipBehind = 2, - PushRejected = 3, - PermissionDenied = 4, - RemoteConnection = 5, - NoUpstream = 6, - Other = 7, + TipBehind, + PushRejected, + PushRejectedWithLease, + PushRejectedWithLeaseIfIncludes, + PermissionDenied, + RemoteConnection, + NoUpstream, + Other, } export class PushError extends Error { @@ -136,15 +138,22 @@ export class PushError extends Error { reason = undefined; } else { reason = messageOrReason; + switch (reason) { case PushErrorReason.RemoteAhead: - message = `${baseMessage} because the remote contains work that you do not have locally. Try doing a fetch first.`; + message = `${baseMessage} because the remote contains work that you do not have locally. Try fetching first.`; break; case PushErrorReason.TipBehind: - message = `${baseMessage} as it is behind its remote counterpart. Try doing a pull first.`; + message = `${baseMessage} as it is behind its remote counterpart. Try pulling first.`; break; case PushErrorReason.PushRejected: - message = `${baseMessage} because some refs failed to push or the push was rejected.`; + message = `${baseMessage} because some refs failed to push or the push was rejected. Try pulling first.`; + break; + case PushErrorReason.PushRejectedWithLease: + case PushErrorReason.PushRejectedWithLeaseIfIncludes: + message = `Unable to force push${branch ? ` branch '${branch}'` : ''}${ + remote ? ` to ${remote}` : '' + } because some refs failed to push or the push was rejected. The tip of the remote-tracking branch has been updated since the last checkout. Try pulling first.`; break; case PushErrorReason.PermissionDenied: message = `${baseMessage} because you don't have permission to push to this remote repository.`;