Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for --force-if-includes to force push more safely #187932

Merged
merged 8 commits into from Oct 23, 2023
5 changes: 5 additions & 0 deletions extensions/git/package.json
Expand Up @@ -2568,6 +2568,11 @@
"default": true,
"description": "%config.useForcePushWithLease%"
},
"git.useForcePushIfIncludes": {
"type": "boolean",
"default": true,
"markdownDescription": "%config.useForcePushIfIncludes%"
},
"git.confirmForcePush": {
"type": "boolean",
"default": true,
Expand Down
1 change: 1 addition & 0 deletions extensions/git/package.nls.json
Expand Up @@ -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.",
lszomoru marked this conversation as resolved.
Show resolved Hide resolved
"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.",
Expand Down
5 changes: 4 additions & 1 deletion extensions/git/src/api/git.d.ts
Expand Up @@ -16,7 +16,8 @@ export interface InputBox {

export const enum ForcePushMode {
Force,
ForceWithLease
ForceWithLease,
ForceWithLeaseIfIncludes,
}

export const enum RefType {
Expand Down Expand Up @@ -359,6 +360,8 @@ export const enum GitErrorCodes {
StashConflict = 'StashConflict',
UnmergedChanges = 'UnmergedChanges',
PushRejected = 'PushRejected',
ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected',
ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected',
lszomoru marked this conversation as resolved.
Show resolved Hide resolved
RemoteConnectionError = 'RemoteConnectionError',
DirtyWorkTree = 'DirtyWorkTree',
CantOpenResource = 'CantOpenResource',
Expand Down
8 changes: 7 additions & 1 deletion extensions/git/src/commands.ts
Expand Up @@ -2789,7 +2789,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?');
Expand Down Expand Up @@ -3658,6 +3660,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.');
lszomoru marked this conversation as resolved.
Show resolved Hide resolved
break;
case GitErrorCodes.Conflict:
message = l10n.t('There are merge conflicts. Resolve them before committing.');
type = 'warning';
Expand Down
13 changes: 11 additions & 2 deletions extensions/git/src/git.ts
Expand Up @@ -1915,8 +1915,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');
}
Expand Down Expand Up @@ -1945,7 +1948,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 || '')) {
Expand Down