From 7e8b404c2c4c59f3528f9e00bf57a9a2562eac8a Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 29 Sep 2018 02:31:59 -0400 Subject: [PATCH] Closes #389 - Adds difftool command to files in explorers --- package.json | 7 ++++- src/commands/common.ts | 56 ++++++++++++++++++++++++++++++-------- src/commands/externalDiff.ts | 56 ++++++++++++++++++++++++++++++++++++-- src/commands/stashSave.ts | 2 +- src/git/git.ts | 15 ++++++++-- src/git/gitService.ts | 17 ++++++++---- src/git/models/file.ts | 6 ++-- src/git/parsers/diffParser.ts | 2 +- src/git/parsers/logParser.ts | 6 ++-- src/git/parsers/stashParser.ts | 2 +- src/views/nodes/resultsFileNode.ts | 22 +++++++-------- 11 files changed, 148 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 0c496e2..ea0c40a 100644 --- a/package.json +++ b/package.json @@ -3260,11 +3260,16 @@ "group": "2_gitlens@1" }, { - "command": "gitlens.explorers.openChangesWithWorking", + "command": "gitlens.externalDiff", "when": "viewItem =~ /gitlens:file\\b/", "group": "2_gitlens@2" }, { + "command": "gitlens.explorers.openChangesWithWorking", + "when": "viewItem =~ /gitlens:file\\b/", + "group": "2_gitlens@3" + }, + { "command": "gitlens.explorers.openFile", "when": "viewItem =~ /gitlens:(file|history-file|status:file)\\b/", "group": "3_gitlens@1" diff --git a/src/commands/common.ts b/src/commands/common.ts index 37966b0..8beb7be 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -180,23 +180,55 @@ export interface CommandViewContext extends CommandBaseContext { export function isCommandViewContextWithBranch( context: CommandContext ): context is CommandViewContext & { node: ExplorerNode & { branch: GitBranch } } { - return ( - context.type === 'view' && (context.node as ExplorerNode & { branch?: GitBranch }).branch instanceof GitBranch - ); + if (context.type !== 'view') return false; + + return (context.node as ExplorerNode & { branch: GitBranch }).branch instanceof GitBranch; } export function isCommandViewContextWithCommit( context: CommandContext ): context is CommandViewContext & { node: ExplorerNode & { commit: T } } { + if (context.type !== 'view') return false; + + return (context.node as ExplorerNode & { commit: GitCommit }).commit instanceof GitCommit; +} + +export function isCommandViewContextWithFile( + context: CommandContext +): context is CommandViewContext & { node: ExplorerNode & { file: GitFile; repoPath: string } } { + if (context.type !== 'view') return false; + + const node = context.node as ExplorerNode & { file: GitFile; repoPath: string }; + return node.file !== undefined && (node.file.repoPath !== undefined || node.repoPath !== undefined); +} + +export function isCommandViewContextWithFileCommit( + context: CommandContext +): context is CommandViewContext & { node: ExplorerNode & { commit: GitCommit; file: GitFile; repoPath: string } } { + if (context.type !== 'view') return false; + + const node = context.node as ExplorerNode & { commit: GitCommit; file: GitFile; repoPath: string }; return ( - context.type === 'view' && (context.node as ExplorerNode & { commit?: GitCommit }).commit instanceof GitCommit + node.file !== undefined && + node.commit instanceof GitCommit && + (node.file.repoPath !== undefined || node.repoPath !== undefined) ); } -export function isCommandViewContextWithFile( +export function isCommandViewContextWithFileRefs( context: CommandContext -): context is CommandViewContext & { node: ExplorerNode & { file: GitFile } } { - return context.type === 'view' && (context.node as ExplorerNode & { file?: GitFile }).file !== undefined; +): context is CommandViewContext & { + node: ExplorerNode & { file: GitFile; ref1: string; ref2: string; repoPath: string }; +} { + if (context.type !== 'view') return false; + + const node = context.node as ExplorerNode & { file: GitFile; ref1: string; ref2: string; repoPath: string }; + return ( + node.file !== undefined && + node.ref1 !== undefined && + node.ref2 !== undefined && + (node.file.repoPath !== undefined || node.repoPath !== undefined) + ); } export function isCommandViewContextWithRef( @@ -208,15 +240,17 @@ export function isCommandViewContextWithRef( export function isCommandViewContextWithRemote( context: CommandContext ): context is CommandViewContext & { node: ExplorerNode & { remote: GitRemote } } { - return ( - context.type === 'view' && (context.node as ExplorerNode & { remote?: GitRemote }).remote instanceof GitRemote - ); + if (context.type !== 'view') return false; + + return (context.node as ExplorerNode & { remote: GitRemote }).remote instanceof GitRemote; } export function isCommandViewContextWithRepo( context: CommandContext ): context is CommandViewContext & { node: ExplorerNode & { repo: Repository } } { - return context.type === 'view' && (context.node as ExplorerNode & { repo?: Repository }).repo instanceof Repository; + if (context.type !== 'view') return false; + + return (context.node as ExplorerNode & { repo?: Repository }).repo instanceof Repository; } export type CommandContext = diff --git a/src/commands/externalDiff.ts b/src/commands/externalDiff.ts index 10d60fe..519e0f8 100644 --- a/src/commands/externalDiff.ts +++ b/src/commands/externalDiff.ts @@ -2,10 +2,18 @@ import { commands, SourceControlResourceState, Uri, window } from 'vscode'; import { BuiltInCommands, GlyphChars } from '../constants'; import { Container } from '../container'; +import { GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { Arrays } from '../system'; -import { Command, CommandContext, Commands, getRepoPathOrPrompt } from './common'; +import { + Command, + CommandContext, + Commands, + getRepoPathOrPrompt, + isCommandViewContextWithFileCommit, + isCommandViewContextWithFileRefs +} from './common'; enum Status { INDEX_MODIFIED, @@ -42,7 +50,9 @@ interface Resource extends SourceControlResourceState { class ExternalDiffFile { constructor( public readonly uri: Uri, - public readonly staged: boolean + public readonly staged: boolean, + public readonly ref1?: string, + public readonly ref2?: string ) {} } @@ -56,6 +66,41 @@ export class ExternalDiffCommand extends Command { } protected async preExecute(context: CommandContext, args: ExternalDiffCommandArgs = {}): Promise { + if (isCommandViewContextWithFileCommit(context)) { + args = { ...args }; + + const ref1 = GitService.isUncommitted(context.node.commit.previousFileSha) + ? '' + : context.node.commit.previousFileSha; + const ref2 = context.node.commit.isUncommitted ? '' : context.node.commit.sha; + + args.files = [ + new ExternalDiffFile( + GitUri.fromFile(context.node.file, context.node.file.repoPath || context.node.repoPath), + context.node.commit.isStagedUncommitted || context.node.file.indexStatus !== undefined, + ref1, + ref2 + ) + ]; + + return this.execute(args); + } + + if (isCommandViewContextWithFileRefs(context)) { + args = { ...args }; + + args.files = [ + new ExternalDiffFile( + GitUri.fromFile(context.node.file, context.node.file.repoPath || context.node.repoPath), + context.node.file.indexStatus !== undefined, + context.node.ref1, + context.node.ref2 + ) + ]; + + return this.execute(args); + } + if (args.files === undefined) { if (context.type === 'scm-states') { args = { ...args }; @@ -162,7 +207,12 @@ export class ExternalDiffCommand extends Command { } for (const file of args.files) { - void Container.git.openDiffTool(repoPath, file.uri, file.staged, tool); + void Container.git.openDiffTool(repoPath, file.uri, { + ref1: file.ref1, + ref2: file.ref2, + staged: file.staged, + tool: tool + }); } return undefined; diff --git a/src/commands/stashSave.ts b/src/commands/stashSave.ts index 0af88f2..83ce145 100644 --- a/src/commands/stashSave.ts +++ b/src/commands/stashSave.ts @@ -31,7 +31,7 @@ export class StashSaveCommand extends Command { protected async preExecute(context: CommandContext, args: StashSaveCommandArgs = {}): Promise { if (isCommandViewContextWithFile(context)) { args = { ...args }; - args.uris = [GitUri.fromFile(context.node.file, context.node.file.repoPath)]; + args.uris = [GitUri.fromFile(context.node.file, context.node.file.repoPath || context.node.repoPath)]; } else if (isCommandViewContextWithRepo(context)) { args = { ...args }; diff --git a/src/git/git.ts b/src/git/git.ts index 84cd1dd..2068e59 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -433,11 +433,22 @@ export class Git { return git({ cwd: repoPath }, ...params); } - static difftool_fileDiff(repoPath: string, fileName: string, tool: string, staged: boolean) { + static difftool_fileDiff( + repoPath: string, + fileName: string, + tool: string, + options: { ref1?: string; ref2?: string; staged?: boolean } = {} + ) { const params = ['difftool', '--no-prompt', `--tool=${tool}`]; - if (staged) { + if (options.staged) { params.push('--staged'); } + if (options.ref1) { + params.push(options.ref1); + } + if (options.ref2) { + params.push(options.ref2); + } params.push('--', fileName); return git({ cwd: repoPath }, ...params); diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 52b3a0f..4ad321f 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -1827,15 +1827,20 @@ export class GitService implements Disposable { return (await Git.config_get('diff.guitool', repoPath)) || (await Git.config_get('diff.tool', repoPath)); } - async openDiffTool(repoPath: string, uri: Uri, staged: boolean, tool?: string) { - if (!tool) { - tool = await this.getDiffTool(repoPath); - if (tool === undefined) throw new Error('No diff tool found'); + async openDiffTool( + repoPath: string, + uri: Uri, + options: { ref1?: string; ref2?: string; staged?: boolean; tool?: string } = {} + ) { + if (!options.tool) { + options.tool = await this.getDiffTool(repoPath); + if (options.tool === undefined) throw new Error('No diff tool found'); } - Logger.log(`openDiffTool('${repoPath}', '${uri.fsPath}', ${staged}, '${tool}')`); + const { tool, ...opts } = options; + Logger.log(`openDiffTool('${repoPath}', '${uri.fsPath}', '${tool}', ${opts})`); - return Git.difftool_fileDiff(repoPath, uri.fsPath, tool, staged); + return Git.difftool_fileDiff(repoPath, uri.fsPath, tool, opts); } async openDirectoryDiff(repoPath: string, ref1: string, ref2?: string, tool?: string) { diff --git a/src/git/models/file.ts b/src/git/models/file.ts index ac111e2..258121c 100644 --- a/src/git/models/file.ts +++ b/src/git/models/file.ts @@ -8,9 +8,9 @@ export declare type GitFileStatus = '!' | '?' | 'A' | 'C' | 'D' | 'M' | 'R' | 'T export interface GitFile { status: GitFileStatus; - readonly repoPath: string; - readonly indexStatus: GitFileStatus | undefined; - readonly workingTreeStatus: GitFileStatus | undefined; + readonly repoPath?: string; + readonly indexStatus?: GitFileStatus; + readonly workingTreeStatus?: GitFileStatus; readonly fileName: string; readonly originalFileName?: string; } diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index 1f91f32..40dec1a 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -158,7 +158,7 @@ export class GitDiffParser { fileName: (' ' + match[2]).substr(1), // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 originalFileName: match[3] === undefined ? undefined : (' ' + match[3]).substr(1) - } as GitFile); + }); } while (match != null); if (!files.length) return undefined; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index ba4f164..7b264b7 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -148,7 +148,7 @@ export class GitLogParser { status: line[0] as GitFileStatus, fileName: line.substring(1), originalFileName: undefined - } as GitFile; + }; this.parseFileName(status); if (status.fileName) { @@ -274,10 +274,10 @@ export class GitLogParser { if (type === GitCommitType.File) { entry.files = [ { - status: entry.status, + status: entry.status!, fileName: relativeFileName, originalFileName: originalFileName - } as GitFile + } ]; } diff --git a/src/git/parsers/stashParser.ts b/src/git/parsers/stashParser.ts index 64536b6..51e1d5e 100644 --- a/src/git/parsers/stashParser.ts +++ b/src/git/parsers/stashParser.ts @@ -102,7 +102,7 @@ export class GitStashParser { status: line[0] as GitFileStatus, fileName: line.substring(1), originalFileName: undefined - } as GitFile; + }; GitLogParser.parseFileName(status); if (status.fileName) { diff --git a/src/views/nodes/resultsFileNode.ts b/src/views/nodes/resultsFileNode.ts index e236b11..f71f272 100644 --- a/src/views/nodes/resultsFileNode.ts +++ b/src/views/nodes/resultsFileNode.ts @@ -10,13 +10,13 @@ import { ExplorerNode, ResourceType } from './explorerNode'; export class ResultsFileNode extends ExplorerNode { constructor( public readonly repoPath: string, - private readonly _file: GitFile, - private readonly _ref1: string, - private readonly _ref2: string, + public readonly file: GitFile, + public readonly ref1: string, + public readonly ref2: string, parent: ExplorerNode, public readonly explorer: Explorer ) { - super(GitUri.fromFile(_file, repoPath, _ref1 ? _ref1 : _ref2 ? _ref2 : undefined), parent); + super(GitUri.fromFile(file, repoPath, ref1 ? ref1 : ref2 ? ref2 : undefined), parent); } getChildren(): ExplorerNode[] { @@ -26,9 +26,9 @@ export class ResultsFileNode extends ExplorerNode { getTreeItem(): TreeItem { const item = new TreeItem(this.label, TreeItemCollapsibleState.None); item.contextValue = ResourceType.ResultsFile; - item.tooltip = StatusFileFormatter.fromTemplate('${file}\n${directory}/\n\n${status}', this._file); + item.tooltip = StatusFileFormatter.fromTemplate('${file}\n${directory}/\n\n${status}', this.file); - const statusIcon = GitFile.getStatusIcon(this._file.status); + const statusIcon = GitFile.getStatusIcon(this.file.status); item.iconPath = { dark: Container.context.asAbsolutePath(path.join('images', 'dark', statusIcon)), light: Container.context.asAbsolutePath(path.join('images', 'light', statusIcon)) @@ -49,7 +49,7 @@ export class ResultsFileNode extends ExplorerNode { private _label: string | undefined; get label() { if (this._label === undefined) { - this._label = StatusFileFormatter.fromTemplate('${filePath}', this._file, { + this._label = StatusFileFormatter.fromTemplate('${filePath}', this.file, { relativePath: this.relativePath } as IStatusFormatOptions); } @@ -77,14 +77,14 @@ export class ResultsFileNode extends ExplorerNode { this.uri, { lhs: { - sha: this._ref1, + sha: this.ref1, uri: this.uri }, rhs: { - sha: this._ref2, + sha: this.ref2, uri: - this._file.status === 'R' - ? GitUri.fromFile(this._file, this.uri.repoPath!, this._ref2, true) + this.file.status === 'R' + ? GitUri.fromFile(this.file, this.uri.repoPath!, this.ref2, true) : this.uri }, repoPath: this.uri.repoPath!,