From 907a021f748219cf000d3c84fe87af415c8367b4 Mon Sep 17 00:00:00 2001 From: nesukun Date: Thu, 28 Jun 2018 23:09:31 +0100 Subject: [PATCH] Add support for force push and force-with-lease --- extensions/git/package.json | 42 +++++++++++ extensions/git/package.nls.json | 8 +- extensions/git/src/api/git.d.ts | 5 ++ extensions/git/src/commands.ts | 123 ++++++++++++++++++++----------- extensions/git/src/git.ts | 10 ++- extensions/git/src/repository.ts | 14 ++-- 6 files changed, 149 insertions(+), 53 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 2e0d5985172..ed83ea05fcc 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -275,16 +275,31 @@ "title": "%command.push%", "category": "Git" }, + { + "command": "git.pushForce", + "title": "%command.pushForce%", + "category": "Git" + }, { "command": "git.pushTo", "title": "%command.pushTo%", "category": "Git" }, + { + "command": "git.pushToForce", + "title": "%command.pushToForce%", + "category": "Git" + }, { "command": "git.pushWithTags", "title": "%command.pushWithTags%", "category": "Git" }, + { + "command": "git.pushWithTagsForce", + "title": "%command.pushWithTagsForce%", + "category": "Git" + }, { "command": "git.sync", "title": "%command.sync%", @@ -493,14 +508,26 @@ "command": "git.push", "when": "config.git.enabled && gitOpenRepositoryCount != 0" }, + { + "command": "git.pushForce", + "when": "config.git.enabled && config.git.allowForcePush && gitOpenRepositoryCount != 0" + }, { "command": "git.pushTo", "when": "config.git.enabled && gitOpenRepositoryCount != 0" }, + { + "command": "git.pushToForce", + "when": "config.git.enabled && config.git.allowForcePush && gitOpenRepositoryCount != 0" + }, { "command": "git.pushWithTags", "when": "config.git.enabled && gitOpenRepositoryCount != 0" }, + { + "command": "git.pushWithTagsForce", + "when": "config.git.enabled && config.git.allowForcePush && gitOpenRepositoryCount != 0" + }, { "command": "git.sync", "when": "config.git.enabled && gitOpenRepositoryCount != 0" @@ -1045,6 +1072,21 @@ "default": [], "scope": "window", "description": "%config.ignoredRepositories%" + }, + "git.allowForcePush": { + "type": "boolean", + "default": false, + "description": "%config.allowForcePush%" + }, + "git.useForceWithLease": { + "type": "boolean", + "default": true, + "description": "%config.useForceWithLease%" + }, + "git.dontAskForcePushConfirmation": { + "type": "boolean", + "default": false, + "description": "%config.dontAskForcePushConfirmation%" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index bc4c812d75b..23371ad264f 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -39,8 +39,11 @@ "command.pullRebase": "Pull (Rebase)", "command.pullFrom": "Pull from...", "command.push": "Push", + "command.pushForce": "Push (Force)", "command.pushTo": "Push to...", + "command.pushToForce": "Push to... (Force)", "command.pushWithTags": "Push With Tags", + "command.pushWithTagsForce": "Push With Tags (Force)", "command.sync": "Sync", "command.syncRebase": "Sync (Rebase)", "command.publish": "Publish Branch", @@ -83,10 +86,13 @@ "config.showPushSuccessNotification": "Controls whether to show a notification when a push is successful.", "config.inputValidation": "Controls when to show commit message input validation.", "config.detectSubmodules": "Controls whether to automatically detect git submodules.", - "colors.added": "Color for added resources.", "config.detectSubmodulesLimit": "Controls the limit of git submodules detected.", "config.alwaysSignOff": "Controls the signoff flag for all commits.", "config.ignoredRepositories": "List of git repositories to ignore.", + "config.allowForcePush": "Controls whether force push (with or without lease) is enabled.", + "config.useForceWithLease": "Controls whether force pushing uses the safer force-with-lease variant.", + "config.dontAskForcePushConfirmation": "Controls whether to ask for confirmation before force-pushing.", + "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", "colors.untracked": "Color for untracked resources.", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index b86b263b940..90713d54442 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -19,6 +19,11 @@ export const enum RefType { Tag } +export enum ForcePushMode { + Force, + ForceWithLease, +} + export interface Ref { readonly type: RefType; readonly name?: string; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index e4b5320a561..8803e2e2e75 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -17,7 +17,7 @@ import { lstat, Stats } from 'fs'; import * as os from 'os'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; -import { Ref, RefType, Branch, GitErrorCodes } from './api/git'; +import { Ref, RefType, Branch, GitErrorCodes, ForcePushMode } from './api/git'; const localize = nls.loadMessageBundle(); @@ -154,6 +154,17 @@ async function categorizeResourceByResolution(resources: Resource[]): Promise<{ return { merge, resolved, unresolved }; } +enum PushType { + Push, + PushTo, + PushTags, +} + +interface PushOptions { + pushType: PushType; + forcePush?: boolean; +} + export class CommandCenter { private disposables: Disposable[]; @@ -1414,76 +1425,100 @@ export class CommandCenter { await repository.pullWithRebase(repository.HEAD); } - @command('git.push', { repository: true }) - async push(repository: Repository): Promise { + private async pushWithOptions(repository: Repository, pushOptions: PushOptions) { const remotes = repository.remotes; + const config = workspace.getConfiguration('git'); + const forcePushMode = pushOptions.forcePush && config.get('useForceWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force; + + if (pushOptions.forcePush && config.get('dontAskForcePushConfirmation') === false) { + const message = localize('confirm force push', "You are about to force push your changes, this can be destructive and could inadvertedly overwrite changes made by others.\n\nAre you sure to continue?"); + const yes = localize('ok', "OK"); + const dontAsk = localize('dontAsk', "OK, do not ask me again"); + const pick = await window.showWarningMessage(message, { modal: true }, yes, dontAsk); + + if (pick === dontAsk) { + config.update('dontAskForcePushConfirmation', true, true); + } else if (pick !== yes) { + return; + } + } if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); return; } + if (pushOptions.pushType === PushType.PushTags) { + await repository.pushTags(undefined, forcePushMode); + + window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags.")); + return; + } + if (!repository.HEAD || !repository.HEAD.name) { window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote.")); return; } - try { - await repository.push(repository.HEAD); - } catch (err) { - if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) { - throw err; - } + if (pushOptions.pushType === PushType.Push) { + try { + await repository.push(repository.HEAD, forcePushMode); + } catch (err) { + if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) { + throw err; + } + const branchName = repository.HEAD.name; + const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName); + const yes = localize('ok', "OK"); + const pick = await window.showWarningMessage(message, { modal: true }, yes); + + if (pick === yes) { + await this.publish(repository); + } + } + } else { const branchName = repository.HEAD.name; - const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName); - const yes = localize('ok', "OK"); - const pick = await window.showWarningMessage(message, { modal: true }, yes); + const picks = remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl! })); + const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName); + const pick = await window.showQuickPick(picks, { placeHolder }); - if (pick === yes) { - await this.publish(repository); + if (!pick) { + return; } + + await repository.pushTo(pick.label, branchName, undefined, forcePushMode); } } + @command('git.push', { repository: true }) + async push(repository: Repository): Promise { + await this.pushWithOptions(repository, { pushType: PushType.Push }); + } + + @command('git.pushForce', { repository: true }) + async pushForce(repository: Repository): Promise { + await this.pushWithOptions(repository, { pushType: PushType.Push, forcePush: true }); + } + @command('git.pushWithTags', { repository: true }) async pushWithTags(repository: Repository): Promise { - const remotes = repository.remotes; + await this.pushWithOptions(repository, { pushType: PushType.PushTags }); + } - if (remotes.length === 0) { - window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); - return; - } - - await repository.pushTags(); - - window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags.")); + @command('git.pushWithTagsForce', { repository: true }) + async pushWithTagsForce(repository: Repository): Promise { + await this.pushWithOptions(repository, { pushType: PushType.PushTags, forcePush: true }); } @command('git.pushTo', { repository: true }) async pushTo(repository: Repository): Promise { - const remotes = repository.remotes; + await this.pushWithOptions(repository, { pushType: PushType.PushTo }); + } - if (remotes.length === 0) { - window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); - return; - } - - if (!repository.HEAD || !repository.HEAD.name) { - window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote.")); - return; - } - - const branchName = repository.HEAD.name; - const picks = remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl! })); - const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName); - const pick = await window.showQuickPick(picks, { placeHolder }); - - if (!pick) { - return; - } - - await repository.pushTo(pick.label, branchName); + @command('git.pushToForce', { repository: true }) + async pushToForce(repository: Repository): Promise { + await this.pushWithOptions(repository, { pushType: PushType.PushTo, forcePush: true }); } private async _sync(repository: Repository, rebase: boolean): Promise { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 3069b8fd77b..1538521407a 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1181,9 +1181,17 @@ export class Repository { } } - async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false): Promise { + async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise { const args = ['push']; + if (forcePushMode) { + if (forcePushMode === ForcePushMode.ForceWithLease) { + args.push('--force-with-lease'); + } else if (forcePushMode === ForcePushMode.Force) { + args.push('--force'); + } + } + if (setUpstream) { args.push('-u'); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 3ef39573c56..73ffe83e1ef 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -15,7 +15,7 @@ import * as path from 'path'; import * as nls from 'vscode-nls'; import * as fs from 'fs'; import { StatusBarCommands } from './statusbar'; -import { Branch, Ref, Remote, RefType, GitErrorCodes } from './api/git'; +import { Branch, Ref, Remote, RefType, GitErrorCodes, ForcePushMode } from './api/git'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -895,7 +895,7 @@ export class Repository implements Disposable { } @throttle - async push(head: Branch): Promise { + async push(head: Branch, forcePushMode?: ForcePushMode): Promise { let remote: string | undefined; let branch: string | undefined; @@ -904,15 +904,15 @@ export class Repository implements Disposable { branch = `${head.name}:${head.upstream.name}`; } - await this.run(Operation.Push, () => this.repository.push(remote, branch)); + await this.run(Operation.Push, () => this.repository.push(remote, branch, undefined, undefined, forcePushMode)); } - async pushTo(remote?: string, name?: string, setUpstream: boolean = false): Promise { - await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream)); + async pushTo(remote?: string, name?: string, setUpstream: boolean = false, forcePushMode?: ForcePushMode): Promise { + await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream, undefined, forcePushMode)); } - async pushTags(remote?: string): Promise { - await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true)); + async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise { + await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true, forcePushMode)); } @throttle