/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { Uri, commands, scm, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn } from 'vscode'; import { Ref, RefType, Git, GitErrorCodes, Branch } from './git'; import { Model, Resource, Status, CommitOptions, WorkingTreeGroup, IndexGroup, MergeGroup } from './model'; import { toGitUri, fromGitUri } from './uri'; import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging'; import * as path from 'path'; import * as os from 'os'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); class CheckoutItem implements QuickPickItem { protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } protected get treeish(): string | undefined { return this.ref.name; } get label(): string { return this.ref.name || this.shortCommit; } get description(): string { return this.shortCommit; } constructor(protected ref: Ref) { } async run(model: Model): Promise { const ref = this.treeish; if (!ref) { return; } await model.checkout(ref); } } class CheckoutTagItem extends CheckoutItem { get description(): string { return localize('tag at', "Tag at {0}", this.shortCommit); } } class CheckoutRemoteHeadItem extends CheckoutItem { get description(): string { return localize('remote branch at', "Remote branch at {0}", this.shortCommit); } protected get treeish(): string | undefined { if (!this.ref.name) { return; } const match = /^[^/]+\/(.*)$/.exec(this.ref.name); return match ? match[1] : this.ref.name; } } class BranchDeleteItem implements QuickPickItem { private get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } get branchName(): string | undefined { return this.ref.name; } get label(): string { return this.branchName || ''; } get description(): string { return this.shortCommit; } constructor(private ref: Ref) { } async run(model: Model, force?: boolean): Promise { if (!this.branchName) { return; } await model.deleteBranch(this.branchName, force); } } class MergeItem implements QuickPickItem { get label(): string { return this.ref.name || ''; } get description(): string { return this.ref.name || ''; } constructor(protected ref: Ref) { } async run(model: Model): Promise { await model.merge(this.ref.name! || this.ref.commit!); } } class CreateBranchItem implements QuickPickItem { get label(): string { return localize('create branch', '$(plus) Create new branch'); } get description(): string { return ''; } async run(model: Model): Promise { await commands.executeCommand('git.branch'); } } interface Command { commandId: string; key: string; method: Function; skipModelCheck: boolean; requiresDiffInformation: boolean; } const Commands: Command[] = []; function command(commandId: string, skipModelCheck = false, requiresDiffInformation = false): Function { return (target: any, key: string, descriptor: any) => { if (!(typeof descriptor.value === 'function')) { throw new Error('not supported'); } Commands.push({ commandId, key, method: descriptor.value, skipModelCheck, requiresDiffInformation }); }; } export class CommandCenter { private model: Model; private disposables: Disposable[]; constructor( private git: Git, model: Model | undefined, private outputChannel: OutputChannel, private telemetryReporter: TelemetryReporter ) { if (model) { this.model = model; } this.disposables = Commands .map(({ commandId, key, method, skipModelCheck, requiresDiffInformation }) => { const command = this.createCommand(commandId, key, method, skipModelCheck); if (requiresDiffInformation) { return commands.registerDiffInformationCommand(commandId, command); } else { return commands.registerCommand(commandId, command); } }); } @command('git.refresh') async refresh(): Promise { await this.model.status(); } @command('git.openResource') async openResource(resource: Resource): Promise { await this._openResource(resource); } private async _openResource(resource: Resource, preview?: boolean): Promise { const left = this.getLeftResource(resource); const right = this.getRightResource(resource); const title = this.getTitle(resource); if (!right) { // TODO console.error('oh no'); return; } const opts: TextDocumentShowOptions = { preserveFocus: true, preview: preview, viewColumn: window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One }; const activeTextEditor = window.activeTextEditor; if (activeTextEditor && activeTextEditor.document.uri.toString() === right.toString()) { opts.selection = activeTextEditor.selection; } if (!left) { const document = await workspace.openTextDocument(right); await window.showTextDocument(document, opts); return; } return await commands.executeCommand('vscode.diff', left, right, title, opts); } private getLeftResource(resource: Resource): Uri | undefined { switch (resource.type) { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: return toGitUri(resource.original, 'HEAD'); case Status.MODIFIED: return toGitUri(resource.resourceUri, '~'); case Status.DELETED_BY_THEM: return toGitUri(resource.resourceUri, ''); } } private getRightResource(resource: Resource): Uri | undefined { switch (resource.type) { case Status.INDEX_MODIFIED: case Status.INDEX_ADDED: case Status.INDEX_COPIED: case Status.INDEX_RENAMED: return toGitUri(resource.resourceUri, ''); case Status.INDEX_DELETED: case Status.DELETED_BY_THEM: case Status.DELETED: return toGitUri(resource.resourceUri, 'HEAD'); case Status.MODIFIED: case Status.UNTRACKED: case Status.IGNORED: const uriString = resource.resourceUri.toString(); const [indexStatus] = this.model.indexGroup.resources.filter(r => r.resourceUri.toString() === uriString); if (indexStatus && indexStatus.renameResourceUri) { return indexStatus.renameResourceUri; } return resource.resourceUri; case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return resource.resourceUri; } } private getTitle(resource: Resource): string { const basename = path.basename(resource.resourceUri.fsPath); switch (resource.type) { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: case Status.DELETED_BY_THEM: return `${basename} (Index)`; case Status.MODIFIED: case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return `${basename} (Working Tree)`; } return ''; } @command('git.clone', true) async clone(): Promise { const url = await window.showInputBox({ prompt: localize('repourl', "Repository URL"), ignoreFocusOut: true }); if (!url) { this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' }); return; } const config = workspace.getConfiguration('git'); const value = config.get('defaultCloneDirectory') || os.homedir(); const parentPath = await window.showInputBox({ prompt: localize('parent', "Parent Directory"), value, ignoreFocusOut: true }); if (!parentPath) { this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' }); return; } const clonePromise = this.git.clone(url, parentPath); window.setStatusBarMessage(localize('cloning', "Cloning git repository..."), clonePromise); try { const repositoryPath = await clonePromise; const open = localize('openrepo', "Open Repository"); const result = await window.showInformationMessage(localize('proposeopen', "Would you like to open the cloned repository?"), open); const openFolder = result === open; this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 }); if (openFolder) { commands.executeCommand('vscode.openFolder', Uri.file(repositoryPath)); } } catch (err) { if (/already exists and is not an empty directory/.test(err && err.stderr || '')) { this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' }); } else { this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' }); } throw err; } } @command('git.init') async init(): Promise { await this.model.init(); } @command('git.openFile') async openFile(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise { let uris: Uri[] | undefined; if (arg instanceof Uri) { if (arg.scheme === 'git') { uris = [Uri.file(fromGitUri(arg).path)]; } else if (arg.scheme === 'file') { uris = [arg]; } } else { let resource = arg; if (!(resource instanceof Resource)) { // can happen when called from a keybinding resource = this.getSCMResource(); } if (resource) { uris = [...resourceStates.map(r => r.resourceUri), resource.resourceUri]; } } if (!uris) { return; } const preview = uris.length === 1 ? true : false; const activeTextEditor = window.activeTextEditor; for (const uri of uris) { // If the active editor matches the current uri, get its selection const selections = activeTextEditor && activeTextEditor.document.uri.toString() === uri.toString() ? activeTextEditor.selections : undefined; const opts: TextDocumentShowOptions = { preserveFocus: true, preview: preview, viewColumn: activeTextEditor && activeTextEditor.viewColumn || ViewColumn.One }; const document = await workspace.openTextDocument(uri); await window.showTextDocument(document, opts); if (selections && window.activeTextEditor) { window.activeTextEditor.selections = selections; } } } @command('git.openHEADFile') async openHEADFile(arg?: Resource | Uri): Promise { let resource: Resource | undefined = undefined; if (arg instanceof Resource) { resource = arg; } else if (arg instanceof Uri) { resource = this.getSCMResource(arg); } else { resource = this.getSCMResource(); } if (!resource) { return; } const HEAD = this.getLeftResource(resource); if (!HEAD) { window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath))); return; } return await commands.executeCommand('vscode.open', HEAD); } @command('git.openChange') async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise { let resources: Resource[] | undefined = undefined; if (arg instanceof Uri) { const resource = this.getSCMResource(arg); if (resource !== undefined) { resources = [resource]; } } else { let resource: Resource | undefined = undefined; if (arg instanceof Resource) { resource = arg; } else { resource = this.getSCMResource(); } if (resource) { resources = [...resourceStates as Resource[], resource]; } } if (!resources) { return; } const preview = resources.length === 1 ? undefined : false; for (const resource of resources) { await this._openResource(resource, preview); } } @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) { const resource = this.getSCMResource(); if (!resource) { return; } resourceStates = [resource]; } const resources = resourceStates .filter(s => s instanceof Resource && (s.resourceGroup instanceof WorkingTreeGroup || s.resourceGroup instanceof MergeGroup)) as Resource[]; if (!resources.length) { return; } return await this.model.add(...resources); } @command('git.stageAll') async stageAll(): Promise { return await this.model.add(); } @command('git.stageSelectedRanges', false, true) async stageSelectedRanges(diffs: LineChange[]): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { return; } const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; if (modifiedUri.scheme !== 'file') { return; } const originalUri = toGitUri(modifiedUri, '~'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); const selectedDiffs = diffs .map(diff => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null)) .filter(d => !!d) as LineChange[]; if (!selectedDiffs.length) { return; } const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs); await this.model.stage(modifiedUri, result); } @command('git.revertSelectedRanges', false, true) async revertSelectedRanges(diffs: LineChange[]): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { return; } const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; if (modifiedUri.scheme !== 'file') { return; } const originalUri = toGitUri(modifiedUri, '~'); const originalDocument = await workspace.openTextDocument(originalUri); const selections = textEditor.selections; const selectedDiffs = diffs.filter(diff => { const modifiedRange = diff.modifiedEndLineNumber === 0 ? new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(diff.modifiedStartLineNumber).range.start) : new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end); return selections.every(selection => !selection.intersection(modifiedRange)); }); if (selectedDiffs.length === diffs.length) { return; } const basename = path.basename(modifiedUri.fsPath); const message = localize('confirm revert', "Are you sure you want to revert the selected changes in {0}?", basename); const yes = localize('revert', "Revert Changes"); const pick = await window.showWarningMessage(message, { modal: true }, yes); if (pick !== yes) { return; } const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs); const edit = new WorkspaceEdit(); edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result); workspace.applyEdit(edit); } @command('git.unstage') async unstage(...resourceStates: SourceControlResourceState[]): Promise { if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) { const resource = this.getSCMResource(); if (!resource) { return; } resourceStates = [resource]; } const resources = resourceStates .filter(s => s instanceof Resource && s.resourceGroup instanceof IndexGroup) as Resource[]; if (!resources.length) { return; } return await this.model.revertFiles(...resources); } @command('git.unstageAll') async unstageAll(): Promise { return await this.model.revertFiles(); } @command('git.unstageSelectedRanges', false, true) async unstageSelectedRanges(diffs: LineChange[]): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { return; } const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; if (modifiedUri.scheme !== 'git') { return; } const { ref } = fromGitUri(modifiedUri); if (ref !== '') { return; } const originalUri = toGitUri(modifiedUri, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); const selectedDiffs = diffs .map(diff => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null)) .filter(d => !!d) as LineChange[]; if (!selectedDiffs.length) { return; } const invertedDiffs = selectedDiffs.map(invertLineChange); const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); await this.model.stage(modifiedUri, result); } @command('git.clean') async clean(...resourceStates: SourceControlResourceState[]): Promise { if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) { const resource = this.getSCMResource(); if (!resource) { return; } resourceStates = [resource]; } const resources = resourceStates .filter(s => s instanceof Resource && s.resourceGroup instanceof WorkingTreeGroup) as Resource[]; if (!resources.length) { return; } const message = resources.length === 1 ? localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath)) : localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", resources.length); const yes = localize('discard', "Discard Changes"); const pick = await window.showWarningMessage(message, { modal: true }, yes); if (pick !== yes) { return; } await this.model.clean(...resources); } @command('git.cleanAll') async cleanAll(): Promise { const message = localize('confirm discard all', "Are you sure you want to discard ALL changes? This is IRREVERSIBLE!"); const yes = localize('discardAll', "Discard ALL Changes"); const pick = await window.showWarningMessage(message, { modal: true }, yes); if (pick !== yes) { return; } await this.model.clean(...this.model.workingTreeGroup.resources); } private async smartCommit( getCommitMessage: () => Promise, opts?: CommitOptions ): Promise { const config = workspace.getConfiguration('git'); const enableSmartCommit = config.get('enableSmartCommit') === true; const enableCommitSigning = config.get('enableCommitSigning') === true; const noStagedChanges = this.model.indexGroup.resources.length === 0; const noUnstagedChanges = this.model.workingTreeGroup.resources.length === 0; // no changes, and the user has not configured to commit all in this case if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit) { // prompt the user if we want to commit all or not const message = localize('no staged changes', "There are no staged changes to commit.\n\nWould you like to automatically stage all your changes and commit them directly?"); const yes = localize('yes', "Yes"); const always = localize('always', "Always"); const pick = await window.showWarningMessage(message, { modal: true }, yes, always); if (pick === always) { config.update('enableSmartCommit', true, true); } else if (pick !== yes) { return false; // do not commit on cancel } } if (!opts) { opts = { all: noStagedChanges }; } // enable signing of commits if configurated opts.signCommit = enableCommitSigning; if ( // no changes (noStagedChanges && noUnstagedChanges) // or no staged changes and not `all` || (!opts.all && noStagedChanges) ) { window.showInformationMessage(localize('no changes', "There are no changes to commit.")); return false; } const message = await getCommitMessage(); if (!message) { // TODO@joao: show modal dialog to confirm empty message commit return false; } await this.model.commit(message, opts); return true; } private async commitWithAnyInput(opts?: CommitOptions): Promise { const message = scm.inputBox.value; const getCommitMessage = async () => { if (message) { return message; } return await window.showInputBox({ placeHolder: localize('commit message', "Commit message"), prompt: localize('provide commit message', "Please provide a commit message"), ignoreFocusOut: true }); }; const didCommit = await this.smartCommit(getCommitMessage, opts); if (message && didCommit) { scm.inputBox.value = await this.model.getCommitTemplate(); } } @command('git.commit') async commit(): Promise { await this.commitWithAnyInput(); } @command('git.commitWithInput') async commitWithInput(): Promise { if (!scm.inputBox.value) { return; } const didCommit = await this.smartCommit(async () => scm.inputBox.value); if (didCommit) { scm.inputBox.value = await this.model.getCommitTemplate(); } } @command('git.commitStaged') async commitStaged(): Promise { await this.commitWithAnyInput({ all: false }); } @command('git.commitStagedSigned') async commitStagedSigned(): Promise { await this.commitWithAnyInput({ all: false, signoff: true }); } @command('git.commitAll') async commitAll(): Promise { await this.commitWithAnyInput({ all: true }); } @command('git.commitAllSigned') async commitAllSigned(): Promise { await this.commitWithAnyInput({ all: true, signoff: true }); } @command('git.undoCommit') async undoCommit(): Promise { const HEAD = this.model.HEAD; if (!HEAD || !HEAD.commit) { return; } const commit = await this.model.getCommit('HEAD'); await this.model.reset('HEAD~'); scm.inputBox.value = commit.message; } @command('git.checkout') async checkout(treeish: string): Promise { if (typeof treeish === 'string') { return await this.model.checkout(treeish); } const config = workspace.getConfiguration('git'); const checkoutType = config.get('checkoutType') || 'all'; const includeTags = checkoutType === 'all' || checkoutType === 'tags'; const includeRemotes = checkoutType === 'all' || checkoutType === 'remote'; const createBranch = new CreateBranchItem(); const heads = this.model.refs.filter(ref => ref.type === RefType.Head) .map(ref => new CheckoutItem(ref)); const tags = (includeTags ? this.model.refs.filter(ref => ref.type === RefType.Tag) : []) .map(ref => new CheckoutTagItem(ref)); const remoteHeads = (includeRemotes ? this.model.refs.filter(ref => ref.type === RefType.RemoteHead) : []) .map(ref => new CheckoutRemoteHeadItem(ref)); const picks = [createBranch, ...heads, ...tags, ...remoteHeads]; const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout'); const choice = await window.showQuickPick(picks, { placeHolder }); if (!choice) { return; } await choice.run(this.model); } @command('git.branch') async branch(): Promise { const result = await window.showInputBox({ placeHolder: localize('branch name', "Branch name"), prompt: localize('provide branch name', "Please provide a branch name"), ignoreFocusOut: true }); if (!result) { return; } const name = result.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-'); await this.model.branch(name); } @command('git.deleteBranch') async deleteBranch(name: string, force?: boolean): Promise { let run: (force?: boolean) => Promise; if (typeof name === 'string') { run = force => this.model.deleteBranch(name, force); } else { const currentHead = this.model.HEAD && this.model.HEAD.name; const heads = this.model.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead) .map(ref => new BranchDeleteItem(ref)); const placeHolder = localize('select branch to delete', 'Select a branch to delete'); const choice = await window.showQuickPick(heads, { placeHolder }); if (!choice || !choice.branchName) { return; } name = choice.branchName; run = force => choice.run(this.model, force); } try { await run(force); } catch (err) { if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) { throw err; } const message = localize('confirm force delete branch', "The branch '{0}' is not fully merged. Delete anyway?", name); const yes = localize('delete branch', "Delete Branch"); const pick = await window.showWarningMessage(message, yes); if (pick === yes) { await run(true); } } } @command('git.renameBranch') async renameBranch(): Promise { const placeHolder = localize('provide branch name', "Please provide a branch name"); const name = await window.showInputBox({ placeHolder }); if (!name || name.trim().length === 0) { return; } const run = force => this.model.renameBranch(name); try { await run(name); } catch (err) { console.log(err); if (err.gitErrorCode !== GitErrorCodes.InvalidBranchName && err.gitErrorCode !== GitErrorCodes.BranchAlreadyExists) { return err; } let message = ''; switch (err.gitErrorCode) { case GitErrorCodes.InvalidBranchName: message = localize('invalid branch name', 'Invalid branch name'); break; case GitErrorCodes.BranchAlreadyExists: message = localize('branch already exists', `A branch named '${name}' already exists`); break; } if (!message) { return; } window.showErrorMessage(message); } } @command('git.merge') async merge(): Promise { const config = workspace.getConfiguration('git'); const checkoutType = config.get('checkoutType') || 'all'; const includeRemotes = checkoutType === 'all' || checkoutType === 'remote'; const heads = this.model.refs.filter(ref => ref.type === RefType.Head) .filter(ref => ref.name || ref.commit) .map(ref => new MergeItem(ref as Branch)); const remoteHeads = (includeRemotes ? this.model.refs.filter(ref => ref.type === RefType.RemoteHead) : []) .filter(ref => ref.name || ref.commit) .map(ref => new MergeItem(ref as Branch)); const picks = [...heads, ...remoteHeads]; const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from'); const choice = await window.showQuickPick(picks, { placeHolder }); if (!choice) { return; } try { await choice.run(this.model); } catch (err) { if (err.gitErrorCode !== GitErrorCodes.Conflict) { throw err; } const message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing."); await window.showWarningMessage(message); } } @command('git.createTag') async createTag(): Promise { const inputTagName = await window.showInputBox({ placeHolder: localize('tag name', "Tag name"), prompt: localize('provide tag name', "Please provide a tag name"), ignoreFocusOut: true }); if (!inputTagName) { return; } const inputMessage = await window.showInputBox({ placeHolder: localize('tag message', "Message"), prompt: localize('provide tag message', "Please provide a message to annotate the tag"), ignoreFocusOut: true }); const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-'); const message = inputMessage || name; await this.model.tag(name, message); } @command('git.pullFrom') async pullFrom(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from.")); return; } const picks = remotes.map(r => ({ label: r.name, description: r.url })); const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from"); const pick = await window.showQuickPick(picks, { placeHolder }); if (!pick) { return; } const branchName = await window.showInputBox({ placeHolder: localize('branch name', "Branch name"), prompt: localize('provide branch name', "Please provide a branch name"), ignoreFocusOut: true }); if (!branchName) { return; } this.model.pull(false, pick.label, branchName); } @command('git.pull') async pull(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from.")); return; } await this.model.pull(); } @command('git.pullRebase') async pullRebase(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from.")); return; } await this.model.pullWithRebase(); } @command('git.push') async push(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); return; } await this.model.push(); } @command('git.pushWithTags') async pushWithTags(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); return; } await this.model.pushTags(); window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags.")); } @command('git.pushTo') async pushTo(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); return; } if (!this.model.HEAD || !this.model.HEAD.name) { window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote.")); return; } const branchName = this.model.HEAD.name; const picks = remotes.map(r => ({ label: r.name, description: r.url })); 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; } this.model.pushTo(pick.label, branchName); } @command('git.sync') async sync(): Promise { const HEAD = this.model.HEAD; if (!HEAD || !HEAD.upstream) { return; } const config = workspace.getConfiguration('git'); const shouldPrompt = config.get('confirmSync') === true; if (shouldPrompt) { const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}'.", HEAD.upstream); const yes = localize('ok', "OK"); const neverAgain = localize('never again', "OK, Never Show Again"); const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain); if (pick === neverAgain) { await config.update('confirmSync', false, true); } else if (pick !== yes) { return; } } await this.model.sync(); } @command('git.publish') async publish(): Promise { const remotes = this.model.remotes; if (remotes.length === 0) { window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to.")); return; } const branchName = this.model.HEAD && this.model.HEAD.name || ''; const picks = this.model.remotes.map(r => r.name); const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName); const choice = await window.showQuickPick(picks, { placeHolder }); if (!choice) { return; } await this.model.pushTo(choice, branchName, true); } @command('git.showOutput') showOutput(): void { this.outputChannel.show(); } @command('git.ignore') async ignore(...resourceStates: SourceControlResourceState[]): Promise { if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) { const uri = window.activeTextEditor && window.activeTextEditor.document.uri; if (!uri) { return; } return await this.model.ignore([uri]); } const uris = resourceStates .filter(s => s instanceof Resource) .map(r => r.resourceUri); if (!uris.length) { return; } await this.model.ignore(uris); } private createCommand(id: string, key: string, method: Function, skipModelCheck: boolean): (...args: any[]) => any { const result = (...args) => { if (!skipModelCheck && !this.model) { window.showInformationMessage(localize('disabled', "Git is either disabled or not supported in this workspace")); return; } this.telemetryReporter.sendTelemetryEvent('git.command', { command: id }); const result = Promise.resolve(method.apply(this, args)); return result.catch(async err => { let message: string; switch (err.gitErrorCode) { case GitErrorCodes.DirtyWorkTree: message = localize('clean repo', "Please clean your repository working tree before checkout."); break; case GitErrorCodes.PushRejected: message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes."); break; default: const hint = (err.stderr || err.message || String(err)) .replace(/^error: /mi, '') .replace(/^> husky.*$/mi, '') .split(/[\r\n]/) .filter(line => !!line) [0]; message = hint ? localize('git error details', "Git: {0}", hint) : localize('git error', "Git error"); break; } if (!message) { console.error(err); return; } const outputChannel = this.outputChannel as OutputChannel; const openOutputChannelChoice = localize('open git log', "Open Git Log"); const choice = await window.showErrorMessage(message, openOutputChannelChoice); if (choice === openOutputChannelChoice) { outputChannel.show(); } }); }; // patch this object, so people can call methods directly this[key] = result; return result; } private getSCMResource(uri?: Uri): Resource | undefined { uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri; if (!uri) { return undefined; } if (uri.scheme === 'git') { const { path } = fromGitUri(uri); uri = Uri.file(path); } if (uri.scheme === 'file') { const uriString = uri.toString(); return this.model.workingTreeGroup.resources.filter(r => r.resourceUri.toString() === uriString)[0] || this.model.indexGroup.resources.filter(r => r.resourceUri.toString() === uriString)[0]; } } dispose(): void { this.disposables.forEach(d => d.dispose()); } }