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>
This commit is contained in:
Tatsunori Uchino 2023-10-24 00:47:46 +09:00 committed by GitHub
parent f4f0a1dd95
commit 2683aa01ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 28 additions and 4 deletions

View file

@ -2569,6 +2569,11 @@
"default": true,
"description": "%config.useForcePushWithLease%"
},
"git.useForcePushIfIncludes": {
"type": "boolean",
"default": true,
"markdownDescription": "%config.useForcePushIfIncludes%"
},
"git.confirmForcePush": {
"type": "boolean",
"default": true,

View file

@ -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.",

View file

@ -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',

View file

@ -2793,7 +2793,9 @@ export class CommandCenter {
return;
}
forcePushMode = config.get<boolean>('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;
const useForcePushWithLease = config.get<boolean>('useForcePushWithLease') === true;
const useForcePushIfIncludes = config.get<boolean>('useForcePushIfIncludes') === true;
forcePushMode = useForcePushWithLease ? useForcePushIfIncludes ? ForcePushMode.ForceWithLeaseIfIncludes : ForcePushMode.ForceWithLease : ForcePushMode.Force;
if (config.get<boolean>('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';

View file

@ -1910,8 +1910,11 @@ export class Repository {
async push(remote?: string, name?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
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 || '')) {