From 61e3708233407e290eb3dc41d69e76b961ea25f2 Mon Sep 17 00:00:00 2001 From: Chris Kaczor Date: Wed, 27 Sep 2017 13:04:17 -0400 Subject: [PATCH] Add "external diff" command to SCM groups and files --- package.json | 21 ++++++-- src/commands.ts | 1 + src/commands/common.ts | 2 + src/commands/externalDiff.ts | 115 ++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 2 + src/git/git.ts | 11 ++++ src/git/models/status.ts | 4 +- src/git/parsers/statusParser.ts | 10 ++-- src/gitService.ts | 6 +++ 9 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 src/commands/externalDiff.ts diff --git a/package.json b/package.json index 7442318..4d5e7c0 100644 --- a/package.json +++ b/package.json @@ -1069,6 +1069,11 @@ } }, { + "command": "gitlens.externalDiff", + "title": "Open Changes (with difftool)", + "category": "GitLens" + }, + { "command": "gitlens.resetSuppressedWarnings", "title": "Reset Suppressed Warnings", "category": "GitLens" @@ -1520,17 +1525,22 @@ { "command": "gitlens.openChangedFiles", "when": "gitlens:enabled", - "group": "1_gitlens@1" + "group": "2_gitlens@1" }, { "command": "gitlens.closeUnchangedFiles", "when": "gitlens:enabled", - "group": "1_gitlens@2" + "group": "2_gitlens@2" + }, + { + "command": "gitlens.externalDiff", + "when": "gitlens:enabled", + "group": "2_gitlens@3" }, { "command": "gitlens.stashSave", "when": "gitlens:enabled", - "group": "2_gitlens@1" + "group": "3_gitlens@1" } ], "scm/resourceState/context": [ @@ -1540,6 +1550,11 @@ "group": "navigation" }, { + "command": "gitlens.externalDiff", + "when": "gitlens:enabled", + "group": "navigation" + }, + { "command": "gitlens.diffWithRevision", "when": "gitlens:enabled", "group": "1_gitlens@1" diff --git a/src/commands.ts b/src/commands.ts index 89427c3..a5b573a 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -14,6 +14,7 @@ export * from './commands/diffWithNext'; export * from './commands/diffWithPrevious'; export * from './commands/diffWithRevision'; export * from './commands/diffWithWorking'; +export * from './commands/externalDiff'; export * from './commands/openChangedFiles'; export * from './commands/openBranchesInRemote'; export * from './commands/openBranchInRemote'; diff --git a/src/commands/common.ts b/src/commands/common.ts index 8d026f4..2871c06 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -19,6 +19,7 @@ export type Commands = 'gitlens.diffWithRevision' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | + 'gitlens.externalDiff' | 'gitlens.openChangedFiles' | 'gitlens.openBranchesInRemote' | 'gitlens.openBranchInRemote' | @@ -61,6 +62,7 @@ export const Commands = { DiffWithRevision: 'gitlens.diffWithRevision' as Commands, DiffWithWorking: 'gitlens.diffWithWorking' as Commands, DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands, + ExternalDiff: 'gitlens.externalDiff' as Commands, OpenChangedFiles: 'gitlens.openChangedFiles' as Commands, OpenBranchesInRemote: 'gitlens.openBranchesInRemote' as Commands, OpenBranchInRemote: 'gitlens.openBranchInRemote' as Commands, diff --git a/src/commands/externalDiff.ts b/src/commands/externalDiff.ts new file mode 100644 index 0000000..d7bb16e --- /dev/null +++ b/src/commands/externalDiff.ts @@ -0,0 +1,115 @@ +'use strict'; +import { commands, SourceControlResourceState, Uri, window } from 'vscode'; +import { Command, Commands } from './common'; +import { BuiltInCommands } from '../constants'; +import { CommandContext } from '../commands'; +import { GitService } from '../gitService'; +import { Logger } from '../logger'; +import { Messages } from '../messages'; + +enum Status { + INDEX_MODIFIED, + INDEX_ADDED, + INDEX_DELETED, + INDEX_RENAMED, + INDEX_COPIED, + + MODIFIED, + DELETED, + UNTRACKED, + IGNORED, + + ADDED_BY_US, + ADDED_BY_THEM, + DELETED_BY_US, + DELETED_BY_THEM, + BOTH_ADDED, + BOTH_DELETED, + BOTH_MODIFIED +} + +enum ResourceGroupType { + Merge, + Index, + WorkingTree +} + +interface Resource extends SourceControlResourceState { + readonly resourceGroupType: ResourceGroupType; + readonly type: Status; +} + +class ExternalDiffFile { + constructor(public uri: Uri, public staged: boolean) { + } +} + +export interface ExternalDiffCommandArgs { + files?: ExternalDiffFile[]; +} + +export class ExternalDiffCommand extends Command { + constructor(private git: GitService) { + super(Commands.ExternalDiff); + } + + protected async preExecute(context: CommandContext, args: ExternalDiffCommandArgs = {}): Promise { + if (context.type === 'scm-states') { + args = { ...args }; + args.files = context.scmResourceStates.map((_: Resource) => new ExternalDiffFile(_.resourceUri, _.resourceGroupType === ResourceGroupType.Index)); + + return this.execute(args); + } else if (context.type === 'scm-groups') { + const isModified = (status: Status): boolean => status === Status.BOTH_MODIFIED || status === Status.INDEX_MODIFIED || status === Status.MODIFIED; + + args = { ...args }; + args.files = context.scmResourceGroups[0].resourceStates.filter((_: Resource) => isModified(_.type)).map((_: Resource) => new ExternalDiffFile(_.resourceUri, _.resourceGroupType === ResourceGroupType.Index)); + + return this.execute(args); + } + + return this.execute(args); + } + + async execute(args: ExternalDiffCommandArgs = {}) { + try { + const diffTool = await this.git.getConfig('diff.tool'); + if (!diffTool) { + const result = await window.showWarningMessage(`Unable to open file compare because there is no Git diff tool configured`, 'View Git Docs'); + if (!result) return undefined; + + return commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://git-scm.com/docs/git-config#git-config-difftool')); + } + + const repoPath = await this.git.getRepoPathFromUri(undefined); + if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open changed files`); + + if (!args.files) { + const status = await this.git.getStatusForRepo(repoPath); + if (status === undefined) return window.showWarningMessage(`Unable to open changed files`); + + args.files = []; + + for (const file of status.files) { + if (file.indexStatus === 'M') { + args.files.push(new ExternalDiffFile(file.Uri, true)); + } + + if (file.workTreeStatus === 'M') { + args.files.push(new ExternalDiffFile(file.Uri, false)); + } + } + } + + for (const file of args.files) { + this.git.openDiffTool(repoPath, file.uri, file.staged); + } + + return undefined; + } + catch (ex) { + Logger.error(ex, 'ExternalDiffCommand'); + return window.showErrorMessage(`Unable to open external diff. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 10282e4..94be411 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import { commands, ExtensionContext, extensions, languages, window, workspace } from 'vscode'; import { AnnotationController } from './annotations/annotationController'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; +import { ExternalDiffCommand } from './commands'; import { OpenBranchesInRemoteCommand, OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands'; @@ -99,6 +100,7 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(commands.registerTextEditorCommand('gitlens.computingFileAnnotations', () => { })); context.subscriptions.push(new CloseUnchangedFilesCommand(git)); + context.subscriptions.push(new ExternalDiffCommand(git)); context.subscriptions.push(new OpenChangedFilesCommand(git)); context.subscriptions.push(new CopyMessageToClipboardCommand(git)); context.subscriptions.push(new CopyShaToClipboardCommand(git)); diff --git a/src/git/git.ts b/src/git/git.ts index 43b54e5..8fb0128 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -276,6 +276,17 @@ export class Git { return gitCommand({ cwd: repoPath }, ...params); } + static difftool_fileDiff(repoPath: string, fileName: string, staged: boolean) { + const params = [`difftool`, `--no-prompt`]; + if (staged) { + params.push('--staged'); + } + params.push('--'); + params.push(fileName); + + return gitCommand({ cwd: repoPath }, ...params); + } + static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) { const params = [...defaultLogParams, `-m`]; if (maxCount && !reverse) { diff --git a/src/git/models/status.ts b/src/git/models/status.ts index 8a6a052..6b506a4 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -26,6 +26,8 @@ export interface IGitStatusFile { status: GitStatusFileStatus; fileName: string; originalFileName?: string; + workTreeStatus: GitStatusFileStatus; + indexStatus: GitStatusFileStatus; } export interface IGitStatusFileWithCommit extends IGitStatusFile { @@ -36,7 +38,7 @@ export class GitStatusFile implements IGitStatusFile { originalFileName?: string; - constructor(public repoPath: string, public status: GitStatusFileStatus, public fileName: string, public staged: boolean, originalFileName?: string) { + constructor(public repoPath: string, public status: GitStatusFileStatus, public workTreeStatus: GitStatusFileStatus, public indexStatus: GitStatusFileStatus, public fileName: string, public staged: boolean, originalFileName?: string) { this.originalFileName = originalFileName; } diff --git a/src/git/parsers/statusParser.ts b/src/git/parsers/statusParser.ts index 89c096b..f0c1443 100644 --- a/src/git/parsers/statusParser.ts +++ b/src/git/parsers/statusParser.ts @@ -6,6 +6,8 @@ interface FileStatusEntry { status: GitStatusFileStatus; fileName: string; originalFileName: string; + workTreeStatus: GitStatusFileStatus; + indexStatus: GitStatusFileStatus; } const aheadStatusV1Regex = /(?:ahead ([0-9]+))/; @@ -69,7 +71,7 @@ export class GitStatusParser { else { entry = this._parseFileEntry(rawStatus, fileName); } - status.files.push(new GitStatusFile(repoPath, entry.status, entry.fileName, entry.staged, entry.originalFileName)); + status.files.push(new GitStatusFile(repoPath, entry.status, entry.workTreeStatus, entry.indexStatus, entry.fileName, entry.staged, entry.originalFileName)); } } } @@ -117,7 +119,7 @@ export class GitStatusParser { } if (entry !== undefined) { - status.files.push(new GitStatusFile(repoPath, entry.status, entry.fileName, entry.staged, entry.originalFileName)); + status.files.push(new GitStatusFile(repoPath, entry.status, entry.workTreeStatus, entry.indexStatus, entry.fileName, entry.staged, entry.originalFileName)); } } } @@ -131,7 +133,9 @@ export class GitStatusParser { status: (indexStatus || workTreeStatus || '?') as GitStatusFileStatus, fileName: fileName, originalFileName: originalFileName, - staged: !!indexStatus + staged: !!indexStatus, + indexStatus: indexStatus, + workTreeStatus: workTreeStatus } as FileStatusEntry; } } \ No newline at end of file diff --git a/src/gitService.ts b/src/gitService.ts index 41b3598..2c35f92 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -1036,6 +1036,12 @@ export class GitService extends Disposable { return !!result; } + openDiffTool(repoPath: string, uri: Uri, staged: boolean) { + Logger.log(`openDiffTool('${repoPath}', '${uri}', ${staged})`); + + return Git.difftool_fileDiff(repoPath, uri.fsPath, staged); + } + openDirectoryDiff(repoPath: string, sha1: string, sha2?: string) { Logger.log(`openDirectoryDiff('${repoPath}', ${sha1}, ${sha2})`);