diff --git a/extensions/git/package.json b/extensions/git/package.json index 0ee3d0d00c7cdecbd43cc037168c15bc8ec366c5..88034d6c62c5d9b2ea4f0e34a73754eaf7eaf795 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -251,6 +251,21 @@ "command": "git.ignore", "title": "%command.ignore%", "category": "Git" + }, + { + "command": "git.stash", + "title": "%command.stash%", + "category": "Git" + }, + { + "command": "git.stashPop", + "title": "%command.stashPop%", + "category": "Git" + }, + { + "command": "git.stashPopLatest", + "title": "%command.stashPopLatest%", + "category": "Git" } ], "menus": { @@ -402,6 +417,18 @@ { "command": "git.showOutput", "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.stash", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.stashPop", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.stashPopLatest", + "when": "config.git.enabled && scmProvider == git && gitState == idle" } ], "scm/title": [ @@ -501,7 +528,22 @@ }, { "command": "git.showOutput", - "group": "5_output", + "group": "6_output", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.stash", + "group": "5_stash", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.stashPop", + "group": "5_stash", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.stashPopLatest", + "group": "5_stash", "when": "config.git.enabled && scmProvider == git && gitState == idle" } ], diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 13779f30d00a0ae26fa61f1f310d0d9d929e81c0..7939d3f274da95019764fa2ab286fb5f031ad40b 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -37,6 +37,9 @@ "command.publish": "Publish Branch", "command.showOutput": "Show Git Output", "command.ignore": "Add File to .gitignore", + "command.stash": "Stash", + "command.stashPop": "Stash Pop", + "command.stashPopLatest": "Stash Pop Latest", "config.enabled": "Whether git is enabled", "config.path": "Path to the git executable", "config.autorefresh": "Whether auto refreshing is enabled", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 3f6ae741e53bc7825f79fc3ea0ebe2781f4f47c1..26d516081e481dc8cc1d69bcf56d5b24a7d78d73 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1092,6 +1092,47 @@ export class CommandCenter { await this.model.ignore(uris); } + @command('git.stash') + async stash() : Promise { + const noUnstagedChanges = this.model.workingTreeGroup.resources.length === 0; + if (noUnstagedChanges){ + window.showInformationMessage(localize('no changes stash', "There are no changes to stash.")); + return; + } + return await this.model.stash(); + } + + @command('git.stashPop') + async stashPop(): Promise { + let stashes = await this.model.getStashes(); + const noStashes = stashes.length === 0; + if (noStashes){ + window.showInformationMessage(localize('no stashes', "There are no stashes to restore.")); + return; + } + + const picks = stashes.map(r => { return { label: `#${r.id}: ${r.description}`, description: "", derails: "", id: r.id }; }); + const placeHolder = localize('pick stash', "Pick a stash"); + const choice = await window.showQuickPick(picks, { placeHolder }); + + if (!choice) { + return; + } + return await this.model.stash(true, choice.id); + } + + @command('git.stashPopLatest') + async stashPopLatest(): Promise { + let stashes = await this.model.getStashes(); + const noStashes = stashes.length === 0; + if (noStashes){ + window.showInformationMessage(localize('no stashes', "There are no stashes to restore.")); + return; + } + return await this.model.stash(true); + } + + private createCommand(id: string, key: string, method: Function, skipModelCheck: boolean): (...args: any[]) => any { const result = (...args) => { if (!skipModelCheck && !this.model) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index b607a5fb2cb8d25b9eee351851f57906a83e02be..b38aa6c1204c16dc1c704a0b12cb749f42c0d19d 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -33,6 +33,11 @@ export interface Remote { url: string; } +export interface Stash { + id : string; + description: string; +} + export enum RefType { Head, RemoteHead, @@ -277,7 +282,10 @@ export const GitErrorCodes = { RepositoryNotFound: 'RepositoryNotFound', RepositoryIsLocked: 'RepositoryIsLocked', BranchNotFullyMerged: 'BranchNotFullyMerged', - NoRemoteReference: 'NoRemoteReference' + NoRemoteReference: 'NoRemoteReference', + NoLocalChanges: 'NoLocalChanges', + NoStashFound: 'NoStashFound', + LocalChangesOverwritten: 'LocalChangesOverwritten' }; function getGitErrorCode(stderr: string): string | undefined { @@ -834,6 +842,32 @@ export class Repository { } } + async stash(pop: boolean = false, index?: string): Promise { + try { + const args = ['stash']; + + if (pop) { + args.push('pop'); + if (index) { + args.push(`stash@{${index}}`); + } + } + + await this.run(args); + } catch (err) { + if (/No local changes to save/.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.NoLocalChanges; + } + else if (/No stash found/.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.NoStashFound; + } + else if (/error: Your local changes to the following files would be overwritten/.test(err.stderr || '')) { + err.gitErrorCode = GitErrorCodes.LocalChangesOverwritten; + } + throw err; + } + } + getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> { return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => { const parser = new GitStatusParser(); @@ -921,6 +955,18 @@ export class Repository { .filter(ref => !!ref) as Ref[]; } + async getStashes(): Promise { + const result = await this.run(['stash', 'list']); + const regex = /^stash@{(\d+)}:(.+)/; + const rawStashes = result.stdout.trim().split('\n') + .filter(b => !!b) + .map(line => regex.exec(line)) + .filter(g => !!g) + .map((groups: RegExpExecArray) => ({ id: groups[1], description: groups[2] })); + return uniqBy(rawStashes, remote => remote.id); + } + + async getRemotes(): Promise { const result = await this.run(['remote', '--verbose']); const regex = /^([^\s]+)\s+([^\s]+)\s/; diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 3f0177dde7d045c9332a7b0a1624c679820e39fa..0bcd68df8f0e3cfe8b3bb4ce2e319d2aabf53012 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -6,7 +6,7 @@ 'use strict'; import { Uri, Command, EventEmitter, Event, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit } from 'vscode'; -import { Git, Repository, Ref, Branch, Remote, Commit, GitErrorCodes } from './git'; +import { Git, Repository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash } from './git'; import { anyEvent, eventToPromise, filterEvent, EmptyDisposable, combinedDisposable, dispose } from './util'; import { memoize, throttle, debounce } from './decorators'; import * as path from 'path'; @@ -215,7 +215,8 @@ export enum Operation { DeleteBranch = 1 << 16, Merge = 1 << 17, Ignore = 1 << 18, - Tag = 1 << 19 + Tag = 1 << 19, + Stash = 1 << 20 } // function getOperationName(operation: Operation): string { @@ -542,6 +543,11 @@ export class Model implements Disposable { }); } + @throttle + async stash(pop: boolean = false, index?: string): Promise { + return await this.run(Operation.Stash, () => this.repository.stash(pop, index)); + } + async getCommitTemplate(): Promise { return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate()); } @@ -568,6 +574,10 @@ export class Model implements Disposable { }); } + async getStashes(): Promise { + return await this.getStashes(); + } + private async run(operation: Operation, runOperation: () => Promise = () => Promise.resolve(null)): Promise { const run = async () => { this._operations = this._operations.start(operation);