From 2683aa01ac250fdb042d130e816db93c3b3d3468 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Tue, 24 Oct 2023 00:47:46 +0900 Subject: [PATCH] Add support for `--force-if-includes` to force push more safely (#187932) * Add support for `--force-if-includes` to force push * Change force push failed error message * Separate force push (no with lease) failed error message * Switch to `"markdownDescription"` * Add Git version requirement for config description * Improve error message when safer force push is rejected * Eliminate the option's effect if Git is too old * Minor improvements to community contribution --------- Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- extensions/git/package.json | 5 +++++ extensions/git/package.nls.json | 1 + extensions/git/src/api/git.d.ts | 5 ++++- extensions/git/src/commands.ts | 8 +++++++- extensions/git/src/git.ts | 13 +++++++++++-- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index b981bbad747..c7f64e2fecd 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2569,6 +2569,11 @@ "default": true, "description": "%config.useForcePushWithLease%" }, + "git.useForcePushIfIncludes": { + "type": "boolean", + "default": true, + "markdownDescription": "%config.useForcePushIfIncludes%" + }, "git.confirmForcePush": { "type": "boolean", "default": true, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 8081c900393..20a807be2cb 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -217,6 +217,7 @@ "config.autoStash": "Stash any changes before pulling and restore them after successful pull.", "config.allowForcePush": "Controls whether force push (with or without lease) is enabled.", "config.useForcePushWithLease": "Controls whether force pushing uses the safer force-with-lease variant.", + "config.useForcePushIfIncludes": "Controls whether force pushing uses the safer force-if-includes variant. Note: This setting requires the `#git.useForcePushWithLease#` setting to be enabled, and Git version `2.30.0` or later.", "config.confirmForcePush": "Controls whether to ask for confirmation before force-pushing.", "config.allowNoVerifyCommit": "Controls whether commits without running pre-commit and commit-msg hooks are allowed.", "config.confirmNoVerifyCommit": "Controls whether to ask for confirmation before committing without verification.", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 191a74e125b..1eb99ff0329 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -16,7 +16,8 @@ export interface InputBox { export const enum ForcePushMode { Force, - ForceWithLease + ForceWithLease, + ForceWithLeaseIfIncludes, } export const enum RefType { @@ -366,6 +367,8 @@ export const enum GitErrorCodes { StashConflict = 'StashConflict', UnmergedChanges = 'UnmergedChanges', PushRejected = 'PushRejected', + ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected', + ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected', RemoteConnectionError = 'RemoteConnectionError', DirtyWorkTree = 'DirtyWorkTree', CantOpenResource = 'CantOpenResource', diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 9b6b93d4078..f6b625a71de 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2793,7 +2793,9 @@ export class CommandCenter { return; } - forcePushMode = config.get('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force; + const useForcePushWithLease = config.get('useForcePushWithLease') === true; + const useForcePushIfIncludes = config.get('useForcePushIfIncludes') === true; + forcePushMode = useForcePushWithLease ? useForcePushIfIncludes ? ForcePushMode.ForceWithLeaseIfIncludes : ForcePushMode.ForceWithLease : ForcePushMode.Force; if (config.get('confirmForcePush')) { const message = l10n.t('You are about to force push your changes, this can be destructive and could inadvertently overwrite changes made by others.\n\nAre you sure to continue?'); @@ -3682,6 +3684,10 @@ export class CommandCenter { case GitErrorCodes.PushRejected: message = l10n.t('Can\'t push refs to remote. Try running "Pull" first to integrate your changes.'); break; + case GitErrorCodes.ForcePushWithLeaseRejected: + case GitErrorCodes.ForcePushWithLeaseIfIncludesRejected: + message = l10n.t('Can\'t force push refs to remote. The tip of the remote-tracking branch has been updated since the last checkout. Try running "Pull" first to pull the latest changes from the remote branch first.'); + break; case GitErrorCodes.Conflict: message = l10n.t('There are merge conflicts. Resolve them before committing.'); type = 'warning'; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 17e17c8c7f9..421cc14b752 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1910,8 +1910,11 @@ export class Repository { async push(remote?: string, name?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise { const args = ['push']; - if (forcePushMode === ForcePushMode.ForceWithLease) { + if (forcePushMode === ForcePushMode.ForceWithLease || forcePushMode === ForcePushMode.ForceWithLeaseIfIncludes) { args.push('--force-with-lease'); + if (forcePushMode === ForcePushMode.ForceWithLeaseIfIncludes && this._git.compareGitVersionTo('2.30') !== -1) { + args.push('--force-if-includes'); + } } else if (forcePushMode === ForcePushMode.Force) { args.push('--force'); } @@ -1940,7 +1943,13 @@ export class Repository { await this.exec(args, { env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent } }); } catch (err) { if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) { - err.gitErrorCode = GitErrorCodes.PushRejected; + if (forcePushMode === ForcePushMode.ForceWithLease && /! \[rejected\].*\(stale info\)/m.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.ForcePushWithLeaseRejected; + } else if (forcePushMode === ForcePushMode.ForceWithLeaseIfIncludes && /! \[rejected\].*\(remote ref updated since checkout\)/m.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.ForcePushWithLeaseIfIncludesRejected; + } else { + err.gitErrorCode = GitErrorCodes.PushRejected; + } } else if (/Permission.*denied/.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.PermissionDenied; } else if (/Could not read from remote repository/.test(err.stderr || '')) {