Ver código fonte

Adds 'Show Stashed Changes` command

Adds experimental 'Apply Stashed Changes' command
Adds experimental 'Delete Stashed Changes' to stashed changes quick pick
main
Eric Amodio 7 anos atrás
pai
commit
9945ee6d94
21 arquivos alterados com 573 adições e 144 exclusões
  1. +18
    -0
      package.json
  2. +3
    -0
      src/commands.ts
  3. +5
    -1
      src/commands/common.ts
  4. +2
    -2
      src/commands/showQuickCommitDetails.ts
  5. +1
    -1
      src/commands/showQuickCommitFileDetails.ts
  6. +42
    -0
      src/commands/showQuickStashList.ts
  7. +48
    -0
      src/commands/stashApply.ts
  8. +31
    -0
      src/commands/stashDelete.ts
  9. +6
    -1
      src/extension.ts
  10. +28
    -16
      src/git/git.ts
  11. +1
    -1
      src/git/models/commit.ts
  12. +2
    -0
      src/git/models/models.ts
  13. +7
    -0
      src/git/models/stash.ts
  14. +24
    -0
      src/git/models/stashCommit.ts
  15. +147
    -0
      src/git/parsers/stashParser.ts
  16. +20
    -1
      src/gitService.ts
  17. +2
    -1
      src/quickPicks.ts
  18. +73
    -54
      src/quickPicks/commitDetails.ts
  19. +75
    -65
      src/quickPicks/commitFileDetails.ts
  20. +6
    -1
      src/quickPicks/common.ts
  21. +32
    -0
      src/quickPicks/stashList.ts

+ 18
- 0
package.json Ver arquivo

@ -459,6 +459,11 @@
"category": "GitLens"
},
{
"command": "gitlens.showQuickStashList",
"title": "Show Stashed Changes",
"category": "GitLens"
},
{
"command": "gitlens.copyShaToClipboard",
"title": "Copy Commit Sha to Clipboard",
"category": "GitLens"
@ -487,6 +492,11 @@
"command": "gitlens.openFileInRemote",
"title": "Open File in Remote",
"category": "GitLens"
},
{
"command": "gitlens.stashApply",
"title": "Apply Stashed Changes",
"category": "GitLens"
}
],
"menus": {
@ -568,6 +578,10 @@
"when": "gitlens:enabled"
},
{
"command": "gitlens.showQuickStashList",
"when": "gitlens:enabled"
},
{
"command": "gitlens.copyShaToClipboard",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable"
},
@ -590,6 +604,10 @@
{
"command": "gitlens.openFileInRemote",
"when": "editorTextFocus && gitlens:enabled && gitlens:hasRemotes"
},
{
"command": "gitlens.stashApply",
"when": "gitlens:enabled && config.gitlens.insiders"
}
],
"explorer/context": [

+ 3
- 0
src/commands.ts Ver arquivo

@ -27,5 +27,8 @@ export * from './commands/showQuickFileHistory';
export * from './commands/showQuickBranchHistory';
export * from './commands/showQuickCurrentBranchHistory';
export * from './commands/showQuickRepoStatus';
export * from './commands/showQuickStashList';
export * from './commands/stashApply';
export * from './commands/stashDelete';
export * from './commands/toggleBlame';
export * from './commands/toggleCodeLens';

+ 5
- 1
src/commands/common.ts Ver arquivo

@ -10,7 +10,8 @@ export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToCli
'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' |
'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' |
'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' |
'gitlens.showQuickRepoStatus' |
'gitlens.showQuickRepoStatus' | 'gitlens.showQuickStashList' |
'gitlens.stashApply' | 'gitlens.stashDelete' | 'gitlens.stashSave' |
'gitlens.toggleBlame' | 'gitlens.toggleCodeLens';
export const Commands = {
CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands,
@ -37,6 +38,9 @@ export const Commands = {
ShowQuickBranchHistory: 'gitlens.showQuickBranchHistory' as Commands,
ShowQuickCurrentBranchHistory: 'gitlens.showQuickRepoHistory' as Commands,
ShowQuickRepoStatus: 'gitlens.showQuickRepoStatus' as Commands,
ShowQuickStashList: 'gitlens.showQuickStashList' as Commands,
StashApply: 'gitlens.stashApply' as Commands,
StashDelete: 'gitlens.stashDelete' as Commands,
ToggleBlame: 'gitlens.toggleBlame' as Commands,
ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands
};

+ 2
- 2
src/commands/showQuickCommitDetails.ts Ver arquivo

@ -46,7 +46,7 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
}
try {
if (!commit || !(commit instanceof GitLogCommit) || commit.type !== 'repo') {
if (!commit || (commit.type !== 'repo' && commit.type !== 'stash')) {
if (repoLog) {
commit = repoLog.commits.get(sha);
// If we can't find the commit, kill the repoLog
@ -88,7 +88,7 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
return pick.execute();
}
return commands.executeCommand(Commands.ShowQuickCommitFileDetails, pick.gitUri, pick.sha, undefined, currentCommand);
return commands.executeCommand(Commands.ShowQuickCommitFileDetails, pick.gitUri, pick.sha, commit, currentCommand);
}
catch (ex) {
Logger.error(ex, 'ShowQuickCommitDetailsCommand');

+ 1
- 1
src/commands/showQuickCommitFileDetails.ts Ver arquivo

@ -44,7 +44,7 @@ export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand
}
try {
if (!commit || !(commit instanceof GitLogCommit) || commit.type !== 'file') {
if (!commit || (commit.type !== 'file' && commit.type !== 'stash')) {
if (fileLog) {
commit = fileLog.commits.get(sha);
// If we can't find the commit, kill the fileLog

+ 42
- 0
src/commands/showQuickStashList.ts Ver arquivo

@ -0,0 +1,42 @@
'use strict';
import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands } from './common';
import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger';
import { CommandQuickPickItem, StashListQuickPick } from '../quickPicks';
export class ShowQuickStashListCommand extends ActiveEditorCachedCommand {
constructor(private git: GitService) {
super(Commands.ShowQuickStashList);
}
async execute(editor: TextEditor, uri?: Uri, goBackCommand?: CommandQuickPickItem) {
if (!(uri instanceof Uri)) {
uri = editor && editor.document && editor.document.uri;
}
try {
const repoPath = await this.git.getRepoPathFromUri(uri, this.git.repoPath);
if (!repoPath) return window.showWarningMessage(`Unable to show stash list`);
const stash = await this.git.getStashList(repoPath);
const pick = await StashListQuickPick.show(stash, undefined, goBackCommand);
if (!pick) return undefined;
if (pick instanceof CommandQuickPickItem) {
return pick.execute();
}
return commands.executeCommand(Commands.ShowQuickCommitDetails, new GitUri(pick.commit.uri, pick.commit), pick.commit.sha, pick.commit,
new CommandQuickPickItem({
label: `go back \u21A9`,
description: `\u00a0 \u2014 \u00a0\u00a0 to the stash list`
}, Commands.ShowQuickStashList, [uri, goBackCommand]));
}
catch (ex) {
Logger.error(ex, 'ShowQuickStashListCommand');
return window.showErrorMessage(`Unable to show stash list. See output channel for more details`);
}
}
}

+ 48
- 0
src/commands/stashApply.ts Ver arquivo

@ -0,0 +1,48 @@
'use strict';
import { MessageItem, window } from 'vscode';
import { GitService, GitStashCommit } from '../gitService';
import { Command, Commands } from './common';
import { CommitQuickPickItem, StashListQuickPick } from '../quickPicks';
import { Logger } from '../logger';
export class StashApplyCommand extends Command {
constructor(private git: GitService) {
super(Commands.StashApply);
}
async execute(stashItem: { stashName: string, message: string }, confirm: boolean = true, deleteAfter: boolean = false) {
if (!this.git.config.insiders) return undefined;
if (!stashItem || !stashItem.stashName) {
const stash = await this.git.getStashList(this.git.repoPath);
if (!stash) return window.showInformationMessage(`There are no stashed changes`);
const pick = await StashListQuickPick.show(stash, 'Apply stashed changes to your working tree\u2026');
if (!pick || !(pick instanceof CommitQuickPickItem)) return undefined;
stashItem = pick.commit as GitStashCommit;
}
try {
if (confirm) {
const message = stashItem.message.length > 80 ? `${stashItem.message.substring(0, 80)}\u2026` : stashItem.message;
const result = await window.showWarningMessage(`Apply stashed changes '${message}' to your working tree?`, { title: 'Yes, delete after applying' } as MessageItem, { title: 'Yes' } as MessageItem, { title: 'No', isCloseAffordance: true } as MessageItem);
if (!result || result.title === 'No') return undefined;
deleteAfter = result.title !== 'Yes';
}
return await this.git.stashApply(this.git.repoPath, stashItem.stashName, deleteAfter);
}
catch (ex) {
Logger.error(ex, 'StashApplyCommand');
if (ex.message.includes('Your local changes to the following files would be overwritten by merge')) {
return window.showErrorMessage(`Unable to apply stash. Your working tree changes would be overwritten.`);
}
else {
return window.showErrorMessage(`Unable to apply stash. See output channel for more details`);
}
}
}
}

+ 31
- 0
src/commands/stashDelete.ts Ver arquivo

@ -0,0 +1,31 @@
'use strict';
import { MessageItem, window } from 'vscode';
import { GitService } from '../gitService';
import { Command, Commands } from './common';
import { Logger } from '../logger';
export class StashDeleteCommand extends Command {
constructor(private git: GitService) {
super(Commands.StashDelete);
}
async execute(stashItem: { stashName: string, message: string }, confirm: boolean = true) {
if (!this.git.config.insiders) return undefined;
if (!stashItem || !stashItem.stashName) return undefined;
try {
if (confirm) {
const message = stashItem.message.length > 80 ? `${stashItem.message.substring(0, 80)}\u2026` : stashItem.message;
const result = await window.showWarningMessage(`Delete stashed changes '${message}'?`, { title: 'Yes' } as MessageItem, { title: 'No', isCloseAffordance: true } as MessageItem);
if (!result || result.title !== 'Yes') return undefined;
}
return await this.git.stashDelete(this.git.repoPath, stashItem.stashName);
}
catch (ex) {
Logger.error(ex, 'StashDeleteCommand');
return window.showErrorMessage(`Unable to delete stash. See output channel for more details`);
}
}
}

+ 6
- 1
src/extension.ts Ver arquivo

@ -12,7 +12,9 @@ import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './comm
import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
import { ShowBlameCommand, ToggleBlameCommand } from './commands';
import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands';
import { ShowLastQuickPickCommand, ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowQuickFileHistoryCommand, ShowQuickRepoStatusCommand} from './commands';
import { ShowLastQuickPickCommand, ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowQuickFileHistoryCommand } from './commands';
import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './commands';
import { StashApplyCommand, StashDeleteCommand } from './commands';
import { ToggleCodeLensCommand } from './commands';
import { Keyboard } from './commands';
import { IConfig } from './configuration';
@ -115,6 +117,9 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(new ShowQuickCommitFileDetailsCommand(git));
context.subscriptions.push(new ShowQuickFileHistoryCommand(git));
context.subscriptions.push(new ShowQuickRepoStatusCommand(git));
context.subscriptions.push(new ShowQuickStashListCommand(git));
context.subscriptions.push(new StashApplyCommand(git));
context.subscriptions.push(new StashDeleteCommand(git));
context.subscriptions.push(new ToggleCodeLensCommand(git));
Telemetry.trackEvent('initialized', Objects.flatten(config, 'config', true));

+ 28
- 16
src/git/git.ts Ver arquivo

@ -9,6 +9,7 @@ import * as tmp from 'tmp';
export * from './models/models';
export * from './parsers/blameParser';
export * from './parsers/logParser';
export * from './parsers/stashParser';
export * from './parsers/statusParser';
export * from './remotes/provider';
@ -16,6 +17,7 @@ let git: IGit;
// `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nparents %P%nsummary %B%nfilename ?`
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--date=iso8601`, `--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?`];
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %ai%nreflog-selector %gd%nsummary %B%nfilename ?`];
async function gitCommand(cwd: string, ...args: any[]) {
try {
@ -163,14 +165,6 @@ export class Git {
return gitCommand(repoPath, ...params);
}
static show(repoPath: string, fileName: string, branchOrSha: string) {
const [file, root] = Git.splitPath(fileName, repoPath);
branchOrSha = branchOrSha.replace('^', '');
if (Git.isUncommitted(branchOrSha)) return Promise.reject(new Error(`sha=${branchOrSha} is uncommitted`));
return gitCommand(root, 'show', `${branchOrSha}:./${file}`);
}
static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) {
const params = [...defaultLogParams, `-m`];
if (maxCount && !reverse) {
@ -227,26 +221,44 @@ export class Git {
}
static remote(repoPath: string): Promise<string> {
const params = ['remote', '-v'];
return gitCommand(repoPath, ...params);
return gitCommand(repoPath, 'remote', '-v');
}
static remote_url(repoPath: string, remote: string): Promise<string> {
const params = ['remote', 'get-url', remote];
return gitCommand(repoPath, ...params);
return gitCommand(repoPath, 'remote', 'get-url', remote);
}
static show(repoPath: string, fileName: string, branchOrSha: string) {
const [file, root] = Git.splitPath(fileName, repoPath);
branchOrSha = branchOrSha.replace('^', '');
if (Git.isUncommitted(branchOrSha)) return Promise.reject(new Error(`sha=${branchOrSha} is uncommitted`));
return gitCommand(root, 'show', `${branchOrSha}:./${file}`);
}
static stash_apply(repoPath: string, stashName: string, deleteAfter: boolean) {
if (!stashName) return undefined;
return gitCommand(repoPath, 'stash', deleteAfter ? 'pop' : 'apply', stashName);
}
static stash_delete(repoPath: string, stashName: string) {
if (!stashName) return undefined;
return gitCommand(repoPath, 'stash', 'drop', stashName);
}
static stash_list(repoPath: string) {
return gitCommand(repoPath, ...defaultStashParams);
}
static status(repoPath: string, porcelainVersion: number = 1): Promise<string> {
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
const params = ['status', porcelain, '--branch'];
return gitCommand(repoPath, ...params);
return gitCommand(repoPath, 'status', porcelain, '--branch');
}
static status_file(repoPath: string, fileName: string, porcelainVersion: number = 1): Promise<string> {
const [file, root] = Git.splitPath(fileName, repoPath);
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
const params = ['status', porcelain, file];
return gitCommand(root, ...params);
return gitCommand(root, 'status', porcelain, file);
}
}

+ 1
- 1
src/git/models/commit.ts Ver arquivo

@ -34,7 +34,7 @@ export interface IGitCommitLine {
code?: string;
}
export type GitCommitType = 'blame' | 'file' | 'repo';
export type GitCommitType = 'blame' | 'file' | 'repo' | 'stash';
export class GitCommit implements IGitCommit {

+ 2
- 0
src/git/models/models.ts Ver arquivo

@ -5,4 +5,6 @@ export * from './commit';
export * from './log';
export * from './logCommit';
export * from './remote';
export * from './stash';
export * from './stashCommit';
export * from './status';

+ 7
- 0
src/git/models/stash.ts Ver arquivo

@ -0,0 +1,7 @@
'use strict';
import { GitStashCommit } from './stashCommit';
export interface IGitStash {
repoPath: string;
commits: Map<string, GitStashCommit>;
}

+ 24
- 0
src/git/models/stashCommit.ts Ver arquivo

@ -0,0 +1,24 @@
'use strict';
import { IGitCommitLine } from './commit';
import { GitLogCommit } from './logCommit';
import { IGitStatusFile, GitStatusFileStatus } from './status';
export class GitStashCommit extends GitLogCommit {
constructor(
public stashName: string,
repoPath: string,
sha: string,
fileName: string,
date: Date,
message: string,
status?: GitStatusFileStatus,
fileStatuses?: IGitStatusFile[],
lines?: IGitCommitLine[],
originalFileName?: string,
previousSha?: string,
previousFileName?: string
) {
super('stash', repoPath, sha, fileName, undefined, date, message, status, fileStatuses, lines, originalFileName, previousSha, previousFileName);
}
}

+ 147
- 0
src/git/parsers/stashParser.ts Ver arquivo

@ -0,0 +1,147 @@
'use strict';
import { Git, GitStashCommit, GitStatusFileStatus, IGitStash, IGitStatusFile } from './../git';
// import { Logger } from '../../logger';
import * as moment from 'moment';
interface IStashEntry {
sha: string;
date?: string;
fileNames?: string;
fileStatuses?: IGitStatusFile[];
summary?: string;
stashName?: string;
}
export class GitStashParser {
private static _parseEntries(data: string): IStashEntry[] {
if (!data) return undefined;
const lines = data.split('\n');
if (!lines.length) return undefined;
const entries: IStashEntry[] = [];
let entry: IStashEntry;
let position = -1;
while (++position < lines.length) {
let lineParts = lines[position].split(' ');
if (lineParts.length < 2) {
continue;
}
if (!entry) {
if (!Git.shaRegex.test(lineParts[0])) continue;
entry = {
sha: lineParts[0]
};
continue;
}
switch (lineParts[0]) {
case 'author-date':
entry.date = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`;
break;
case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim();
while (++position < lines.length) {
const next = lines[position];
if (!next) break;
if (next === 'filename ?') {
position--;
break;
}
entry.summary += `\n${lines[position]}`;
}
break;
case 'reflog-selector':
entry.stashName = lineParts.slice(1).join(' ').trim();
break;
case 'filename':
const nextLine = lines[position + 1];
// If the next line isn't blank, make sure it isn't starting a new commit
if (nextLine && Git.shaRegex.test(nextLine)) continue;
position++;
while (++position < lines.length) {
const line = lines[position];
lineParts = line.split(' ');
if (Git.shaRegex.test(lineParts[0])) {
position--;
break;
}
if (entry.fileStatuses == null) {
entry.fileStatuses = [];
}
const status = {
status: line[0] as GitStatusFileStatus,
fileName: line.substring(1),
originalFileName: undefined as string
} as IGitStatusFile;
this._parseFileName(status);
entry.fileStatuses.push(status);
}
if (entry.fileStatuses) {
entry.fileNames = entry.fileStatuses.filter(_ => !!_.fileName).map(_ => _.fileName).join(', ');
}
entries.push(entry);
entry = undefined;
break;
default:
break;
}
}
return entries;
}
static parse(data: string, repoPath: string): IGitStash {
const entries = this._parseEntries(data);
if (!entries) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
let commit = commits.get(entry.sha);
if (!commit) {
commit = new GitStashCommit(entry.stashName, repoPath, entry.sha, entry.fileNames, moment(entry.date).toDate(), entry.summary, undefined, entry.fileStatuses);
commits.set(entry.sha, commit);
}
}
return {
repoPath: repoPath,
commits: commits
} as IGitStash;
}
private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) {
const index = entry.fileName.indexOf('\t') + 1;
if (index) {
const next = entry.fileName.indexOf('\t', index) + 1;
if (next) {
entry.originalFileName = entry.fileName.substring(index, next - 1);
entry.fileName = entry.fileName.substring(next);
}
else {
entry.fileName = entry.fileName.substring(index);
}
}
}
}

+ 20
- 1
src/gitService.ts Ver arquivo

@ -4,7 +4,7 @@ import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, l
import { CommandContext, setCommandContext } from './commands';
import { CodeLensVisibility, IConfig } from './configuration';
import { DocumentSchemes } from './constants';
import { Git, GitBlameParser, GitBranch, GitCommit, GitLogCommit, GitLogParser, GitRemote, GitStatusFile, GitStatusParser, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog, IGitStatus } from './git/git';
import { Git, GitBlameParser, GitBranch, GitCommit, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog, IGitStash, IGitStatus } from './git/git';
import { IGitUriData, GitUri } from './git/gitUri';
import { GitCodeLensProvider } from './gitCodeLensProvider';
import { Logger } from './logger';
@ -667,6 +667,13 @@ export class GitService extends Disposable {
return (await this.getRepoPathFromFile(gitUri.fsPath)) || fallbackRepoPath;
}
async getStashList(repoPath: string): Promise<IGitStash> {
Logger.log(`getStash('${repoPath}')`);
const data = await Git.stash_list(repoPath);
return GitStashParser.parse(data, repoPath);
}
async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile> {
Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`);
@ -738,6 +745,18 @@ export class GitService extends Disposable {
return Git.difftool_dirDiff(repoPath, sha1, sha2);
}
stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) {
Logger.log(`stashApply('${repoPath}', ${stashName}, ${deleteAfter})`);
return Git.stash_apply(repoPath, stashName, deleteAfter);
}
stashDelete(repoPath: string, stashName: string) {
Logger.log(`stashDelete('${repoPath}', ${stashName}})`);
return Git.stash_delete(repoPath, stashName);
}
toggleCodeLens(editor: TextEditor) {
if (this.config.codeLens.visibility !== CodeLensVisibility.OnDemand ||
(!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return;

+ 2
- 1
src/quickPicks.ts Ver arquivo

@ -7,4 +7,5 @@ export * from './quickPicks/commitFileDetails';
export * from './quickPicks/branchHistory';
export * from './quickPicks/fileHistory';
export * from './quickPicks/remotes';
export * from './quickPicks/repoStatus';
export * from './quickPicks/repoStatus';
export * from './quickPicks/stashList';

+ 73
- 54
src/quickPicks/commitDetails.ts Ver arquivo

@ -3,7 +3,7 @@ import { Arrays, Iterables } from '../system';
import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, Keyboard, KeyNoopCommand } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem } from './common';
import { getGitStatusIcon, Git, GitCommit, GitLogCommit, GitService, GitStatusFileStatus, GitUri, IGitLog, IGitStatusFile } from '../gitService';
import { getGitStatusIcon, Git, GitCommit, GitLogCommit, GitService, GitStashCommit, GitStatusFileStatus, GitUri, IGitLog, IGitStatusFile } from '../gitService';
import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment';
import * as path from 'path';
@ -72,27 +72,44 @@ export class CommitDetailsQuickPick {
static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, repoLog?: IGitLog): Promise<CommitWithFileStatusQuickPickItem | CommandQuickPickItem | undefined> {
const items: (CommitWithFileStatusQuickPickItem | CommandQuickPickItem)[] = commit.fileStatuses.map(fs => new CommitWithFileStatusQuickPickItem(commit, fs));
const stash = commit.type === 'stash';
const type = stash ? 'Stash' : 'Commit';
let index = 0;
if (stash && git.config.insiders) {
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(repo-forked) Apply Stash`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`
}, Commands.StashApply, [commit as GitStashCommit, true, false]));
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(x) Delete Stash`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`
}, Commands.StashDelete, [commit as GitStashCommit, true]));
}
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(clippy) Copy Commit Sha to Clipboard`,
label: `$(clippy) Copy ${type} Sha to Clipboard`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.shortSha}`
}, Commands.CopyShaToClipboard, [uri, commit.sha]));
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(clippy) Copy Commit Message to Clipboard`,
label: `$(clippy) Copy ${type} Message to Clipboard`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`
}, Commands.CopyMessageToClipboard, [uri, commit.sha, commit.message]));
const remotes = Arrays.uniqueBy(await git.getRemotes(git.repoPath), _ => _.url, _ => !!_.provider);
if (remotes.length) {
items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, 'commit', commit.sha, currentCommand));
}
if (!stash) {
const remotes = Arrays.uniqueBy(await git.getRemotes(git.repoPath), _ => _.url, _ => !!_.provider);
if (remotes.length) {
items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, 'commit', commit.sha, currentCommand));
}
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(git-compare) Directory Compare with Previous Commit`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.previousShortSha || `${commit.shortSha}^`} \u00a0 $(git-compare) \u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.DiffDirectory, [commit.uri, commit.previousSha || `${commit.sha}^`, commit.sha]));
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(git-compare) Directory Compare with Previous Commit`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.previousShortSha || `${commit.shortSha}^`} \u00a0 $(git-compare) \u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.DiffDirectory, [commit.uri, commit.previousSha || `${commit.sha}^`, commit.sha]));
}
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(git-compare) Directory Compare with Working Tree`,
@ -117,50 +134,52 @@ export class CommitDetailsQuickPick {
let previousCommand: CommandQuickPickItem | (() => Promise<CommandQuickPickItem>);
let nextCommand: CommandQuickPickItem | (() => Promise<CommandQuickPickItem>);
// If we have the full history, we are good
if (repoLog && !repoLog.truncated && !repoLog.sha) {
previousCommand = commit.previousSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [commit.previousUri, commit.previousSha, undefined, goBackCommand, repoLog]);
nextCommand = commit.nextSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [commit.nextUri, commit.nextSha, undefined, goBackCommand, repoLog]);
}
else {
previousCommand = async () => {
let log = repoLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the previous commit isn't available (since it isn't trustworthy)
if (!c || !c.previousSha) {
log = await git.getLogForRepo(commit.repoPath, commit.sha, git.config.advanced.maxQuickHistory);
c = log && log.commits.get(commit.sha);
if (c) {
// Copy over next info, since it is trustworthy at this point
c.nextSha = commit.nextSha;
if (!stash) {
// If we have the full history, we are good
if (repoLog && !repoLog.truncated && !repoLog.sha) {
previousCommand = commit.previousSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [commit.previousUri, commit.previousSha, undefined, goBackCommand, repoLog]);
nextCommand = commit.nextSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [commit.nextUri, commit.nextSha, undefined, goBackCommand, repoLog]);
}
else {
previousCommand = async () => {
let log = repoLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the previous commit isn't available (since it isn't trustworthy)
if (!c || !c.previousSha) {
log = await git.getLogForRepo(commit.repoPath, commit.sha, git.config.advanced.maxQuickHistory);
c = log && log.commits.get(commit.sha);
if (c) {
// Copy over next info, since it is trustworthy at this point
c.nextSha = commit.nextSha;
}
}
}
if (!c || !c.previousSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [c.previousUri, c.previousSha, undefined, goBackCommand, log]);
};
nextCommand = async () => {
let log = repoLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the next commit isn't available (since it isn't trustworthy)
if (!c || !c.nextSha) {
log = undefined;
c = undefined;
// Try to find the next commit
const nextLog = await git.getLogForRepo(commit.repoPath, commit.sha, 1, true);
const next = nextLog && Iterables.first(nextLog.commits.values());
if (next && next.sha !== commit.sha) {
c = commit;
c.nextSha = next.sha;
if (!c || !c.previousSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [c.previousUri, c.previousSha, undefined, goBackCommand, log]);
};
nextCommand = async () => {
let log = repoLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the next commit isn't available (since it isn't trustworthy)
if (!c || !c.nextSha) {
log = undefined;
c = undefined;
// Try to find the next commit
const nextLog = await git.getLogForRepo(commit.repoPath, commit.sha, 1, true);
const next = nextLog && Iterables.first(nextLog.commits.values());
if (next && next.sha !== commit.sha) {
c = commit;
c.nextSha = next.sha;
}
}
}
if (!c || !c.nextSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [c.nextUri, c.nextSha, undefined, goBackCommand, log]);
};
if (!c || !c.nextSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [c.nextUri, c.nextSha, undefined, goBackCommand, log]);
};
}
}
const scope = await Keyboard.instance.beginScope({
@ -172,7 +191,7 @@ export class CommitDetailsQuickPick {
const pick = await window.showQuickPick(items, {
matchOnDescription: true,
matchOnDetail: true,
placeHolder: `${commit.shortSha} \u00a0\u2022\u00a0 ${commit.author}, ${moment(commit.date).fromNow()} \u00a0\u2022\u00a0 ${commit.message}`,
placeHolder: `${commit.shortSha} \u00a0\u2022\u00a0 ${commit.author ? `${commit.author}, ` : ''}${moment(commit.date).fromNow()} \u00a0\u2022\u00a0 ${commit.message}`,
ignoreFocusOut: getQuickPickIgnoreFocusOut(),
onDidSelectItem: (item: QuickPickItem) => {
scope.setKeyCommand('right', item);

+ 75
- 65
src/quickPicks/commitFileDetails.ts Ver arquivo

@ -35,6 +35,8 @@ export class CommitFileDetailsQuickPick {
static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, fileLog?: IGitLog): Promise<CommandQuickPickItem | undefined> {
const items: CommandQuickPickItem[] = [];
const stash = commit.type === 'stash';
const workingName = (commit.workingFileName && path.basename(commit.workingFileName)) || path.basename(commit.fileName);
const isUncommitted = commit.isUncommitted;
@ -44,16 +46,18 @@ export class CommitFileDetailsQuickPick {
if (!commit) return undefined;
}
items.push(new CommandQuickPickItem({
label: `$(git-commit) Show Commit Details`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.ShowQuickCommitDetails, [new GitUri(commit.uri, commit), commit.sha, commit, currentCommand]));
if (commit.previousSha) {
if (!stash) {
items.push(new CommandQuickPickItem({
label: `$(git-compare) Compare with Previous Commit`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.previousShortSha} \u00a0 $(git-compare) \u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.DiffWithPrevious, [commit.uri, commit]));
label: `$(git-commit) Show Commit Details`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.ShowQuickCommitDetails, [new GitUri(commit.uri, commit), commit.sha, commit, currentCommand]));
if (commit.previousSha) {
items.push(new CommandQuickPickItem({
label: `$(git-compare) Compare with Previous Commit`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.previousShortSha} \u00a0 $(git-compare) \u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.DiffWithPrevious, [commit.uri, commit]));
}
}
if (commit.workingFileName) {
@ -63,15 +67,17 @@ export class CommitFileDetailsQuickPick {
}, Commands.DiffWithWorking, [Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), commit]));
}
items.push(new CommandQuickPickItem({
label: `$(clippy) Copy Commit Sha to Clipboard`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.shortSha}`
}, Commands.CopyShaToClipboard, [uri, commit.sha]));
if (!stash) {
items.push(new CommandQuickPickItem({
label: `$(clippy) Copy Commit Sha to Clipboard`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.shortSha}`
}, Commands.CopyShaToClipboard, [uri, commit.sha]));
items.push(new CommandQuickPickItem({
label: `$(clippy) Copy Commit Message to Clipboard`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`
}, Commands.CopyMessageToClipboard, [uri, commit.sha, commit.message]));
items.push(new CommandQuickPickItem({
label: `$(clippy) Copy Commit Message to Clipboard`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`
}, Commands.CopyMessageToClipboard, [uri, commit.sha, commit.message]));
}
items.push(new OpenCommitFileCommandQuickPickItem(commit));
if (commit.workingFileName) {
@ -80,7 +86,9 @@ export class CommitFileDetailsQuickPick {
const remotes = Arrays.uniqueBy(await git.getRemotes(git.repoPath), _ => _.url, _ => !!_.provider);
if (remotes.length) {
items.push(new OpenRemotesCommandQuickPickItem(remotes, 'file', commit.fileName, undefined, commit.sha, currentCommand));
if (!stash) {
items.push(new OpenRemotesCommandQuickPickItem(remotes, 'file', commit.fileName, undefined, commit.sha, currentCommand));
}
if (commit.workingFileName) {
const branch = await git.getBranch(commit.repoPath || git.repoPath);
items.push(new OpenRemotesCommandQuickPickItem(remotes, 'working-file', commit.workingFileName, branch.name, undefined, currentCommand));
@ -105,55 +113,57 @@ export class CommitFileDetailsQuickPick {
let previousCommand: CommandQuickPickItem | (() => Promise<CommandQuickPickItem>);
let nextCommand: CommandQuickPickItem | (() => Promise<CommandQuickPickItem>);
// If we have the full history, we are good
if (fileLog && !fileLog.truncated && !fileLog.sha) {
previousCommand = commit.previousSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [commit.previousUri, commit.previousSha, undefined, goBackCommand, fileLog]);
nextCommand = commit.nextSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [commit.nextUri, commit.nextSha, undefined, goBackCommand, fileLog]);
}
else {
previousCommand = async () => {
let log = fileLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the previous commit isn't available (since it isn't trustworthy)
if (!c || !c.previousSha) {
log = await git.getLogForFile(commit.repoPath, uri.fsPath, commit.sha, git.config.advanced.maxQuickHistory);
c = log && log.commits.get(commit.sha);
// Since we exclude merge commits in file log, just grab the first returned commit
if (!c && commit.isMerge) {
c = Iterables.first(log.commits.values());
}
if (c) {
// Copy over next info, since it is trustworthy at this point
c.nextSha = commit.nextSha;
c.nextFileName = commit.nextFileName;
if (!stash) {
// If we have the full history, we are good
if (fileLog && !fileLog.truncated && !fileLog.sha) {
previousCommand = commit.previousSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [commit.previousUri, commit.previousSha, undefined, goBackCommand, fileLog]);
nextCommand = commit.nextSha && new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [commit.nextUri, commit.nextSha, undefined, goBackCommand, fileLog]);
}
else {
previousCommand = async () => {
let log = fileLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the previous commit isn't available (since it isn't trustworthy)
if (!c || !c.previousSha) {
log = await git.getLogForFile(commit.repoPath, uri.fsPath, commit.sha, git.config.advanced.maxQuickHistory);
c = log && log.commits.get(commit.sha);
// Since we exclude merge commits in file log, just grab the first returned commit
if (!c && commit.isMerge) {
c = Iterables.first(log.commits.values());
}
if (c) {
// Copy over next info, since it is trustworthy at this point
c.nextSha = commit.nextSha;
c.nextFileName = commit.nextFileName;
}
}
}
if (!c || !c.previousSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [c.previousUri, c.previousSha, undefined, goBackCommand, log]);
};
nextCommand = async () => {
let log = fileLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the next commit isn't available (since it isn't trustworthy)
if (!c || !c.nextSha) {
log = undefined;
c = undefined;
// Try to find the next commit
const next = await git.findNextCommit(commit.repoPath, uri.fsPath, commit.sha);
if (next && next.sha !== commit.sha) {
c = commit;
c.nextSha = next.sha;
c.nextFileName = next.originalFileName || next.fileName;
if (!c || !c.previousSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [c.previousUri, c.previousSha, undefined, goBackCommand, log]);
};
nextCommand = async () => {
let log = fileLog;
let c = log && log.commits.get(commit.sha);
// If we can't find the commit or the next commit isn't available (since it isn't trustworthy)
if (!c || !c.nextSha) {
log = undefined;
c = undefined;
// Try to find the next commit
const next = await git.findNextCommit(commit.repoPath, uri.fsPath, commit.sha);
if (next && next.sha !== commit.sha) {
c = commit;
c.nextSha = next.sha;
c.nextFileName = next.originalFileName || next.fileName;
}
}
}
if (!c || !c.nextSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [c.nextUri, c.nextSha, undefined, goBackCommand, log]);
};
if (!c || !c.nextSha) return KeyNoopCommand;
return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [c.nextUri, c.nextSha, undefined, goBackCommand, log]);
};
}
}
const scope = await Keyboard.instance.beginScope({

+ 6
- 1
src/quickPicks/common.ts Ver arquivo

@ -117,7 +117,12 @@ export class CommitQuickPickItem implements QuickPickItem {
detail: string;
constructor(public commit: GitCommit, descriptionSuffix: string = '') {
this.label = `${commit.author}, ${moment(commit.date).fromNow()}`;
if (commit.author) {
this.label = `${commit.author}, ${moment(commit.date).fromNow()}`;
}
else {
this.label = `${moment(commit.date).fromNow()}`;
}
this.description = `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha}${descriptionSuffix}`;
this.detail = commit.message;
}

+ 32
- 0
src/quickPicks/stashList.ts Ver arquivo

@ -0,0 +1,32 @@
'use strict';
import { Iterables } from '../system';
import { QuickPickOptions, window } from 'vscode';
import { Keyboard } from '../commands';
import { IGitStash } from '../gitService';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks';
export class StashListQuickPick {
static async show(stash: IGitStash, placeHolder?: string, goBackCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> {
const items = ((stash && Array.from(Iterables.map(stash.commits.values(), c => new CommitQuickPickItem(c, ` \u2014 ${c.fileNames}`)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[];
if (goBackCommand) {
items.splice(0, 0, goBackCommand);
}
const scope = await Keyboard.instance.beginScope({ left: goBackCommand });
const pick = await window.showQuickPick(items, {
matchOnDescription: true,
placeHolder: placeHolder || `stash list \u2014 search by message, filename, or sha`,
ignoreFocusOut: getQuickPickIgnoreFocusOut()
// onDidSelectItem: (item: QuickPickItem) => {
// scope.setKeyCommand('right', item);
// }
} as QuickPickOptions);
await scope.dispose();
return pick;
}
}

Carregando…
Cancelar
Salvar