Skip to content

Add support for force push and force-with-lease #53286

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

Merged
merged 1 commit into from
Sep 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions extensions/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%",
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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%"
}
}
},
Expand Down
8 changes: 7 additions & 1 deletion extensions/git/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
5 changes: 5 additions & 0 deletions extensions/git/src/api/git.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const enum RefType {
Tag
}

export enum ForcePushMode {
Force,
ForceWithLease,
}

export interface Ref {
readonly type: RefType;
readonly name?: string;
Expand Down
123 changes: 79 additions & 44 deletions extensions/git/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -1414,76 +1425,100 @@ export class CommandCenter {
await repository.pullWithRebase(repository.HEAD);
}

@command('git.push', { repository: true })
async push(repository: Repository): Promise<void> {
private async pushWithOptions(repository: Repository, pushOptions: PushOptions) {
const remotes = repository.remotes;
const config = workspace.getConfiguration('git');
const forcePushMode = pushOptions.forcePush && config.get<boolean>('useForceWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;

if (pushOptions.forcePush && config.get<boolean>('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.pushWithTags', { repository: true })
async pushWithTags(repository: Repository): Promise<void> {
const remotes = repository.remotes;
@command('git.push', { repository: true })
async push(repository: Repository): Promise<void> {
await this.pushWithOptions(repository, { pushType: PushType.Push });
}

if (remotes.length === 0) {
window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
return;
}
@command('git.pushForce', { repository: true })
async pushForce(repository: Repository): Promise<void> {
await this.pushWithOptions(repository, { pushType: PushType.Push, forcePush: true });
}

await repository.pushTags();
@command('git.pushWithTags', { repository: true })
async pushWithTags(repository: Repository): Promise<void> {
await this.pushWithOptions(repository, { pushType: PushType.PushTags });
}

window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags."));
@command('git.pushWithTagsForce', { repository: true })
async pushWithTagsForce(repository: Repository): Promise<void> {
await this.pushWithOptions(repository, { pushType: PushType.PushTags, forcePush: true });
}

@command('git.pushTo', { repository: true })
async pushTo(repository: Repository): Promise<void> {
const remotes = repository.remotes;

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 this.pushWithOptions(repository, { pushType: PushType.PushTo });
}

await repository.pushTo(pick.label, branchName);
@command('git.pushToForce', { repository: true })
async pushToForce(repository: Repository): Promise<void> {
await this.pushWithOptions(repository, { pushType: PushType.PushTo, forcePush: true });
}

private async _sync(repository: Repository, rebase: boolean): Promise<void> {
Expand Down
10 changes: 9 additions & 1 deletion extensions/git/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1181,9 +1181,17 @@ export class Repository {
}
}

async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false): Promise<void> {
async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
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');
}
Expand Down
14 changes: 7 additions & 7 deletions extensions/git/src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -895,7 +895,7 @@ export class Repository implements Disposable {
}

@throttle
async push(head: Branch): Promise<void> {
async push(head: Branch, forcePushMode?: ForcePushMode): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;

Expand All @@ -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<void> {
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream));
async pushTo(remote?: string, name?: string, setUpstream: boolean = false, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream, undefined, forcePushMode));
}

async pushTags(remote?: string): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true));
async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true, forcePushMode));
}

@throttle
Expand Down