From 1acc1836210ccda414b36aed2f54eb6dfbbcf3d6 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 14 May 2017 01:48:07 -0400 Subject: [PATCH] Refactors commands to use typed args objects --- src/commands/closeUnchangedFiles.ts | 22 +- src/commands/common.ts | 18 +- src/commands/copyMessageToClipboard.ts | 33 +- src/commands/copyShaToClipboard.ts | 26 +- src/commands/diffDirectory.ts | 28 +- src/commands/diffLineWithPrevious.ts | 68 +- src/commands/diffLineWithWorking.ts | 41 +- src/commands/diffWithBranch.ts | 42 +- src/commands/diffWithNext.ts | 66 +- src/commands/diffWithPrevious.ts | 65 +- src/commands/diffWithWorking.ts | 42 +- src/commands/keyboard.ts | 275 ++- src/commands/openChangedFiles.ts | 26 +- src/commands/openCommitInRemote.ts | 24 +- src/commands/openFileInRemote.ts | 25 +- src/commands/openInRemote.ts | 63 +- src/commands/showBlame.ts | 14 +- src/commands/showBlameHistory.ts | 29 +- src/commands/showCommitSearch.ts | 94 +- src/commands/showFileHistory.ts | 26 +- src/commands/showQuickBranchHistory.ts | 75 +- src/commands/showQuickCommitDetails.ts | 82 +- src/commands/showQuickCommitFileDetails.ts | 91 +- src/commands/showQuickCurrentBranchHistory.ts | 22 +- src/commands/showQuickFileHistory.ts | 62 +- src/commands/showQuickRepoStatus.ts | 22 +- src/commands/showQuickStashList.ts | 38 +- src/commands/stashApply.ts | 34 +- src/commands/stashDelete.ts | 24 +- src/commands/stashSave.ts | 21 +- src/commands/toggleBlame.ts | 14 +- src/git/remotes/provider.ts | 26 +- src/gitCodeLensProvider.ts | 48 +- src/gitRevisionCodeLensProvider.ts | 14 +- src/gitService.ts | 1874 +++++++++-------- src/quickPicks/branchHistory.ts | 60 +- src/quickPicks/commitDetails.ts | 123 +- src/quickPicks/commitFileDetails.ts | 128 +- src/quickPicks/common.ts | 64 +- src/quickPicks/fileHistory.ts | 76 +- src/quickPicks/remotes.ts | 110 +- src/quickPicks/repoStatus.ts | 76 +- src/quickPicks/stashList.ts | 16 +- 43 files changed, 2366 insertions(+), 1761 deletions(-) diff --git a/src/commands/closeUnchangedFiles.ts b/src/commands/closeUnchangedFiles.ts index 9a25056..51a7c5b 100644 --- a/src/commands/closeUnchangedFiles.ts +++ b/src/commands/closeUnchangedFiles.ts @@ -1,31 +1,33 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; import { ActiveEditorTracker } from '../activeEditorTracker'; -import { ActiveEditorCommand, Commands } from './common'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { TextEditorComparer, UriComparer } from '../comparers'; import { GitService } from '../gitService'; import { Logger } from '../logger'; +export interface CloseUnchangedFilesCommandArgs { + uris?: Uri[]; +} + export class CloseUnchangedFilesCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.CloseUnchangedFiles); } - async execute(editor: TextEditor, uri?: Uri, uris?: Uri[]) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: CloseUnchangedFilesCommandArgs = {}) { + uri = getCommandUri(uri, editor); try { - if (!uris) { + if (args.uris === undefined) { const repoPath = await this.git.getRepoPathFromUri(uri); - if (!repoPath) return window.showWarningMessage(`Unable to close unchanged files`); + if (repoPath === undefined) return window.showWarningMessage(`Unable to close unchanged files`); const status = await this.git.getStatusForRepo(repoPath); - if (!status) return window.showWarningMessage(`Unable to close unchanged files`); + if (status === undefined) return window.showWarningMessage(`Unable to close unchanged files`); - uris = status.files.map(_ => _.Uri); + args.uris = status.files.map(_ => _.Uri); } const editorTracker = new ActiveEditorTracker(); @@ -35,7 +37,7 @@ export class CloseUnchangedFilesCommand extends ActiveEditorCommand { do { if (editor !== undefined) { if ((editor.document !== undefined && editor.document.isDirty) || - uris.some(_ => UriComparer.equals(_, editor!.document && editor!.document.uri))) { + args.uris.some(_ => UriComparer.equals(_, editor!.document && editor!.document.uri))) { // If we didn't start with a valid editor, set one once we find it if (active === undefined) { active = editor; diff --git a/src/commands/common.ts b/src/commands/common.ts index 15a0977..1d3eb4f 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -1,5 +1,5 @@ 'use strict'; -import { commands, Disposable, TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; +import { commands, Disposable, TextDocumentShowOptions, TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; import { BuiltInCommands } from '../constants'; import { Logger } from '../logger'; import { Telemetry } from '../telemetry'; @@ -48,6 +48,12 @@ export const Commands = { ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands }; +export function getCommandUri(uri?: Uri, editor?: TextEditor): Uri | undefined { + if (uri instanceof Uri) return uri; + if (editor === undefined || editor.document === undefined) return undefined; + return editor.document.uri; +} + export type CommandContext = 'gitlens:canToggleCodeLens' | 'gitlens:enabled' | 'gitlens:hasRemotes' | 'gitlens:isBlameable' | 'gitlens:isRepository' | 'gitlens:isTracked' | 'gitlens:key'; export const CommandContext = { CanToggleCodeLens: 'gitlens:canToggleCodeLens' as CommandContext, @@ -140,12 +146,16 @@ export abstract class ActiveEditorCachedCommand extends ActiveEditorCommand { abstract execute(editor: TextEditor, ...args: any[]): any; } -export async function openEditor(uri: Uri, pinned: boolean = false) { +export async function openEditor(uri: Uri, options?: TextDocumentShowOptions): Promise { try { - if (!pinned) return await commands.executeCommand(BuiltInCommands.Open, uri); + const defaults: TextDocumentShowOptions = { + preserveFocus: false, + preview: true, + viewColumn: (window.activeTextEditor && window.activeTextEditor.viewColumn) || 1 + }; const document = await workspace.openTextDocument(uri); - return window.showTextDocument(document, (window.activeTextEditor && window.activeTextEditor.viewColumn) || 1, true); + return window.showTextDocument(document, { ...defaults, ...(options || {}) }); } catch (ex) { Logger.error(ex, 'openEditor'); diff --git a/src/commands/copyMessageToClipboard.ts b/src/commands/copyMessageToClipboard.ts index d7a3a39..83fe955 100644 --- a/src/commands/copyMessageToClipboard.ts +++ b/src/commands/copyMessageToClipboard.ts @@ -1,40 +1,43 @@ 'use strict'; import { Iterables } from '../system'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { copy } from 'copy-paste'; +export interface CopyMessageToClipboardCommandArgs { + message?: string; + sha?: string; +} + export class CopyMessageToClipboardCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.CopyMessageToClipboard); } - async execute(editor: TextEditor, uri?: Uri, sha?: string, message?: string): Promise { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: CopyMessageToClipboardCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); try { // If we don't have an editor then get the message of the last commit to the branch - if (!uri) { + if (uri === undefined) { if (!this.git.repoPath) return undefined; const log = await this.git.getLogForRepo(this.git.repoPath, undefined, 1); if (!log) return undefined; - message = Iterables.first(log.commits.values()).message; - copy(message); + args.message = Iterables.first(log.commits.values()).message; + copy(args.message); return undefined; } const gitUri = await GitUri.fromUri(uri, this.git); - if (!message) { - if (!sha) { - if (editor && editor.document && editor.document.isDirty) return undefined; + if (args.message === undefined) { + if (args.sha === undefined) { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; const line = (editor && editor.selection.active.line) || gitUri.offset; const blameline = line - gitUri.offset; @@ -46,7 +49,7 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand { if (blame.commit.isUncommitted) return undefined; - sha = blame.commit.sha; + args.sha = blame.commit.sha; if (!gitUri.repoPath) { gitUri.repoPath = blame.commit.repoPath; } @@ -58,13 +61,13 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand { } // Get the full commit message -- since blame only returns the summary - const commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, sha); + const commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, args.sha); if (!commit) return undefined; - message = commit.message; + args.message = commit.message; } - copy(message); + copy(args.message); return undefined; } catch (ex) { diff --git a/src/commands/copyShaToClipboard.ts b/src/commands/copyShaToClipboard.ts index e6de0e6..ed6ec42 100644 --- a/src/commands/copyShaToClipboard.ts +++ b/src/commands/copyShaToClipboard.ts @@ -1,39 +1,41 @@ 'use strict'; import { Iterables } from '../system'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { copy } from 'copy-paste'; +export interface CopyShaToClipboardCommandArgs { + sha?: string; +} + export class CopyShaToClipboardCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.CopyShaToClipboard); } - async execute(editor: TextEditor, uri?: Uri, sha?: string): Promise { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: CopyShaToClipboardCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); try { // If we don't have an editor then get the sha of the last commit to the branch - if (!uri) { + if (uri === undefined) { if (!this.git.repoPath) return undefined; const log = await this.git.getLogForRepo(this.git.repoPath, undefined, 1); if (!log) return undefined; - sha = Iterables.first(log.commits.values()).sha; - copy(sha); + args.sha = Iterables.first(log.commits.values()).sha; + copy(args.sha); return undefined; } const gitUri = await GitUri.fromUri(uri, this.git); - if (!sha) { - if (editor && editor.document && editor.document.isDirty) return undefined; + if (args.sha === undefined) { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; const line = (editor && editor.selection.active.line) || gitUri.offset; const blameline = line - gitUri.offset; @@ -43,7 +45,7 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand { const blame = await this.git.getBlameForLine(gitUri, blameline); if (!blame) return undefined; - sha = blame.commit.sha; + args.sha = blame.commit.sha; } catch (ex) { Logger.error(ex, 'CopyShaToClipboardCommand', `getBlameForLine(${blameline})`); @@ -51,7 +53,7 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand { } } - copy(sha); + copy(args.sha); return undefined; } catch (ex) { diff --git a/src/commands/diffDirectory.ts b/src/commands/diffDirectory.ts index f8209f5..34db366 100644 --- a/src/commands/diffDirectory.ts +++ b/src/commands/diffDirectory.ts @@ -1,51 +1,53 @@ 'use strict'; import { Iterables } from '../system'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitService } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, BranchesQuickPick } from '../quickPicks'; +export interface DiffDirectoryCommandCommandArgs { + shaOrBranch1?: string; + shaOrBranch2?: string; +} + export class DiffDirectoryCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffDirectory); } - async execute(editor: TextEditor, uri?: Uri, shaOrBranch1?: string, shaOrBranch2?: string): Promise { + async execute(editor: TextEditor, uri?: Uri, args: DiffDirectoryCommandCommandArgs = {}): Promise { const diffTool = await this.git.getConfig('diff.tool'); if (!diffTool) { const result = await window.showWarningMessage(`Unable to open directory 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')); } - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + uri = getCommandUri(uri, editor); try { const repoPath = await this.git.getRepoPathFromUri(uri); if (!repoPath) return window.showWarningMessage(`Unable to open directory compare`); - if (!shaOrBranch1) { + if (!args.shaOrBranch1) { const branches = await this.git.getBranches(repoPath); const current = Iterables.find(branches, _ => _.current); if (current == null) return window.showWarningMessage(`Unable to open directory compare`); const pick = await BranchesQuickPick.show(branches, `Compare ${current.name} to \u2026`); - if (!pick) return undefined; + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); - shaOrBranch1 = pick.branch.name; - if (!shaOrBranch1) return undefined; + args.shaOrBranch1 = pick.branch.name; + if (args.shaOrBranch1 === undefined) return undefined; } - this.git.openDirectoryDiff(repoPath, shaOrBranch1, shaOrBranch2); + this.git.openDirectoryDiff(repoPath, args.shaOrBranch1, args.shaOrBranch2); return undefined; } catch (ex) { diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index f8da141..82a43d5 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -1,51 +1,63 @@ 'use strict'; -import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; +import { DiffWithPreviousCommandArgs } from './diffWithPrevious'; +import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { GitCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import * as path from 'path'; +export interface DiffLineWithPreviousCommandArgs { + commit?: GitCommit; + line?: number; + showOptions?: TextDocumentShowOptions; +} + export class DiffLineWithPreviousCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffLineWithPrevious); } - async execute(editor: TextEditor): Promise; - async execute(editor: TextEditor, uri: Uri): Promise; - async execute(editor: TextEditor, uri?: Uri, commit?: GitCommit, line?: number): Promise { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: DiffLineWithPreviousCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; const gitUri = await GitUri.fromUri(uri, this.git); - line = line || (editor && editor.selection.active.line) || gitUri.offset; + args.line = args.line || (editor === undefined ? gitUri.offset : editor.selection.active.line); - if (!commit || GitService.isUncommitted(commit.sha)) { - if (editor && editor.document && editor.document.isDirty) return undefined; + if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const blameline = line - gitUri.offset; + const blameline = args.line - gitUri.offset; if (blameline < 0) return undefined; try { const blame = await this.git.getBlameForLine(gitUri, blameline); - if (!blame) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); + if (blame === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); - commit = blame.commit; + args.commit = blame.commit; // If we don't have a sha or the current commit matches the blame, show the previous - if (!gitUri.sha || gitUri.sha === commit.sha) { - return commands.executeCommand(Commands.DiffWithPrevious, new GitUri(uri, commit), undefined, line); + if (gitUri.sha === undefined || gitUri.sha === args.commit.sha) { + return commands.executeCommand(Commands.DiffWithPrevious, new GitUri(uri, args.commit), { + line: args.line, + showOptions: args.showOptions + } as DiffWithPreviousCommandArgs); } // If the line is uncommitted, find the previous commit and treat it as a DiffWithWorking - if (commit.isUncommitted) { - uri = commit.uri; - commit = new GitCommit(commit.type, commit.repoPath, commit.previousSha!, commit.previousFileName!, commit.author, commit.date, commit.message); - line = (blame.line.line + 1) + gitUri.offset; - return commands.executeCommand(Commands.DiffWithWorking, uri, commit, line); + if (args.commit.isUncommitted) { + uri = args.commit.uri; + args.commit = new GitCommit(args.commit.type, args.commit.repoPath, args.commit.previousSha!, args.commit.previousFileName!, args.commit.author, args.commit.date, args.commit.message); + args.line = (blame.line.line + 1) + gitUri.offset; + + return commands.executeCommand(Commands.DiffWithWorking, uri, { + commit: args.commit, + line: args.line, + showOptions: args.showOptions + } as DiffWithWorkingCommandArgs); } } catch (ex) { @@ -57,11 +69,17 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand { try { const [rhs, lhs] = await Promise.all([ this.git.getVersionedFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha!), - this.git.getVersionedFile(commit.repoPath, commit.uri.fsPath, commit.sha) + this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha) ]); - await commands.executeCommand(BuiltInCommands.Diff, Uri.file(lhs), Uri.file(rhs), `${path.basename(commit.uri.fsPath)} (${commit.shortSha}) \u2194 ${path.basename(gitUri.fsPath)} (${gitUri.shortSha})`); + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(lhs), + Uri.file(rhs), + `${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha}) \u2194 ${path.basename(gitUri.fsPath)} (${gitUri.shortSha})`, + args.showOptions); + // TODO: Figure out how to focus the left pane - return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: args.line, at: 'center' }); } catch (ex) { Logger.error(ex, 'DiffWithPreviousLineCommand', 'getVersionedFile'); diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index 1771aa3..de7db94 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -1,41 +1,44 @@ 'use strict'; -import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; +import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { GitCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; +export interface DiffLineWithWorkingCommandArgs { + commit?: GitCommit; + line?: number; + showOptions?: TextDocumentShowOptions; +} + export class DiffLineWithWorkingCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffLineWithWorking); } - async execute(editor: TextEditor): Promise; - async execute(editor: TextEditor, uri: Uri): Promise; - async execute(editor: TextEditor, uri?: Uri, commit?: GitCommit, line?: number): Promise { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: DiffLineWithWorkingCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; const gitUri = await GitUri.fromUri(uri, this.git); - line = line || (editor && editor.selection.active.line) || gitUri.offset; + args.line = args.line || (editor === undefined ? gitUri.offset : editor.selection.active.line); - if (!commit || GitService.isUncommitted(commit.sha)) { - if (editor && editor.document && editor.document.isDirty) return undefined; + if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const blameline = line - gitUri.offset; + const blameline = args.line - gitUri.offset; if (blameline < 0) return undefined; try { const blame = await this.git.getBlameForLine(gitUri, blameline); - if (!blame) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); + if (blame === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); - commit = blame.commit; + args.commit = blame.commit; // If the line is uncommitted, find the previous commit - if (commit.isUncommitted) { - commit = new GitCommit(commit.type, commit.repoPath, commit.previousSha!, commit.previousFileName!, commit.author, commit.date, commit.message); - line = blame.line.line + 1 + gitUri.offset; + if (args.commit.isUncommitted) { + args.commit = new GitCommit(args.commit.type, args.commit.repoPath, args.commit.previousSha!, args.commit.previousFileName!, args.commit.author, args.commit.date, args.commit.message); + args.line = blame.line.line + 1 + gitUri.offset; } } catch (ex) { @@ -44,6 +47,6 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { } } - return commands.executeCommand(Commands.DiffWithWorking, uri, commit, line); + return commands.executeCommand(Commands.DiffWithWorking, uri, args as DiffWithWorkingCommandArgs); } } diff --git a/src/commands/diffWithBranch.ts b/src/commands/diffWithBranch.ts index 88345bd..517eb61 100644 --- a/src/commands/diffWithBranch.ts +++ b/src/commands/diffWithBranch.ts @@ -1,44 +1,54 @@ 'use strict'; -import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, BranchesQuickPick } from '../quickPicks'; import * as path from 'path'; +export interface DiffWithBranchCommandArgs { + line?: number; + showOptions?: TextDocumentShowOptions; + + goBackCommand?: CommandQuickPickItem; +} + export class DiffWithBranchCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffWithBranch); } - async execute(editor: TextEditor, uri?: Uri, goBackCommand?: CommandQuickPickItem): Promise { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: DiffWithBranchCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - const line = (editor && editor.selection.active.line) || 0; + args.line = args.line || (editor === undefined ? 0 : editor.selection.active.line); const gitUri = await GitUri.fromUri(uri, this.git); if (gitUri.repoPath === undefined) return undefined; const branches = await this.git.getBranches(gitUri.repoPath); - const pick = await BranchesQuickPick.show(branches, `Compare ${path.basename(gitUri.fsPath)} to \u2026`, goBackCommand); - if (!pick) return undefined; + const pick = await BranchesQuickPick.show(branches, `Compare ${path.basename(gitUri.fsPath)} to \u2026`, args.goBackCommand); + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); const branch = pick.branch.name; - if (!branch) return undefined; + if (branch === undefined) return undefined; try { const compare = await this.git.getVersionedFile(gitUri.repoPath, gitUri.fsPath, branch); - await commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), gitUri.fileUri(), `${path.basename(gitUri.fsPath)} (${branch}) \u2194 ${path.basename(gitUri.fsPath)}`); - return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(compare), + gitUri.fileUri(), + `${path.basename(gitUri.fsPath)} (${branch}) \u2194 ${path.basename(gitUri.fsPath)}`, + args.showOptions); + + // TODO: Figure out how to focus the left pane + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: args.line, at: 'center' }); } catch (ex) { Logger.error(ex, 'DiffWithBranchCommand', 'getVersionedFile'); diff --git a/src/commands/diffWithNext.ts b/src/commands/diffWithNext.ts index 6db8264..45e1e94 100644 --- a/src/commands/diffWithNext.ts +++ b/src/commands/diffWithNext.ts @@ -1,51 +1,46 @@ 'use strict'; import { Iterables } from '../system'; -import { commands, Range, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitLogCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import * as path from 'path'; +export interface DiffWithNextCommandArgs { + commit?: GitLogCommit; + line?: number; + range?: Range; + showOptions?: TextDocumentShowOptions; +} + export class DiffWithNextCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffWithNext); } - async execute(editor: TextEditor): Promise; - async execute(editor: TextEditor, uri: Uri): Promise; - async execute(editor: TextEditor, uri: Uri, commit: GitLogCommit, range?: Range): Promise; - async execute(editor: TextEditor, uri: Uri, commit: GitLogCommit, line?: number): Promise; - async execute(editor: TextEditor, uri?: Uri, commit?: GitLogCommit, rangeOrLine?: Range | number): Promise { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: DiffWithNextCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - let line = (editor && editor.selection.active.line) || 0; - if (typeof rangeOrLine === 'number') { - line = rangeOrLine || line; - rangeOrLine = undefined; - } + args.line = args.line || (editor === undefined ? 0 : editor.selection.active.line); - if (!commit || !(commit instanceof GitLogCommit) || rangeOrLine instanceof Range) { + if (args.commit === undefined || !(args.commit instanceof GitLogCommit) || args.range !== undefined) { const gitUri = await GitUri.fromUri(uri, this.git); try { - if (!gitUri.sha) { - // If the file is uncommitted, treat it as a DiffWithWorking - if (await this.git.isFileUncommitted(gitUri)) { - return commands.executeCommand(Commands.DiffWithWorking, uri); - } + // If the sha is missing or the file is uncommitted, treat it as a DiffWithWorking + if (gitUri.sha === undefined && await this.git.isFileUncommitted(gitUri)) { + return commands.executeCommand(Commands.DiffWithWorking, uri); } - const sha = (commit && commit.sha) || gitUri.sha; + const sha = args.commit === undefined ? gitUri.sha : args.commit.sha; - const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, rangeOrLine!); - if (!log) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); + const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, args.range!); + if (log === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); - commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); + args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); } catch (ex) { Logger.error(ex, 'DiffWithNextCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`); @@ -53,17 +48,22 @@ export class DiffWithNextCommand extends ActiveEditorCommand { } } - if (!commit.nextSha) { - return commands.executeCommand(Commands.DiffWithWorking, uri); - } + if (args.commit.nextSha === undefined) return commands.executeCommand(Commands.DiffWithWorking, uri); try { const [rhs, lhs] = await Promise.all([ - this.git.getVersionedFile(commit.repoPath, commit.nextUri.fsPath, commit.nextSha), - this.git.getVersionedFile(commit.repoPath, commit.uri.fsPath, commit.sha) + this.git.getVersionedFile(args.commit.repoPath, args.commit.nextUri.fsPath, args.commit.nextSha), + this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha) ]); - await commands.executeCommand(BuiltInCommands.Diff, Uri.file(lhs), Uri.file(rhs), `${path.basename(commit.uri.fsPath)} (${commit.shortSha}) \u2194 ${path.basename(commit.nextUri.fsPath)} (${commit.nextShortSha})`); - return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(lhs), + Uri.file(rhs), + `${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha}) \u2194 ${path.basename(args.commit.nextUri.fsPath)} (${args.commit.nextShortSha})`, + args.showOptions); + + // TODO: Figure out how to focus the left pane + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: args.line, at: 'center' }); } catch (ex) { Logger.error(ex, 'DiffWithNextCommand', 'getVersionedFile'); diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index ced9b57..bd50225 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -1,52 +1,47 @@ 'use strict'; import { Iterables } from '../system'; -import { commands, Range, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import * as moment from 'moment'; import * as path from 'path'; +export interface DiffWithPreviousCommandArgs { + commit?: GitCommit; + line?: number; + range?: Range; + showOptions?: TextDocumentShowOptions; +} + export class DiffWithPreviousCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffWithPrevious); } - async execute(editor: TextEditor): Promise; - async execute(editor: TextEditor, uri: Uri): Promise; - async execute(editor: TextEditor, uri: Uri, commit: GitCommit, range?: Range): Promise; - async execute(editor: TextEditor, uri: Uri, commit: GitCommit, line?: number): Promise; - async execute(editor: TextEditor, uri?: Uri, commit?: GitCommit, rangeOrLine?: Range | number): Promise { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: DiffWithPreviousCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - let line = (editor && editor.selection.active.line) || 0; - if (typeof rangeOrLine === 'number') { - line = rangeOrLine || line; - rangeOrLine = undefined; - } + args.line = args.line || (editor === undefined ? 0 : editor.selection.active.line); - if (!commit || rangeOrLine instanceof Range) { + if (args.commit === undefined || args.range !== undefined) { const gitUri = await GitUri.fromUri(uri, this.git); try { - if (!gitUri.sha) { - // If the file is uncommitted, treat it as a DiffWithWorking - if (await this.git.isFileUncommitted(gitUri)) { - return commands.executeCommand(Commands.DiffWithWorking, uri); - } + // If the sha is missing or the file is uncommitted, treat it as a DiffWithWorking + if (gitUri.sha === undefined && await this.git.isFileUncommitted(gitUri)) { + return commands.executeCommand(Commands.DiffWithWorking, uri); } - const sha = (commit && commit.sha) || gitUri.sha; + const sha = args.commit === undefined ? gitUri.sha : args.commit.sha; - const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, rangeOrLine!); - if (!log) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); + const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, args.range!); + if (log === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); - commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); + args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); } catch (ex) { Logger.error(ex, 'DiffWithPreviousCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`); @@ -54,18 +49,22 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand { } } - if (!commit.previousSha) { - return window.showInformationMessage(`Commit ${commit.shortSha} (${commit.author}, ${moment(commit.date).fromNow()}) has no previous commit`); - } + if (args.commit.previousSha === undefined) return window.showInformationMessage(`Commit ${args.commit.shortSha} (${args.commit.author}, ${moment(args.commit.date).fromNow()}) has no previous commit`); try { const [rhs, lhs] = await Promise.all([ - this.git.getVersionedFile(commit.repoPath, commit.uri.fsPath, commit.sha), - this.git.getVersionedFile(commit.repoPath, commit.previousUri.fsPath, commit.previousSha) + this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha), + this.git.getVersionedFile(args.commit.repoPath, args.commit.previousUri.fsPath, args.commit.previousSha) ]); - await commands.executeCommand(BuiltInCommands.Diff, Uri.file(lhs), Uri.file(rhs), `${path.basename(commit.previousUri.fsPath)} (${commit.previousShortSha}) \u2194 ${path.basename(commit.uri.fsPath)} (${commit.shortSha})`); + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(lhs), + Uri.file(rhs), + `${path.basename(args.commit.previousUri.fsPath)} (${args.commit.previousShortSha}) \u2194 ${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha})`, + args.showOptions); + // TODO: Figure out how to focus the left pane - return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: args.line, at: 'center' }); } catch (ex) { Logger.error(ex, 'DiffWithPreviousCommand', 'getVersionedFile'); diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index 90f3992..e2d60e7 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -1,34 +1,35 @@ 'use strict'; -// import { Iterables } from '../system'; -import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import * as path from 'path'; +export interface DiffWithWorkingCommandArgs { + commit?: GitCommit; + line?: number; + showOptions?: TextDocumentShowOptions; +} + export class DiffWithWorkingCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.DiffWithWorking); } - async execute(editor: TextEditor): Promise; - async execute(editor: TextEditor, uri: Uri): Promise; - async execute(editor: TextEditor, uri?: Uri, commit?: GitCommit, line?: number): Promise { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: DiffWithWorkingCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - line = line || (editor && editor.selection.active.line) || 0; + args.line = args.line || (editor === undefined ? 0 : editor.selection.active.line); - if (!commit || GitService.isUncommitted(commit.sha)) { + if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) { const gitUri = await GitUri.fromUri(uri, this.git); try { - commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { firstIfMissing: true }); - if (!commit) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); + args.commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { firstIfMissing: true }); + if (args.commit === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); } catch (ex) { Logger.error(ex, 'DiffWithWorkingCommand', `getLogCommit(${gitUri.repoPath}, ${gitUri.fsPath}, ${gitUri.sha})`); @@ -42,9 +43,16 @@ export class DiffWithWorkingCommand extends ActiveEditorCommand { if (workingFileName === undefined) return undefined; try { - const compare = await this.git.getVersionedFile(commit.repoPath, commit.uri.fsPath, commit.sha); - await commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), Uri.file(path.resolve(gitUri.repoPath, workingFileName)), `${path.basename(commit.uri.fsPath)} (${commit.shortSha}) \u2194 ${path.basename(workingFileName)}`); - return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }); + const compare = await this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha); + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(compare), + Uri.file(path.resolve(gitUri.repoPath, workingFileName)), + `${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha}) \u2194 ${path.basename(workingFileName)}`, + args.showOptions); + + // TODO: Figure out how to focus the left pane + return await commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: args.line, at: 'center' }); } catch (ex) { Logger.error(ex, 'DiffWithWorkingCommand', 'getVersionedFile'); diff --git a/src/commands/keyboard.ts b/src/commands/keyboard.ts index 3869cb5..0a107d2 100644 --- a/src/commands/keyboard.ts +++ b/src/commands/keyboard.ts @@ -1,142 +1,137 @@ -'use strict'; -import { commands, Disposable, QuickPickItem } from 'vscode'; -import { CommandContext, setCommandContext } from './common'; -import { ExtensionKey } from '../constants'; -import { CommandQuickPickItem, OpenFileCommandQuickPickItem } from '../quickPicks'; -import { Logger } from '../logger'; - -const keyNoopCommand = Object.create(null) as QuickPickItem; -export { keyNoopCommand as KeyNoopCommand }; - -export declare type Keys = 'left' | 'right' | ',' | '.'; -export const keys: Keys[] = [ - 'left', - 'right', - ',', - '.' -]; - +'use strict'; +import { commands, Disposable } from 'vscode'; +import { CommandContext, setCommandContext } from './common'; +import { ExtensionKey } from '../constants'; +import { QuickPickItem } from '../quickPicks'; +import { Logger } from '../logger'; + +const keyNoopCommand = Object.create(null) as QuickPickItem; +export { keyNoopCommand as KeyNoopCommand }; + +export declare type Keys = 'left' | 'right' | ',' | '.'; +export const keys: Keys[] = [ + 'left', + 'right', + ',', + '.' +]; + export declare type KeyMapping = { [id: string]: (QuickPickItem | (() => Promise) | undefined) }; -let mappings: KeyMapping[] = []; - -let _instance: Keyboard; - -export class KeyboardScope extends Disposable { - - constructor(private mapping: KeyMapping) { - super(() => this.dispose()); - - for (const key in mapping) { - mapping[key] = mapping[key] || keyNoopCommand; - } - } - - async dispose() { - const index = mappings.indexOf(this.mapping); - Logger.log('KeyboardScope.dispose', mappings.length, index); - if (index === (mappings.length - 1)) { - mappings.pop(); - await this.updateKeyCommandsContext(mappings[mappings.length - 1]); - } - else { - mappings.splice(index, 1); - } - } - - async begin() { - mappings.push(this.mapping); - await this.updateKeyCommandsContext(this.mapping); - return this; - } - - async clearKeyCommand(key: Keys) { - const mapping = mappings[mappings.length - 1]; - if (mapping !== this.mapping || !mapping[key]) return; - - Logger.log('KeyboardScope.clearKeyCommand', mappings.length, key); - mapping[key] = undefined; - await setCommandContext(`${CommandContext.Key}:${key}`, false); - } - - async setKeyCommand(key: Keys, command: QuickPickItem | (() => Promise)) { - const mapping = mappings[mappings.length - 1]; - if (mapping !== this.mapping) return; - - Logger.log('KeyboardScope.setKeyCommand', mappings.length, key, !!mapping[key]); - - if (!mapping[key]) { - mapping[key] = command; - await setCommandContext(`${CommandContext.Key}:${key}`, true); - } - else { - mapping[key] = command; - } - } - - private async updateKeyCommandsContext(mapping: KeyMapping) { - const promises = []; - for (const key of keys) { - promises.push(setCommandContext(`${CommandContext.Key}:${key}`, !!(mapping && mapping[key]))); - } - await Promise.all(promises); - } -} - -export class Keyboard extends Disposable { - - static get instance(): Keyboard { - return _instance; - } - - private _disposable: Disposable; - - constructor() { - super(() => this.dispose()); - - const subscriptions: Disposable[] = []; - - for (const key of keys) { - subscriptions.push(commands.registerCommand(`${ExtensionKey}.key.${key}`, () => this.execute(key), this)); - } - - this._disposable = Disposable.from(...subscriptions); - - _instance = this; - } - - dispose() { - this._disposable && this._disposable.dispose(); - } - - async beginScope(mapping?: KeyMapping): Promise { - Logger.log('Keyboard.beginScope', mappings.length); - return await new KeyboardScope(mapping ? Object.assign(Object.create(null), mapping) : Object.create(null)).begin(); - } - - async execute(key: Keys): Promise<{} | undefined> { - if (!mappings.length) return undefined; - - try { - const mapping = mappings[mappings.length - 1]; - - let command = mapping[key] as CommandQuickPickItem | (() => Promise); - if (typeof command === 'function') { - command = await command(); - } - if (!command || !(command instanceof CommandQuickPickItem)) return undefined; - - Logger.log('Keyboard.execute', key); - - if (command instanceof OpenFileCommandQuickPickItem) { - // Have to open this pinned right now, because vscode doesn't have a way to open a unpinned, but unfocused editor - return await command.execute(true); - } - - return await command.execute(); - } - catch (ex) { - Logger.error(ex, 'Keyboard.execute'); - return undefined; - } - } +let mappings: KeyMapping[] = []; + +let _instance: Keyboard; + +export class KeyboardScope extends Disposable { + + constructor(private mapping: KeyMapping) { + super(() => this.dispose()); + + for (const key in mapping) { + mapping[key] = mapping[key] || keyNoopCommand; + } + } + + async dispose() { + const index = mappings.indexOf(this.mapping); + Logger.log('KeyboardScope.dispose', mappings.length, index); + if (index === (mappings.length - 1)) { + mappings.pop(); + await this.updateKeyCommandsContext(mappings[mappings.length - 1]); + } + else { + mappings.splice(index, 1); + } + } + + async begin() { + mappings.push(this.mapping); + await this.updateKeyCommandsContext(this.mapping); + return this; + } + + async clearKeyCommand(key: Keys) { + const mapping = mappings[mappings.length - 1]; + if (mapping !== this.mapping || !mapping[key]) return; + + Logger.log('KeyboardScope.clearKeyCommand', mappings.length, key); + mapping[key] = undefined; + await setCommandContext(`${CommandContext.Key}:${key}`, false); + } + + async setKeyCommand(key: Keys, command: QuickPickItem | (() => Promise)) { + const mapping = mappings[mappings.length - 1]; + if (mapping !== this.mapping) return; + + Logger.log('KeyboardScope.setKeyCommand', mappings.length, key, !!mapping[key]); + + if (!mapping[key]) { + mapping[key] = command; + await setCommandContext(`${CommandContext.Key}:${key}`, true); + } + else { + mapping[key] = command; + } + } + + private async updateKeyCommandsContext(mapping: KeyMapping) { + const promises = []; + for (const key of keys) { + promises.push(setCommandContext(`${CommandContext.Key}:${key}`, !!(mapping && mapping[key]))); + } + await Promise.all(promises); + } +} + +export class Keyboard extends Disposable { + + static get instance(): Keyboard { + return _instance; + } + + private _disposable: Disposable; + + constructor() { + super(() => this.dispose()); + + const subscriptions: Disposable[] = []; + + for (const key of keys) { + subscriptions.push(commands.registerCommand(`${ExtensionKey}.key.${key}`, () => this.execute(key), this)); + } + + this._disposable = Disposable.from(...subscriptions); + + _instance = this; + } + + dispose() { + this._disposable && this._disposable.dispose(); + } + + async beginScope(mapping?: KeyMapping): Promise { + Logger.log('Keyboard.beginScope', mappings.length); + return await new KeyboardScope(mapping ? Object.assign(Object.create(null), mapping) : Object.create(null)).begin(); + } + + async execute(key: Keys): Promise<{} | undefined> { + if (!mappings.length) return undefined; + + try { + const mapping = mappings[mappings.length - 1]; + + let command = mapping[key] as QuickPickItem | (() => Promise); + if (typeof command === 'function') { + command = await command(); + } + if (!command || typeof command.onDidPressKey !== 'function') return undefined; + + Logger.log('Keyboard.execute', key); + + return await command.onDidPressKey(key); + } + catch (ex) { + Logger.error(ex, 'Keyboard.execute'); + return undefined; + } + } } \ No newline at end of file diff --git a/src/commands/openChangedFiles.ts b/src/commands/openChangedFiles.ts index a825750..de8cd75 100644 --- a/src/commands/openChangedFiles.ts +++ b/src/commands/openChangedFiles.ts @@ -1,33 +1,35 @@ 'use strict'; -import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands, openEditor } from './common'; +import { TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands, getCommandUri, openEditor } from './common'; import { GitService } from '../gitService'; import { Logger } from '../logger'; +export interface OpenChangedFilesCommandArgs { + uris?: Uri[]; +} + export class OpenChangedFilesCommand extends ActiveEditorCommand { constructor(private git: GitService) { super(Commands.OpenChangedFiles); } - async execute(editor: TextEditor, uri?: Uri, uris?: Uri[]) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: OpenChangedFilesCommandArgs = {}) { + uri = getCommandUri(uri, editor); try { - if (!uris) { + if (args.uris === undefined) { const repoPath = await this.git.getRepoPathFromUri(uri); - if (!repoPath) return window.showWarningMessage(`Unable to open changed files`); + if (repoPath === undefined) return window.showWarningMessage(`Unable to open changed files`); const status = await this.git.getStatusForRepo(repoPath); - if (!status) return window.showWarningMessage(`Unable to open changed files`); + if (status === undefined) return window.showWarningMessage(`Unable to open changed files`); - uris = status.files.filter(_ => _.status !== 'D').map(_ => _.Uri); + args.uris = status.files.filter(_ => _.status !== 'D').map(_ => _.Uri); } - for (const uri of uris) { - await openEditor(uri, true); + for (const uri of args.uris) { + await openEditor(uri, { preserveFocus: true, preview: false } as TextDocumentShowOptions); } return undefined; diff --git a/src/commands/openCommitInRemote.ts b/src/commands/openCommitInRemote.ts index 425852e..9e683dc 100644 --- a/src/commands/openCommitInRemote.ts +++ b/src/commands/openCommitInRemote.ts @@ -1,9 +1,10 @@ 'use strict'; import { Arrays } from '../system'; import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { GitCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; +import { OpenInRemoteCommandArgs } from './openInRemote'; export class OpenCommitInRemoteCommand extends ActiveEditorCommand { @@ -12,24 +13,21 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri) { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } - - if ((editor && editor.document && editor.document.isDirty) || !uri) return undefined; + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; const gitUri = await GitUri.fromUri(uri, this.git); if (!gitUri.repoPath) return undefined; - const line = (editor && editor.selection.active.line) || gitUri.offset; + const line = editor === undefined ? gitUri.offset : editor.selection.active.line; try { const blameline = line - gitUri.offset; if (blameline < 0) return undefined; const blame = await this.git.getBlameForLine(gitUri, blameline); - if (!blame) return window.showWarningMessage(`Unable to open commit in remote provider. File is probably not under source control`); + if (blame === undefined) return window.showWarningMessage(`Unable to open commit in remote provider. File is probably not under source control`); let commit = blame.commit; // If the line is uncommitted, find the previous commit @@ -38,7 +36,13 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand { } const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider); - return commands.executeCommand(Commands.OpenInRemote, uri, remotes, 'commit', [commit.sha]); + return commands.executeCommand(Commands.OpenInRemote, uri, { + resource: { + type: 'commit', + sha: commit.sha + }, + remotes + } as OpenInRemoteCommandArgs); } catch (ex) { Logger.error(ex, 'OpenCommitInRemoteCommand'); diff --git a/src/commands/openFileInRemote.ts b/src/commands/openFileInRemote.ts index 908e600..d834c1e 100644 --- a/src/commands/openFileInRemote.ts +++ b/src/commands/openFileInRemote.ts @@ -1,9 +1,10 @@ 'use strict'; import { Arrays } from '../system'; import { commands, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; +import { OpenInRemoteCommandArgs } from './openInRemote'; export class OpenFileInRemoteCommand extends ActiveEditorCommand { @@ -12,12 +13,8 @@ export class OpenFileInRemoteCommand extends ActiveEditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri) { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } - - if (!uri) return undefined; + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; const gitUri = await GitUri.fromUri(uri, this.git); if (!gitUri.repoPath) return undefined; @@ -26,8 +23,18 @@ export class OpenFileInRemoteCommand extends ActiveEditorCommand { try { const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider); - const range = editor && new Range(editor.selection.start.with({ line: editor.selection.start.line + 1 }), editor.selection.end.with({ line: editor.selection.end.line + 1 })); - return commands.executeCommand(Commands.OpenInRemote, uri, remotes, 'file', [gitUri.getRelativePath(), branch === undefined ? 'Current' : branch.name, gitUri.sha, range]); + const range = editor === undefined ? undefined : new Range(editor.selection.start.with({ line: editor.selection.start.line + 1 }), editor.selection.end.with({ line: editor.selection.end.line + 1 })); + + return commands.executeCommand(Commands.OpenInRemote, uri, { + resource: { + type: 'file', + branch: branch === undefined ? 'Current' : branch.name, + fileName: gitUri.getRelativePath(), + range: range, + sha: gitUri.sha + }, + remotes + } as OpenInRemoteCommandArgs); } catch (ex) { Logger.error(ex, 'OpenFileInRemoteCommand'); diff --git a/src/commands/openInRemote.ts b/src/commands/openInRemote.ts index 455a962..64056a6 100644 --- a/src/commands/openInRemote.ts +++ b/src/commands/openInRemote.ts @@ -1,50 +1,73 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands } from './common'; -import { GitRemote, RemoteOpenType } from '../gitService'; +import { ActiveEditorCommand, Commands, getCommandUri } from './common'; +import { GitLogCommit, GitRemote, RemoteResource } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, OpenRemoteCommandQuickPickItem, RemotesQuickPick } from '../quickPicks'; +export interface OpenInRemoteCommandArgs { + remotes?: GitRemote[]; + resource?: RemoteResource; + + goBackCommand?: CommandQuickPickItem; +} + export class OpenInRemoteCommand extends ActiveEditorCommand { constructor() { super(Commands.OpenInRemote); } - async execute(editor: TextEditor, uri?: Uri, remotes?: GitRemote[], type?: RemoteOpenType, args: string[] = [], goBackCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: OpenInRemoteCommandArgs = {}) { + uri = getCommandUri(uri, editor); try { - if (remotes === undefined) return undefined; - if (type === undefined) throw new Error(`Invalid type ${type}`); + if (args.remotes === undefined || args.resource === undefined) return undefined; - if (remotes.length === 1) { - const command = new OpenRemoteCommandQuickPickItem(remotes[0], type, ...args); + if (args.remotes.length === 1) { + const command = new OpenRemoteCommandQuickPickItem(args.remotes[0], args.resource); return command.execute(); } let placeHolder: string = ''; - switch (type) { + switch (args.resource.type) { case 'branch': - placeHolder = `open ${args[0]} branch in\u2026`; + placeHolder = `open ${args.resource.branch} branch in\u2026`; break; + case 'commit': - const shortSha = args[0].substring(0, 8); + const shortSha = args.resource.sha.substring(0, 8); placeHolder = `open commit ${shortSha} in\u2026`; break; - case 'file': - case 'working-file': - const shortFileSha = (args[2] && args[2].substring(0, 8)) || ''; - const shaSuffix = shortFileSha ? ` \u00a0\u2022\u00a0 ${shortFileSha}` : ''; - placeHolder = `open ${args[0]}${shaSuffix} in\u2026`; + case 'file': + if (args.resource.commit !== undefined && args.resource.commit instanceof GitLogCommit) { + if (args.resource.commit.status === 'D') { + args.resource.sha = args.resource.commit.previousSha; + placeHolder = `open ${args.resource.fileName} \u00a0\u2022\u00a0 ${args.resource.commit.previousShortSha} in\u2026`; + } + else { + args.resource.sha = args.resource.commit.sha; + placeHolder = `open ${args.resource.fileName} \u00a0\u2022\u00a0 ${args.resource.commit.shortSha} in\u2026`; + } + } + else { + const shortFileSha = args.resource.sha === undefined ? '' : args.resource.sha.substring(0, 8); + const shaSuffix = shortFileSha ? ` \u00a0\u2022\u00a0 ${shortFileSha}` : ''; + + placeHolder = `open ${args.resource.fileName}${shaSuffix} in\u2026`; + } + break; + + case 'working-file': + placeHolder = `open ${args.resource.fileName} in\u2026`; break; } - const pick = await RemotesQuickPick.show(remotes, placeHolder, type, args, goBackCommand); - return pick && pick.execute(); + const pick = await RemotesQuickPick.show(args.remotes, placeHolder, args.resource, args.goBackCommand); + if (pick === undefined) return undefined; + + return pick.execute(); } catch (ex) { diff --git a/src/commands/showBlame.ts b/src/commands/showBlame.ts index b1c6875..efb78d7 100644 --- a/src/commands/showBlame.ts +++ b/src/commands/showBlame.ts @@ -4,19 +4,21 @@ import { BlameAnnotationController } from '../blameAnnotationController'; import { Commands, EditorCommand } from './common'; import { Logger } from '../logger'; +export interface ShowBlameCommandArgs { + sha?: string; +} + export class ShowBlameCommand extends EditorCommand { constructor(private annotationController: BlameAnnotationController) { super(Commands.ShowBlame); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string): Promise { - try { - if (sha) { - return this.annotationController.showBlameAnnotation(editor, sha); - } + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowBlameCommandArgs = {}): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - return this.annotationController.showBlameAnnotation(editor, editor.selection.active.line); + try { + return this.annotationController.showBlameAnnotation(editor, args.sha !== undefined ? args.sha : editor.selection.active.line); } catch (ex) { Logger.error(ex, 'ShowBlameCommand'); diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts index e9cb0cd..cd7b8a6 100644 --- a/src/commands/showBlameHistory.ts +++ b/src/commands/showBlameHistory.ts @@ -1,35 +1,40 @@ 'use strict'; import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; -import { Commands, EditorCommand } from './common'; +import { Commands, EditorCommand, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; +export interface ShowBlameHistoryCommandArgs { + line?: number; + position?: Position; + range?: Range; + sha?: string; +} + export class ShowBlameHistoryCommand extends EditorCommand { constructor(private git: GitService) { super(Commands.ShowBlameHistory); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position, sha?: string, line?: number) { - if (!(uri instanceof Uri)) { - if (!editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowBlameHistoryCommandArgs = {}) { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - if (range == null || position == null) { + if (args.range == null || args.position == null) { // If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file) - range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000)); - position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; + args.range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000)); + args.position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; } const gitUri = await GitUri.fromUri(uri, this.git); try { - const locations = await this.git.getBlameLocations(gitUri, range, sha, line); - if (!locations) return window.showWarningMessage(`Unable to show blame history. File is probably not under source control`); + const locations = await this.git.getBlameLocations(gitUri, args.range, args.sha, args.line); + if (locations === undefined) return window.showWarningMessage(`Unable to show blame history. File is probably not under source control`); - return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations); + return commands.executeCommand(BuiltInCommands.ShowReferences, uri, args.position, locations); } catch (ex) { Logger.error(ex, 'ShowBlameHistoryCommand', 'getBlameLocations'); diff --git a/src/commands/showCommitSearch.ts b/src/commands/showCommitSearch.ts index c30624b..98b8136 100644 --- a/src/commands/showCommitSearch.ts +++ b/src/commands/showCommitSearch.ts @@ -1,9 +1,10 @@ 'use strict'; import { commands, InputBoxOptions, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitRepoSearchBy, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, CommitsQuickPick } from '../quickPicks'; +import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; const searchByRegex = /^([@:#])/; const searchByMap = new Map([ @@ -12,68 +13,73 @@ const searchByMap = new Map([ ['#', GitRepoSearchBy.Sha] ]); +export interface ShowCommitSearchCommandArgs { + search?: string; + searchBy?: GitRepoSearchBy; + + goBackCommand?: CommandQuickPickItem; +} + export class ShowCommitSearchCommand extends ActiveEditorCachedCommand { constructor(private git: GitService) { super(Commands.ShowCommitSearch); } - async execute(editor: TextEditor, uri?: Uri, search?: string, searchBy?: GitRepoSearchBy, goBackCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowCommitSearchCommandArgs = {}) { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; const gitUri = await GitUri.fromUri(uri, this.git); if (gitUri.repoPath === undefined) return undefined; - if (!search || searchBy == null) { - search = await window.showInputBox({ - value: search, + if (!args.search || args.searchBy == null) { + args.search = await window.showInputBox({ + value: args.search, prompt: `Please enter a search string`, placeHolder: `search by message, author (use @), files (use :), or commit id (use #)` } as InputBoxOptions); - if (search === undefined) return goBackCommand && goBackCommand.execute(); + if (args.search === undefined) return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - const match = searchByRegex.exec(search); + const match = searchByRegex.exec(args.search); if (match && match[1]) { - searchBy = searchByMap.get(match[1]); - search = search.substring((search[1] === ' ') ? 2 : 1); + args.searchBy = searchByMap.get(match[1]); + args.search = args.search.substring((args.search[1] === ' ') ? 2 : 1); } - else if (GitService.isSha(search)) { - searchBy = GitRepoSearchBy.Sha; + else if (GitService.isSha(args.search)) { + args.searchBy = GitRepoSearchBy.Sha; } else { - searchBy = GitRepoSearchBy.Message; + args.searchBy = GitRepoSearchBy.Message; } } try { - if (searchBy === undefined) { - searchBy = GitRepoSearchBy.Message; + if (args.searchBy === undefined) { + args.searchBy = GitRepoSearchBy.Message; } - const log = await this.git.getLogForRepoSearch(gitUri.repoPath, search, searchBy); + const log = await this.git.getLogForRepoSearch(gitUri.repoPath, args.search, args.searchBy); if (log === undefined) return undefined; let originalSearch: string | undefined = undefined; let placeHolder: string | undefined = undefined; - switch (searchBy) { + switch (args.searchBy) { case GitRepoSearchBy.Author: - originalSearch = `@${search}`; - placeHolder = `commits with author matching '${search}'`; + originalSearch = `@${args.search}`; + placeHolder = `commits with author matching '${args.search}'`; break; case GitRepoSearchBy.Files: - originalSearch = `:${search}`; - placeHolder = `commits with files matching '${search}'`; + originalSearch = `:${args.search}`; + placeHolder = `commits with files matching '${args.search}'`; break; case GitRepoSearchBy.Message: - originalSearch = search; - placeHolder = `commits with message matching '${search}'`; + originalSearch = args.search; + placeHolder = `commits with message matching '${args.search}'`; break; case GitRepoSearchBy.Sha: - originalSearch = `#${search}`; - placeHolder = `commits with id matching '${search}'`; + originalSearch = `#${args.search}`; + placeHolder = `commits with id matching '${args.search}'`; break; } @@ -81,20 +87,32 @@ export class ShowCommitSearchCommand extends ActiveEditorCachedCommand { const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to commit search` - }, Commands.ShowCommitSearch, [gitUri, originalSearch, undefined, goBackCommand]); + }, Commands.ShowCommitSearch, [ + gitUri, + { + search: originalSearch, + goBackCommand: args.goBackCommand + } as ShowCommitSearchCommandArgs + ]); const pick = await CommitsQuickPick.show(this.git, log, placeHolder!, currentCommand); - if (!pick) return undefined; + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + 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 search for ${placeHolder}` - }, Commands.ShowCommitSearch, [gitUri, search, searchBy, goBackCommand])); + return commands.executeCommand(Commands.ShowQuickCommitDetails, + new GitUri(pick.commit.uri, pick.commit), + { + sha: pick.commit.sha, + commit: pick.commit, + goBackCommand: new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to search for ${placeHolder}` + }, Commands.ShowCommitSearch, [ + gitUri, + args + ]) + } as ShowQuickCommitDetailsCommandArgs); } catch (ex) { Logger.error(ex, 'ShowCommitSearchCommand'); diff --git a/src/commands/showFileHistory.ts b/src/commands/showFileHistory.ts index 60412a2..f4c2003 100644 --- a/src/commands/showFileHistory.ts +++ b/src/commands/showFileHistory.ts @@ -1,34 +1,38 @@ 'use strict'; import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; -import { Commands, EditorCommand } from './common'; +import { Commands, EditorCommand, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; +export interface ShowFileHistoryCommandArgs { + line?: number; + position?: Position; + sha?: string; +} + export class ShowFileHistoryCommand extends EditorCommand { constructor(private git: GitService) { super(Commands.ShowFileHistory); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, position?: Position, sha?: string, line?: number) { - if (!(uri instanceof Uri)) { - if (!editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileHistoryCommandArgs = {}) { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - if (position == null) { + if (args.position == null) { // If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file) - position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; + args.position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; } const gitUri = await GitUri.fromUri(uri, this.git); try { - const locations = await this.git.getLogLocations(gitUri, sha, line); - if (!locations) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); + const locations = await this.git.getLogLocations(gitUri, args.sha, args.line); + if (locations === undefined) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); - return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations); + return commands.executeCommand(BuiltInCommands.ShowReferences, uri, args.position, locations); } catch (ex) { Logger.error(ex, 'ShowFileHistoryCommand', 'getLogLocations'); diff --git a/src/commands/showQuickBranchHistory.ts b/src/commands/showQuickBranchHistory.ts index 46832d6..717f9b2 100644 --- a/src/commands/showQuickBranchHistory.ts +++ b/src/commands/showQuickBranchHistory.ts @@ -1,9 +1,19 @@ 'use strict'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitService, GitUri, IGitLog } from '../gitService'; import { Logger } from '../logger'; import { BranchesQuickPick, BranchHistoryQuickPick, CommandQuickPickItem } from '../quickPicks'; +import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; + +export interface ShowQuickBranchHistoryCommandArgs { + branch?: string; + log?: IGitLog; + maxCount?: number; + + goBackCommand?: CommandQuickPickItem; + nextPageCommand?: CommandQuickPickItem; +} export class ShowQuickBranchHistoryCommand extends ActiveEditorCachedCommand { @@ -11,58 +21,63 @@ export class ShowQuickBranchHistoryCommand extends ActiveEditorCachedCommand { super(Commands.ShowQuickBranchHistory); } - async execute(editor: TextEditor, uri?: Uri, branch?: string, maxCount?: number, goBackCommand?: CommandQuickPickItem, log?: IGitLog, nextPageCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickBranchHistoryCommandArgs = {}) { + uri = getCommandUri(uri, editor); const gitUri = uri && await GitUri.fromUri(uri, this.git); - if (maxCount == null) { - maxCount = this.git.config.advanced.maxQuickHistory; + if (args.maxCount == null) { + args.maxCount = this.git.config.advanced.maxQuickHistory; } - let progressCancellation = branch === undefined ? undefined : BranchHistoryQuickPick.showProgress(branch); + let progressCancellation = args.branch === undefined ? undefined : BranchHistoryQuickPick.showProgress(args.branch); try { const repoPath = (gitUri && gitUri.repoPath) || this.git.repoPath; if (repoPath === undefined) return window.showWarningMessage(`Unable to show branch history`); - if (branch === undefined) { + if (args.branch === undefined) { const branches = await this.git.getBranches(repoPath); const pick = await BranchesQuickPick.show(branches, `Show history for branch\u2026`); - if (!pick) return undefined; + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); - branch = pick.branch.name; - if (branch === undefined) return undefined; + args.branch = pick.branch.name; + if (args.branch === undefined) return undefined; - progressCancellation = BranchHistoryQuickPick.showProgress(branch); + progressCancellation = BranchHistoryQuickPick.showProgress(args.branch); } - if (!log) { - log = await this.git.getLogForRepo(repoPath, (gitUri && gitUri.sha) || branch, maxCount); - if (!log) return window.showWarningMessage(`Unable to show branch history`); + if (args.log === undefined) { + args.log = await this.git.getLogForRepo(repoPath, (gitUri && gitUri.sha) || args.branch, args.maxCount); + if (args.log === undefined) return window.showWarningMessage(`Unable to show branch history`); } if (progressCancellation !== undefined && progressCancellation.token.isCancellationRequested) return undefined; - const pick = await BranchHistoryQuickPick.show(this.git, log, gitUri, branch, progressCancellation!, goBackCommand, nextPageCommand); - if (!pick) return undefined; + const pick = await BranchHistoryQuickPick.show(this.git, args.log, gitUri, args.branch, progressCancellation!, args.goBackCommand, args.nextPageCommand); + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + 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 \u00a0$(git-branch) ${branch} history` - }, Commands.ShowQuickBranchHistory, [uri, branch, maxCount, goBackCommand, log]), - log); + // Create a command to get back to here + const currentCommand = new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to \u00a0$(git-branch) ${args.branch} history` + }, Commands.ShowQuickBranchHistory, [ + uri, + args + ]); + + return commands.executeCommand(Commands.ShowQuickCommitDetails, + new GitUri(pick.commit.uri, pick.commit), + { + sha: pick.commit.sha, + commit: pick.commit, + repoLog: args.log, + goBackCommand: currentCommand + } as ShowQuickCommitDetailsCommandArgs); } catch (ex) { Logger.error(ex, 'ShowQuickBranchHistoryCommand'); diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index 153e2eb..681bb4d 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -1,43 +1,50 @@ 'use strict'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitCommit, GitLogCommit, GitService, GitUri, IGitLog } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, CommitDetailsQuickPick, CommitWithFileStatusQuickPickItem } from '../quickPicks'; +import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails'; import * as path from 'path'; +export interface ShowQuickCommitDetailsCommandArgs { + sha?: string; + commit?: GitCommit | GitLogCommit; + repoLog?: IGitLog; + + goBackCommand?: CommandQuickPickItem; +} + export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { constructor(private git: GitService) { super(Commands.ShowQuickCommitDetails); } - async execute(editor: TextEditor, uri?: Uri, sha?: string, commit?: GitCommit | GitLogCommit, goBackCommand?: CommandQuickPickItem, repoLog?: IGitLog) { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickCommitDetailsCommandArgs = {}) { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; const gitUri = await GitUri.fromUri(uri, this.git); let repoPath = gitUri.repoPath; let workingFileName = path.relative(repoPath || '', gitUri.fsPath); - if (!sha) { - if (!editor) return undefined; + if (args.sha === undefined) { + if (editor === undefined) return undefined; const blameline = editor.selection.active.line - gitUri.offset; if (blameline < 0) return undefined; try { const blame = await this.git.getBlameForLine(gitUri, blameline); - if (!blame) return window.showWarningMessage(`Unable to show commit details. File is probably not under source control`); + if (blame === undefined) return window.showWarningMessage(`Unable to show commit details. File is probably not under source control`); - sha = blame.commit.isUncommitted ? blame.commit.previousSha : blame.commit.sha; + args.sha = blame.commit.isUncommitted ? blame.commit.previousSha : blame.commit.sha; repoPath = blame.commit.repoPath; workingFileName = blame.commit.fileName; - commit = blame.commit; + args.commit = blame.commit; } catch (ex) { Logger.error(ex, 'ShowQuickCommitDetailsCommand', `getBlameForLine(${blameline})`); @@ -46,51 +53,60 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { } try { - if (!commit || (commit.type !== 'branch' && commit.type !== 'stash')) { - if (repoLog) { - commit = repoLog.commits.get(sha!); + if (args.commit === undefined || (args.commit.type !== 'branch' && args.commit.type !== 'stash')) { + if (args.repoLog !== undefined) { + args.commit = args.repoLog.commits.get(args.sha!); // If we can't find the commit, kill the repoLog - if (commit === undefined) { - repoLog = undefined; + if (args.commit === undefined) { + args.repoLog = undefined; } } - if (repoLog === undefined) { - const log = await this.git.getLogForRepo(repoPath!, sha, 2); + if (args.repoLog === undefined) { + const log = await this.git.getLogForRepo(repoPath!, args.sha, 2); if (log === undefined) return window.showWarningMessage(`Unable to show commit details`); - commit = log.commits.get(sha!); + args.commit = log.commits.get(args.sha!); } } - if (commit === undefined) return window.showWarningMessage(`Unable to show commit details`); + if (args.commit === undefined) return window.showWarningMessage(`Unable to show commit details`); - if (!commit.workingFileName) { - commit.workingFileName = workingFileName; + if (args.commit.workingFileName === undefined) { + args.commit.workingFileName = workingFileName; } - if (!goBackCommand) { + if (args.goBackCommand === undefined) { // Create a command to get back to the branch history - goBackCommand = new CommandQuickPickItem({ + args.goBackCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to branch history` - }, Commands.ShowQuickCurrentBranchHistory, [new GitUri(commit.uri, commit)]); + }, Commands.ShowQuickCurrentBranchHistory, [ + new GitUri(args.commit.uri, args.commit) + ]); } // Create a command to get back to where we are right now const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, - description: `\u00a0 \u2014 \u00a0\u00a0 to details of \u00a0$(git-commit) ${commit.shortSha}` - }, Commands.ShowQuickCommitDetails, [new GitUri(commit.uri, commit), sha, commit, goBackCommand, repoLog]); + description: `\u00a0 \u2014 \u00a0\u00a0 to details of \u00a0$(git-commit) ${args.commit.shortSha}` + }, Commands.ShowQuickCommitDetails, [ + new GitUri(args.commit.uri, args.commit), + args + ]); - const pick = await CommitDetailsQuickPick.show(this.git, commit as GitLogCommit, uri, goBackCommand, currentCommand, repoLog); - if (!pick) return undefined; + const pick = await CommitDetailsQuickPick.show(this.git, args.commit as GitLogCommit, uri, args.goBackCommand, currentCommand, args.repoLog); + if (pick === undefined) return undefined; - if (!(pick instanceof CommitWithFileStatusQuickPickItem)) { - return pick.execute(); - } + if (!(pick instanceof CommitWithFileStatusQuickPickItem)) return pick.execute(); - return commands.executeCommand(Commands.ShowQuickCommitFileDetails, pick.gitUri, pick.sha, commit, currentCommand); + return commands.executeCommand(Commands.ShowQuickCommitFileDetails, + pick.gitUri, + { + commit: args.commit, + sha: pick.sha, + goBackCommand: currentCommand + } as ShowQuickCommitFileDetailsCommandArgs); } catch (ex) { Logger.error(ex, 'ShowQuickCommitDetailsCommand'); diff --git a/src/commands/showQuickCommitFileDetails.ts b/src/commands/showQuickCommitFileDetails.ts index 964e689..9aaf369 100644 --- a/src/commands/showQuickCommitFileDetails.ts +++ b/src/commands/showQuickCommitFileDetails.ts @@ -1,41 +1,48 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitCommit, GitLogCommit, GitService, GitUri, IGitLog } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, CommitFileDetailsQuickPick } from '../quickPicks'; +import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; import * as path from 'path'; +export interface ShowQuickCommitFileDetailsCommandArgs { + sha?: string; + commit?: GitCommit | GitLogCommit; + fileLog?: IGitLog; + + goBackCommand?: CommandQuickPickItem; +} + export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand { constructor(private git: GitService) { super(Commands.ShowQuickCommitFileDetails); } - async execute(editor: TextEditor, uri?: Uri, sha?: string, commit?: GitCommit | GitLogCommit, goBackCommand?: CommandQuickPickItem, fileLog?: IGitLog) { - if (!(uri instanceof Uri)) { - if (!editor || !editor.document) return undefined; - uri = editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickCommitFileDetailsCommandArgs = {}) { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; - let workingFileName = commit && commit.workingFileName; + let workingFileName = args.commit && args.commit.workingFileName; const gitUri = await GitUri.fromUri(uri, this.git); - if (!sha) { - if (!editor) return undefined; + if (args.sha === undefined) { + if (editor === undefined) return undefined; const blameline = editor.selection.active.line - gitUri.offset; if (blameline < 0) return undefined; try { const blame = await this.git.getBlameForLine(gitUri, blameline); - if (!blame) return window.showWarningMessage(`Unable to show commit file details. File is probably not under source control`); + if (blame === undefined) return window.showWarningMessage(`Unable to show commit file details. File is probably not under source control`); - sha = blame.commit.isUncommitted ? blame.commit.previousSha : blame.commit.sha; + args.sha = blame.commit.isUncommitted ? blame.commit.previousSha : blame.commit.sha; - commit = blame.commit; - workingFileName = path.relative(commit.repoPath, gitUri.fsPath); + args.commit = blame.commit; + workingFileName = path.relative(args.commit.repoPath, gitUri.fsPath); } catch (ex) { Logger.error(ex, 'ShowQuickCommitFileDetailsCommand', `getBlameForLine(${blameline})`); @@ -44,50 +51,56 @@ export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand } try { - if (!commit || (commit.type !== 'file' && commit.type !== 'stash')) { - if (fileLog) { - commit = fileLog.commits.get(sha!); + if (args.commit === undefined || (args.commit.type !== 'file' && args.commit.type !== 'stash')) { + if (args.fileLog !== undefined) { + args.commit = args.fileLog.commits.get(args.sha!); // If we can't find the commit, kill the fileLog - if (commit === undefined) { - fileLog = undefined; + if (args.commit === undefined) { + args.fileLog = undefined; } } - if (fileLog === undefined) { - commit = await this.git.getLogCommit(commit ? commit.repoPath : gitUri.repoPath, gitUri.fsPath, sha, { previous: true }); - if (commit === undefined) return window.showWarningMessage(`Unable to show commit file details`); + if (args.fileLog === undefined) { + args.commit = await this.git.getLogCommit(args.commit ? args.commit.repoPath : gitUri.repoPath, gitUri.fsPath, args.sha, { previous: true }); + if (args.commit === undefined) return window.showWarningMessage(`Unable to show commit file details`); } } - if (commit === undefined) return window.showWarningMessage(`Unable to show commit file details`); + if (args.commit === undefined) return window.showWarningMessage(`Unable to show commit file details`); // Attempt to the most recent commit -- so that we can find the real working filename if there was a rename - commit.workingFileName = workingFileName; - commit.workingFileName = await this.git.findWorkingFileName(commit); + args.commit.workingFileName = workingFileName; + args.commit.workingFileName = await this.git.findWorkingFileName(args.commit); - const shortSha = sha!.substring(0, 8); + const shortSha = args.sha!.substring(0, 8); - if (!goBackCommand) { + if (args.goBackCommand === undefined) { // Create a command to get back to the commit details - goBackCommand = new CommandQuickPickItem({ + args.goBackCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to details of \u00a0$(git-commit) ${shortSha}` - }, Commands.ShowQuickCommitDetails, [new GitUri(commit.uri, commit), sha, commit]); + }, Commands.ShowQuickCommitDetails, [ + new GitUri(args.commit.uri, args.commit), + { + commit: args.commit, + sha: args.sha + } as ShowQuickCommitDetailsCommandArgs + ]); } - const pick = await CommitFileDetailsQuickPick.show(this.git, commit as GitLogCommit, uri, goBackCommand, - // Create a command to get back to where we are right now - new CommandQuickPickItem({ - label: `go back \u21A9`, - description: `\u00a0 \u2014 \u00a0\u00a0 to details of \u00a0$(file-text) ${path.basename(commit.fileName)} in \u00a0$(git-commit) ${shortSha}` - }, Commands.ShowQuickCommitFileDetails, [new GitUri(commit.uri, commit), sha, commit, goBackCommand]), - fileLog); + // Create a command to get back to where we are right now + const currentCommand = new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to details of \u00a0$(file-text) ${path.basename(args.commit.fileName)} in \u00a0$(git-commit) ${shortSha}` + }, Commands.ShowQuickCommitFileDetails, [ + new GitUri(args.commit.uri, args.commit), + args + ]); - if (!pick) return undefined; + const pick = await CommitFileDetailsQuickPick.show(this.git, args.commit as GitLogCommit, uri, args.goBackCommand, currentCommand, args.fileLog); + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); return undefined; } diff --git a/src/commands/showQuickCurrentBranchHistory.ts b/src/commands/showQuickCurrentBranchHistory.ts index c337ff9..24eb71f 100644 --- a/src/commands/showQuickCurrentBranchHistory.ts +++ b/src/commands/showQuickCurrentBranchHistory.ts @@ -1,29 +1,37 @@ 'use strict'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; +import { ShowQuickBranchHistoryCommandArgs } from './showQuickBranchHistory'; import { GitService } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem } from '../quickPicks'; +export interface ShowQuickCurrentBranchHistoryCommandArgs { + goBackCommand?: CommandQuickPickItem; +} + export class ShowQuickCurrentBranchHistoryCommand extends ActiveEditorCachedCommand { constructor(private git: GitService) { super(Commands.ShowQuickCurrentBranchHistory); } - async execute(editor: TextEditor, uri?: Uri, goBackCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickCurrentBranchHistoryCommandArgs = {}) { + uri = getCommandUri(uri, editor); try { const repoPath = await this.git.getRepoPathFromUri(uri); if (!repoPath) return window.showWarningMessage(`Unable to show branch history`); const branch = await this.git.getBranch(repoPath); - if (!branch) return undefined; + if (branch === undefined) return undefined; - return commands.executeCommand(Commands.ShowQuickBranchHistory, uri, branch.name, undefined, goBackCommand); + return commands.executeCommand(Commands.ShowQuickBranchHistory, + uri, + { + branch: branch.name, + goBackCommand: args.goBackCommand + } as ShowQuickBranchHistoryCommandArgs); } catch (ex) { Logger.error(ex, 'ShowQuickCurrentBranchHistoryCommand'); diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 85dc66f..0cf16cf 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -1,52 +1,68 @@ 'use strict'; import { commands, Range, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitService, GitUri, IGitLog } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; +import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails'; import * as path from 'path'; +export interface ShowQuickFileHistoryCommandArgs { + log?: IGitLog; + maxCount?: number; + range?: Range; + + goBackCommand?: CommandQuickPickItem; + nextPageCommand?: CommandQuickPickItem; +} + export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand { constructor(private git: GitService) { super(Commands.ShowQuickFileHistory); } - async execute(editor: TextEditor, uri?: Uri, range?: Range, maxCount?: number, goBackCommand?: CommandQuickPickItem, log?: IGitLog, nextPageCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } - - if (!uri) return commands.executeCommand(Commands.ShowQuickCurrentBranchHistory); + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickFileHistoryCommandArgs = {}) { + uri = getCommandUri(uri, editor); + if (uri === undefined) return commands.executeCommand(Commands.ShowQuickCurrentBranchHistory); const gitUri = await GitUri.fromUri(uri, this.git); - if (maxCount == null) { - maxCount = this.git.config.advanced.maxQuickHistory; + if (args.maxCount == null) { + args.maxCount = this.git.config.advanced.maxQuickHistory; } const progressCancellation = FileHistoryQuickPick.showProgress(gitUri); try { - if (!log) { - log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, maxCount, range); - if (!log) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); + if (args.log === undefined) { + args.log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, args.maxCount, args.range); + if (args.log === undefined) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); } if (progressCancellation.token.isCancellationRequested) return undefined; - const pick = await FileHistoryQuickPick.show(this.git, log, gitUri, progressCancellation, goBackCommand, nextPageCommand); - if (!pick) return undefined; + const pick = await FileHistoryQuickPick.show(this.git, args.log, gitUri, progressCancellation, args.goBackCommand, args.nextPageCommand); + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); - return commands.executeCommand(Commands.ShowQuickCommitFileDetails, new GitUri(pick.commit.uri, pick.commit), pick.commit.sha, pick.commit, - new CommandQuickPickItem({ - label: `go back \u21A9`, - description: `\u00a0 \u2014 \u00a0\u00a0 to history of \u00a0$(file-text) ${path.basename(pick.commit.fileName)}${gitUri.sha ? ` from \u00a0$(git-commit) ${gitUri.shortSha}` : ''}` - }, Commands.ShowQuickFileHistory, [uri, range, maxCount, goBackCommand, log]), - log); + // Create a command to get back to where we are right now + const currentCommand = new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to history of \u00a0$(file-text) ${path.basename(pick.commit.fileName)}${gitUri.sha ? ` from \u00a0$(git-commit) ${gitUri.shortSha}` : ''}` + }, Commands.ShowQuickFileHistory, [ + uri, + args + ]); + + return commands.executeCommand(Commands.ShowQuickCommitFileDetails, + new GitUri(pick.commit.uri, pick.commit), + { + commit: pick.commit, + fileLog: args.log, + sha: pick.commit.sha, + goBackCommand: currentCommand + } as ShowQuickCommitFileDetailsCommandArgs); } catch (ex) { Logger.error(ex, 'ShowQuickFileHistoryCommand'); diff --git a/src/commands/showQuickRepoStatus.ts b/src/commands/showQuickRepoStatus.ts index 0cbe692..57a2659 100644 --- a/src/commands/showQuickRepoStatus.ts +++ b/src/commands/showQuickRepoStatus.ts @@ -1,34 +1,34 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitService } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, RepoStatusQuickPick } from '../quickPicks'; +export interface ShowQuickRepoStatusCommandArgs { + goBackCommand?: CommandQuickPickItem; +} + export class ShowQuickRepoStatusCommand extends ActiveEditorCachedCommand { constructor(private git: GitService) { super(Commands.ShowQuickRepoStatus); } - async execute(editor: TextEditor, uri?: Uri, goBackCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickRepoStatusCommandArgs = {}) { + uri = getCommandUri(uri, editor); try { const repoPath = await this.git.getRepoPathFromUri(uri); if (!repoPath) return window.showWarningMessage(`Unable to show repository status`); const status = await this.git.getStatusForRepo(repoPath); - if (!status) return window.showWarningMessage(`Unable to show repository status`); + if (status === undefined) return window.showWarningMessage(`Unable to show repository status`); - const pick = await RepoStatusQuickPick.show(status, goBackCommand); - if (!pick) return undefined; + const pick = await RepoStatusQuickPick.show(status, args.goBackCommand); + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); return undefined; } diff --git a/src/commands/showQuickStashList.ts b/src/commands/showQuickStashList.ts index ddeeaa6..ef43190 100644 --- a/src/commands/showQuickStashList.ts +++ b/src/commands/showQuickStashList.ts @@ -1,9 +1,14 @@ 'use strict'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands } from './common'; +import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, StashListQuickPick } from '../quickPicks'; +import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; + +export interface ShowQuickStashListCommandArgs { + goBackCommand?: CommandQuickPickItem; +} export class ShowQuickStashListCommand extends ActiveEditorCachedCommand { @@ -11,14 +16,12 @@ export class ShowQuickStashListCommand extends ActiveEditorCachedCommand { super(Commands.ShowQuickStashList); } - async execute(editor: TextEditor, uri?: Uri, goBackCommand?: CommandQuickPickItem) { - if (!(uri instanceof Uri)) { - uri = editor && editor.document && editor.document.uri; - } + async execute(editor: TextEditor, uri?: Uri, args: ShowQuickStashListCommandArgs = {}) { + uri = getCommandUri(uri, editor); try { const repoPath = await this.git.getRepoPathFromUri(uri); - if (repoPath === undefined) return window.showWarningMessage(`Unable to show stashed changes`); + if (!repoPath) return window.showWarningMessage(`Unable to show stashed changes`); const stash = await this.git.getStashList(repoPath); if (stash === undefined) return window.showWarningMessage(`Unable to show stashed changes`); @@ -27,16 +30,25 @@ export class ShowQuickStashListCommand extends ActiveEditorCachedCommand { const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to stashed changes` - }, Commands.ShowQuickStashList, [uri, goBackCommand]); + }, Commands.ShowQuickStashList, [ + uri, + { + goBackCommand: args.goBackCommand + } as ShowQuickStashListCommandArgs + ]); - const pick = await StashListQuickPick.show(this.git, stash, 'list', goBackCommand, currentCommand); - if (!pick) return undefined; + const pick = await StashListQuickPick.show(this.git, stash, 'list', args.goBackCommand, currentCommand); + if (pick === undefined) return undefined; - if (pick instanceof CommandQuickPickItem) { - return pick.execute(); - } + if (pick instanceof CommandQuickPickItem) return pick.execute(); - return commands.executeCommand(Commands.ShowQuickCommitDetails, new GitUri(pick.commit.uri, pick.commit), pick.commit.sha, pick.commit, currentCommand); + return commands.executeCommand(Commands.ShowQuickCommitDetails, + new GitUri(pick.commit.uri, pick.commit), + { + commit: pick.commit, + sha: pick.commit.sha, + goBackCommand: currentCommand + } as ShowQuickCommitDetailsCommandArgs); } catch (ex) { Logger.error(ex, 'ShowQuickStashListCommand'); diff --git a/src/commands/stashApply.ts b/src/commands/stashApply.ts index 64e77ef..bb0e5dc 100644 --- a/src/commands/stashApply.ts +++ b/src/commands/stashApply.ts @@ -6,42 +6,50 @@ import { CommitQuickPickItem, StashListQuickPick } from '../quickPicks'; import { Logger } from '../logger'; import { CommandQuickPickItem } from '../quickPicks'; +export interface StashApplyCommandArgs { + confirm?: boolean; + deleteAfter?: boolean; + stashItem?: { stashName: string, message: string }; + + goBackCommand?: CommandQuickPickItem; +} + 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, goBackCommand?: CommandQuickPickItem) { + async execute(args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) { if (!this.git.config.insiders) return undefined; if (!this.git.repoPath) return undefined; - if (!stashItem || !stashItem.stashName) { + if (args.stashItem === undefined || args.stashItem.stashName === undefined) { const stash = await this.git.getStashList(this.git.repoPath); - if (!stash) return window.showInformationMessage(`There are no stashed changes`); + if (stash === undefined) return window.showInformationMessage(`There are no stashed changes`); const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to apply stashed changes` - }, Commands.StashApply, [stashItem, confirm, deleteAfter, goBackCommand]); + }, Commands.StashApply, [args]); - const pick = await StashListQuickPick.show(this.git, stash, 'apply', goBackCommand, currentCommand); - if (!pick || !(pick instanceof CommitQuickPickItem)) return goBackCommand && goBackCommand.execute(); + const pick = await StashListQuickPick.show(this.git, stash, 'apply', args.goBackCommand, currentCommand); + if (pick === undefined || !(pick instanceof CommitQuickPickItem)) return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - goBackCommand = currentCommand; - stashItem = pick.commit as GitStashCommit; + args.goBackCommand = currentCommand; + args.stashItem = pick.commit as GitStashCommit; } try { - if (confirm) { - const message = stashItem.message.length > 80 ? `${stashItem.message.substring(0, 80)}\u2026` : stashItem.message; + if (args.confirm) { + const message = args.stashItem.message.length > 80 ? `${args.stashItem.message.substring(0, 80)}\u2026` : args.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 goBackCommand && goBackCommand.execute(); + if (result === undefined || result.title === 'No') return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); - deleteAfter = result.title !== 'Yes'; + args.deleteAfter = result.title !== 'Yes'; } - return await this.git.stashApply(this.git.repoPath, stashItem.stashName, deleteAfter); + return await this.git.stashApply(this.git.repoPath, args.stashItem.stashName, args.deleteAfter); } catch (ex) { Logger.error(ex, 'StashApplyCommand'); diff --git a/src/commands/stashDelete.ts b/src/commands/stashDelete.ts index 3245eb2..f697647 100644 --- a/src/commands/stashDelete.ts +++ b/src/commands/stashDelete.ts @@ -5,25 +5,37 @@ import { Command, Commands } from './common'; import { Logger } from '../logger'; import { CommandQuickPickItem } from '../quickPicks'; +export interface StashDeleteCommandArgs { + confirm?: boolean; + stashItem?: { stashName: string, message: string }; + + goBackCommand?: CommandQuickPickItem; +} + export class StashDeleteCommand extends Command { constructor(private git: GitService) { super(Commands.StashDelete); } - async execute(stashItem: { stashName: string, message: string }, confirm: boolean = true, goBackCommand?: CommandQuickPickItem) { + async execute(args: StashDeleteCommandArgs = { confirm: true }) { if (!this.git.config.insiders) return undefined; if (!this.git.repoPath) return undefined; - if (!stashItem || !stashItem.stashName) return undefined; + + if (args.stashItem === undefined || args.stashItem.stashName === undefined) return undefined; + + if (args.confirm === undefined) { + args.confirm = true; + } try { - if (confirm) { - const message = stashItem.message.length > 80 ? `${stashItem.message.substring(0, 80)}\u2026` : stashItem.message; + if (args.confirm) { + const message = args.stashItem.message.length > 80 ? `${args.stashItem.message.substring(0, 80)}\u2026` : args.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 goBackCommand && goBackCommand.execute(); + if (result === undefined || result.title !== 'Yes') return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); } - return await this.git.stashDelete(this.git.repoPath, stashItem.stashName); + return await this.git.stashDelete(this.git.repoPath, args.stashItem.stashName); } catch (ex) { Logger.error(ex, 'StashDeleteCommand'); diff --git a/src/commands/stashSave.ts b/src/commands/stashSave.ts index 1a2c725..4aa003b 100644 --- a/src/commands/stashSave.ts +++ b/src/commands/stashSave.ts @@ -5,26 +5,37 @@ import { Command, Commands } from './common'; import { Logger } from '../logger'; import { CommandQuickPickItem } from '../quickPicks'; +export interface StashSaveCommandArgs { + message?: string; + unstagedOnly?: boolean; + + goBackCommand?: CommandQuickPickItem; +} + export class StashSaveCommand extends Command { constructor(private git: GitService) { super(Commands.StashSave); } - async execute(message?: string, unstagedOnly: boolean = false, goBackCommand?: CommandQuickPickItem) { + async execute(args: StashSaveCommandArgs = { unstagedOnly : false }) { if (!this.git.config.insiders) return undefined; if (!this.git.repoPath) return undefined; + if (args.unstagedOnly === undefined) { + args.unstagedOnly = false; + } + try { - if (message == null) { - message = await window.showInputBox({ + if (args.message == null) { + args.message = await window.showInputBox({ prompt: `Please provide a stash message`, placeHolder: `Stash message` } as InputBoxOptions); - if (message === undefined) return goBackCommand && goBackCommand.execute(); + if (args.message === undefined) return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute(); } - return await this.git.stashSave(this.git.repoPath, message, unstagedOnly); + return await this.git.stashSave(this.git.repoPath, args.message, args.unstagedOnly); } catch (ex) { Logger.error(ex, 'StashSaveCommand'); diff --git a/src/commands/toggleBlame.ts b/src/commands/toggleBlame.ts index 5d18fe2..b26d053 100644 --- a/src/commands/toggleBlame.ts +++ b/src/commands/toggleBlame.ts @@ -4,21 +4,21 @@ import { BlameAnnotationController } from '../blameAnnotationController'; import { Commands, EditorCommand } from './common'; import { Logger } from '../logger'; +export interface ToggleBlameCommandArgs { + sha?: string; +} + export class ToggleBlameCommand extends EditorCommand { constructor(private annotationController: BlameAnnotationController) { super(Commands.ToggleBlame); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string): Promise { - if (editor && editor.document && editor.document.isDirty) return undefined; + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleBlameCommandArgs = {}): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; try { - if (sha) { - return this.annotationController.toggleBlameAnnotation(editor, sha); - } - - return this.annotationController.toggleBlameAnnotation(editor, editor.selection.active.line); + return this.annotationController.toggleBlameAnnotation(editor, args.sha !== undefined ? args.sha : editor.selection.active.line); } catch (ex) { Logger.error(ex, 'ToggleBlameCommand'); diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index 6042e35..8967d89 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -1,11 +1,16 @@ 'use strict'; import { commands, Range, Uri } from 'vscode'; import { BuiltInCommands } from '../../constants'; +import { GitLogCommit } from '../../gitService'; -export type RemoteOpenType = 'branch' | 'commit' | 'file' | 'working-file'; +export type RemoteResourceType = 'branch' | 'commit' | 'file' | 'working-file'; +export type RemoteResource = { type: 'branch', branch: string } | + { type: 'commit', sha: string } | + { type: 'file', branch?: string, commit?: GitLogCommit, fileName: string, range?: Range, sha?: string } | + { type: 'working-file', branch?: string, fileName: string, range?: Range }; -export function getNameFromRemoteOpenType(type: RemoteOpenType) { - switch (type) { +export function getNameFromRemoteResource(resource: RemoteResource) { + switch (resource.type) { case 'branch': return 'Branch'; case 'commit': return 'Commit'; case 'file': return 'File'; @@ -34,19 +39,16 @@ export abstract class RemoteProvider { return commands.executeCommand(BuiltInCommands.Open, Uri.parse(url)); } - open(type: 'branch', branch: string): Promise<{}>; - open(type: 'commit', sha: string): Promise<{}>; - open(type: 'file', fileName: string, branch?: string, sha?: string, range?: Range): Promise<{}>; - open(type: RemoteOpenType, ...args: any[]): Promise<{}>; - open(type: RemoteOpenType, branchOrShaOrFileName: string, fileBranch?: string, fileSha?: string, fileRange?: Range): Promise<{}> { - switch (type) { + open(resource: RemoteResource): Promise<{}> { + switch (resource.type) { case 'branch': - return this.openBranch(branchOrShaOrFileName); + return this.openBranch(resource.branch); case 'commit': - return this.openCommit(branchOrShaOrFileName); + return this.openCommit(resource.sha); case 'file': + return this.openFile(resource.fileName, resource.branch, resource.sha, resource.range); case 'working-file': - return this.openFile(branchOrShaOrFileName, fileBranch, fileSha, fileRange); + return this.openFile(resource.fileName, resource.branch, undefined, resource.range); } } diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 523f5c0..3b51b5f 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -1,7 +1,7 @@ 'use strict'; import { Functions, Iterables, Strings } from './system'; import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; -import { Commands } from './commands'; +import { Commands, DiffWithPreviousCommandArgs, ShowBlameHistoryCommandArgs, ShowFileHistoryCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands'; import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants'; import { CodeLensCommand, CodeLensLocation, IConfig, ICodeLensLanguageLocation } from './configuration'; import { GitCommit, GitService, GitUri, IGitBlame, IGitBlameLines } from './gitService'; @@ -321,7 +321,15 @@ export class GitCodeLensProvider implements CodeLensProvider { lens.command = { title: title, command: Commands.ShowBlameHistory, - arguments: [Uri.file(lens.uri.fsPath), lens.blameRange, position, commit && commit.sha, line] + arguments: [ + Uri.file(lens.uri.fsPath), + { + line, + position, + range: lens.blameRange, + sha: commit && commit.sha + } as ShowBlameHistoryCommandArgs + ] }; return lens; } @@ -339,7 +347,14 @@ export class GitCodeLensProvider implements CodeLensProvider { lens.command = { title: title, command: Commands.ShowFileHistory, - arguments: [Uri.file(lens.uri.fsPath), position, commit && commit.sha, line] + arguments: [ + Uri.file(lens.uri.fsPath), + { + line, + position, + sha: commit && commit.sha + } as ShowFileHistoryCommandArgs + ] }; return lens; } @@ -355,8 +370,10 @@ export class GitCodeLensProvider implements CodeLensProvider { command: Commands.DiffWithPrevious, arguments: [ Uri.file(lens.uri.fsPath), - commit, - lens.isFullRange ? undefined : lens.blameRange + { + commit: commit, + range: lens.isFullRange ? undefined : lens.blameRange + } as DiffWithPreviousCommandArgs ] }; return lens; @@ -366,7 +383,12 @@ export class GitCodeLensProvider implements CodeLensProvider { lens.command = { title: title, command: CodeLensCommand.ShowQuickCommitDetails, - arguments: [Uri.file(lens.uri.fsPath), commit === undefined ? undefined : commit.sha, commit] + arguments: [ + Uri.file(lens.uri.fsPath), + { + commit, + sha: commit === undefined ? undefined : commit.sha + } as ShowQuickCommitDetailsCommandArgs] }; return lens; } @@ -375,7 +397,12 @@ export class GitCodeLensProvider implements CodeLensProvider { lens.command = { title: title, command: CodeLensCommand.ShowQuickCommitFileDetails, - arguments: [Uri.file(lens.uri.fsPath), commit === undefined ? undefined : commit.sha, commit] + arguments: [ + Uri.file(lens.uri.fsPath), + { + commit, + sha: commit === undefined ? undefined : commit.sha + } as ShowQuickCommitFileDetailsCommandArgs] }; return lens; } @@ -384,7 +411,12 @@ export class GitCodeLensProvider implements CodeLensProvider { lens.command = { title: title, command: CodeLensCommand.ShowQuickFileHistory, - arguments: [Uri.file(lens.uri.fsPath), lens.isFullRange ? undefined : lens.blameRange] + arguments: [ + Uri.file(lens.uri.fsPath), + { + range: lens.isFullRange ? undefined : lens.blameRange + } as ShowQuickFileHistoryCommandArgs + ] }; return lens; } diff --git a/src/gitRevisionCodeLensProvider.ts b/src/gitRevisionCodeLensProvider.ts index 5989c6e..6658b4c 100644 --- a/src/gitRevisionCodeLensProvider.ts +++ b/src/gitRevisionCodeLensProvider.ts @@ -1,7 +1,7 @@ 'use strict'; // import { Iterables } from './system'; import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode'; -import { Commands } from './commands'; +import { Commands, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs } from './commands'; import { DocumentSchemes } from './constants'; import { GitCommit, GitService, GitUri } from './gitService'; @@ -55,8 +55,10 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { command: Commands.DiffWithWorking, arguments: [ Uri.file(lens.fileName), - lens.commit, - lens.range.start.line + { + commit: lens.commit, + line: lens.range.start.line + } as DiffWithWorkingCommandArgs ] }; return Promise.resolve(lens); @@ -68,8 +70,10 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { command: Commands.DiffWithPrevious, arguments: [ Uri.file(lens.fileName), - lens.commit, - lens.range.start.line + { + commit: lens.commit, + line: lens.range.start.line + } as DiffWithPreviousCommandArgs ] }; return Promise.resolve(lens); diff --git a/src/gitService.ts b/src/gitService.ts index bec303e..c1187e8 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -1,935 +1,943 @@ -'use strict'; -import { Iterables, Objects } from './system'; -import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextEditor, Uri, workspace } from 'vscode'; -import { CommandContext, setCommandContext } from './commands'; -import { CodeLensVisibility, IConfig } from './configuration'; -import { DocumentSchemes, ExtensionKey } from './constants'; -import { Git, GitBlameParser, GitBranch, GitCommit, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog, IGitStash, IGitStatus } from './git/git'; -import { IGitUriData, GitUri } from './git/gitUri'; -import { GitCodeLensProvider } from './gitCodeLensProvider'; -import { Logger } from './logger'; -import * as fs from 'fs'; -import * as ignore from 'ignore'; -import * as moment from 'moment'; -import * as path from 'path'; - -export { GitUri }; -export * from './git/models/models'; -export { getNameFromRemoteOpenType, RemoteOpenType, RemoteProvider } from './git/remotes/provider'; -export * from './git/gitContextTracker'; - -class UriCacheEntry { - - constructor(public uri: GitUri) { } -} - -class GitCacheEntry { - - blame?: ICachedBlame; - log?: ICachedLog; - - get hasErrors(): boolean { - return (this.blame !== undefined && this.blame.errorMessage !== undefined) || - (this.log !== undefined && this.log.errorMessage !== undefined); - } - - constructor(public key: string) { } -} - -interface ICachedItem { - //date: Date; - item: Promise; - errorMessage?: string; -} - -interface ICachedBlame extends ICachedItem { } -interface ICachedLog extends ICachedItem { } - -enum RemoveCacheReason { - DocumentClosed, - DocumentSaved -} - -export type GitRepoSearchBy = 'author' | 'files' | 'message' | 'sha'; -export const GitRepoSearchBy = { - Author: 'author' as GitRepoSearchBy, - Files: 'files' as GitRepoSearchBy, - Message: 'message' as GitRepoSearchBy, - Sha: 'sha' as GitRepoSearchBy -}; - -export class GitService extends Disposable { - - private _onDidChangeGitCache = new EventEmitter(); - get onDidChangeGitCache(): Event { - return this._onDidChangeGitCache.event; - } - - private _onDidBlameFail = new EventEmitter(); - get onDidBlameFail(): Event { - return this._onDidBlameFail.event; - } - - private _gitCache: Map; - private _remotesCache: Map; - private _cacheDisposable: Disposable | undefined; - private _uriCache: Map; - - config: IConfig; - private _codeLensProvider: GitCodeLensProvider | undefined; - private _codeLensProviderDisposable: Disposable | undefined; - private _disposable: Disposable | undefined; - private _fsWatcher: FileSystemWatcher | undefined; - private _gitignore: Promise; - - static EmptyPromise: Promise = Promise.resolve(undefined); - - constructor(private context: ExtensionContext, public repoPath: string) { - super(() => this.dispose()); - - this._gitCache = new Map(); - this._remotesCache = new Map(); - this._uriCache = new Map(); - - this._onConfigurationChanged(); - - const subscriptions: Disposable[] = []; - - subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); - - this._disposable = Disposable.from(...subscriptions); - } - - dispose() { - this._disposable && this._disposable.dispose(); - - this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = undefined; - this._codeLensProvider = undefined; - - this._cacheDisposable && this._cacheDisposable.dispose(); - this._cacheDisposable = undefined; - - this._fsWatcher && this._fsWatcher.dispose(); - this._fsWatcher = undefined; - - this._gitCache.clear(); - this._remotesCache.clear(); - this._uriCache.clear(); - } - - public get UseCaching() { - return this.config.advanced.caching.enabled; - } - - private _onConfigurationChanged() { - const cfg = workspace.getConfiguration().get(ExtensionKey)!; - - const codeLensChanged = !Objects.areEquivalent(cfg.codeLens, this.config && this.config.codeLens); - const advancedChanged = !Objects.areEquivalent(cfg.advanced, this.config && this.config.advanced); - - if (codeLensChanged) { - Logger.log('CodeLens config changed; resetting CodeLens provider'); - if (cfg.codeLens.visibility === CodeLensVisibility.Auto && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) { - if (this._codeLensProvider) { - this._codeLensProvider.reset(); - } - else { - this._codeLensProvider = new GitCodeLensProvider(this.context, this); - this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._codeLensProvider); - } - } - else { - this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = undefined; - this._codeLensProvider = undefined; - } - - setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.visibility !== CodeLensVisibility.Off && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)); - } - - if (advancedChanged) { - if (cfg.advanced.caching.enabled) { - this._cacheDisposable && this._cacheDisposable.dispose(); - - this._fsWatcher = this._fsWatcher || workspace.createFileSystemWatcher('**/.git/index', true, false, true); - - const disposables: Disposable[] = []; - - disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentClosed))); - disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved))); - disposables.push(this._fsWatcher.onDidChange(this._onGitChanged, this)); - - this._cacheDisposable = Disposable.from(...disposables); - } - else { - this._cacheDisposable && this._cacheDisposable.dispose(); - this._cacheDisposable = undefined; - - this._fsWatcher && this._fsWatcher.dispose(); - this._fsWatcher = undefined; - - this._gitCache.clear(); - this._remotesCache.clear(); - } - - this._gitignore = new Promise((resolve, reject) => { - if (!cfg.advanced.gitignore.enabled) { - resolve(undefined); - return; - } - - const gitignorePath = path.join(this.repoPath, '.gitignore'); - fs.exists(gitignorePath, e => { - if (e) { - fs.readFile(gitignorePath, 'utf8', (err, data) => { - if (!err) { - resolve(ignore().add(data)); - return; - } - resolve(undefined); - }); - return; - } - resolve(undefined); - }); - }); - } - - this.config = cfg; - } - - private _onGitChanged() { - this._gitCache.clear(); - - this._onDidChangeGitCache.fire(); - this._codeLensProvider && this._codeLensProvider.reset(); - } - - private _removeCachedEntry(document: TextDocument, reason: RemoveCacheReason) { - if (!this.UseCaching) return; - if (document.uri.scheme !== DocumentSchemes.File) return; - - const cacheKey = this.getCacheEntryKey(document.fileName); - - if (reason === RemoveCacheReason.DocumentSaved) { - // Don't remove broken blame on save (since otherwise we'll have to run the broken blame again) - const entry = this._gitCache.get(cacheKey); - if (entry && entry.hasErrors) return; - } - - if (this._gitCache.delete(cacheKey)) { - Logger.log(`Clear cache entry for '${cacheKey}', reason=${RemoveCacheReason[reason]}`); - - if (reason === RemoveCacheReason.DocumentSaved) { - this._onDidChangeGitCache.fire(); - - // Refresh the codelenses with the updated blame - this._codeLensProvider && this._codeLensProvider.reset(); - } - } - } - - private async _fileExists(repoPath: string, fileName: string): Promise { - return await new Promise((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), e => resolve(e))); - } - - async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise { - let log = await this.getLogForFile(repoPath, fileName, sha, 1, undefined, true); - let commit = log && Iterables.first(log.commits.values()); - if (commit) return commit; - - const nextFileName = await this.findNextFileName(repoPath, fileName, sha); - if (nextFileName) { - log = await this.getLogForFile(repoPath, nextFileName, sha, 1, undefined, true); - commit = log && Iterables.first(log.commits.values()); - } - - return commit; - } - - async findNextFileName(repoPath: string | undefined, fileName: string, sha?: string): Promise { - [fileName, repoPath] = Git.splitPath(fileName, repoPath); - - return (await this._fileExists(repoPath, fileName)) - ? fileName - : await this._findNextFileName(repoPath, fileName, sha); - } - - async _findNextFileName(repoPath: string, fileName: string, sha?: string): Promise { - if (sha === undefined) { - // Get the most recent commit for this file name - const c = await this.getLogCommit(repoPath, fileName); - if (!c) return undefined; - - sha = c.sha; - } - - // Get the full commit (so we can see if there are any matching renames in the file statuses) - const log = await this.getLogForRepo(repoPath, sha, 1); - if (!log) return undefined; - - const c = Iterables.first(log.commits.values()); - const status = c.fileStatuses.find(_ => _.originalFileName === fileName); - if (!status) return undefined; - - return status.fileName; - } - - async findWorkingFileName(commit: GitCommit): Promise; - async findWorkingFileName(repoPath: string | undefined, fileName: string): Promise; - async findWorkingFileName(commitOrRepoPath: GitCommit | string | undefined, fileName?: string): Promise { - let repoPath: string | undefined; - if (commitOrRepoPath === undefined || typeof commitOrRepoPath === 'string') { - repoPath = commitOrRepoPath; - if (fileName === undefined) throw new Error('Invalid fileName'); - - [fileName] = Git.splitPath(fileName, repoPath); - } - else { - const c = commitOrRepoPath; - repoPath = c.repoPath; - if (c.workingFileName && await this._fileExists(repoPath, c.workingFileName)) return c.workingFileName; - fileName = c.fileName; - } - - while (true) { - if (await this._fileExists(repoPath!, fileName)) return fileName; - - fileName = await this._findNextFileName(repoPath!, fileName); - if (fileName === undefined) return undefined; - } - } - - public async getBlameability(uri: GitUri): Promise { - if (!this.UseCaching) return await this.isTracked(uri); - - const cacheKey = this.getCacheEntryKey(uri.fsPath); - const entry = this._gitCache.get(cacheKey); - if (!entry) return await this.isTracked(uri); - - return !entry.hasErrors; - } - - async getBlameForFile(uri: GitUri): Promise { - Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); - - const fileName = uri.fsPath; - - let entry: GitCacheEntry | undefined; - if (this.UseCaching && !uri.sha) { - const cacheKey = this.getCacheEntryKey(fileName); - entry = this._gitCache.get(cacheKey); - - if (entry !== undefined && entry.blame !== undefined) return entry.blame.item; - if (entry === undefined) { - entry = new GitCacheEntry(cacheKey); - } - } - - const promise = this._getBlameForFile(uri, fileName, entry); - - if (entry) { - Logger.log(`Add blame cache for '${entry.key}'`); - - entry.blame = { - //date: new Date(), - item: promise - } as ICachedBlame; - - this._gitCache.set(entry.key, entry); - } - - return promise; - } - - private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined): Promise { - const [file, root] = Git.splitPath(fileName, uri.repoPath, false); - - const ignore = await this._gitignore; - if (ignore && !ignore.filter([file]).length) { - Logger.log(`Skipping blame; '${fileName}' is gitignored`); - if (entry && entry.key) { - this._onDidBlameFail.fire(entry.key); - } - return await GitService.EmptyPromise as IGitBlame; - } - - try { - const data = await Git.blame(root, file, uri.sha); - return GitBlameParser.parse(data, root, file); - } - catch (ex) { - // Trap and cache expected blame errors - if (entry) { - const msg = ex && ex.toString(); - Logger.log(`Replace blame cache with empty promise for '${entry.key}'`); - - entry.blame = { - //date: new Date(), - item: GitService.EmptyPromise, - errorMessage: msg - } as ICachedBlame; - - this._onDidBlameFail.fire(entry.key); - this._gitCache.set(entry.key, entry); - return await GitService.EmptyPromise as IGitBlame; - } - - return undefined; - } - } - - async getBlameForLine(uri: GitUri, line: number): Promise { - Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, ${uri.sha})`); - - if (this.UseCaching && !uri.sha) { - const blame = await this.getBlameForFile(uri); - if (blame === undefined) return undefined; - - const blameLine = blame.lines[line]; - if (blameLine === undefined) return undefined; - - const commit = blame.commits.get(blameLine.sha); - if (commit === undefined) return undefined; - - return { - author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }), - commit: commit, - line: blameLine - } as IGitBlameLine; - } - - const fileName = uri.fsPath; - - try { - const data = await Git.blame(uri.repoPath, fileName, uri.sha, line + 1, line + 1); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName); - if (!blame) return undefined; - - const commit = Iterables.first(blame.commits.values()); - if (uri.repoPath) { - commit.repoPath = uri.repoPath; - } - return { - author: Iterables.first(blame.authors.values()), - commit: commit, - line: blame.lines[line] - } as IGitBlameLine; - } - catch (ex) { - return undefined; - } - } - - async getBlameForRange(uri: GitUri, range: Range): Promise { - Logger.log(`getBlameForRange('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); - - const blame = await this.getBlameForFile(uri); - if (!blame) return undefined; - - return this.getBlameForRangeSync(blame, uri, range); - } - - getBlameForRangeSync(blame: IGitBlame, uri: GitUri, range: Range): IGitBlameLines | undefined { - Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); - - if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame); - - if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { - return Object.assign({ allLines: blame.lines }, blame); - } - - const lines = blame.lines.slice(range.start.line, range.end.line + 1); - const shas: Set = new Set(); - lines.forEach(l => shas.add(l.sha)); - - const authors: Map = new Map(); - const commits: Map = new Map(); - blame.commits.forEach(c => { - if (!shas.has(c.sha)) return; - - const commit: GitCommit = new GitCommit('blame', c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, - c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName); - commits.set(c.sha, commit); - - let author = authors.get(commit.author); - if (author === undefined) { - author = { - name: commit.author, - lineCount: 0 - }; - authors.set(author.name, author); - } - - author.lineCount += commit.lines.length; - }); - - const sortedAuthors: Map = new Map(); - Array.from(authors.values()) - .sort((a, b) => b.lineCount - a.lineCount) - .forEach(a => sortedAuthors.set(a.name, a)); - - return { - authors: sortedAuthors, - commits: commits, - lines: lines, - allLines: blame.lines - } as IGitBlameLines; - } - - async getBlameLocations(uri: GitUri, range: Range, selectedSha?: string, line?: number): Promise { - Logger.log(`getBlameLocations('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); - - const blame = await this.getBlameForRange(uri, range); - if (!blame) return undefined; - - const commitCount = blame.commits.size; - - const locations: Array = []; - Iterables.forEach(blame.commits.values(), (c, i) => { - if (c.isUncommitted) return; - - const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; - const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration); - locations.push(new Location(uri, new Position(0, 0))); - if (c.sha === selectedSha) { - locations.push(new Location(uri, new Position((line || 0) + 1, 0))); - } - }); - - return locations; - } - - async getBranch(repoPath: string): Promise { - Logger.log(`getBranch('${repoPath}')`); - - const data = await Git.branch(repoPath, false); - const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_)); - return branches.find(_ => _.current); - } - - async getBranches(repoPath: string): Promise { - Logger.log(`getBranches('${repoPath}')`); - - const data = await Git.branch(repoPath, true); - const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_)); - return branches; - } - - getCacheEntryKey(fileName: string) { - return Git.normalizePath(fileName).toLowerCase(); - } - - async getConfig(key: string, repoPath?: string): Promise { - Logger.log(`getConfig('${key}', '${repoPath}')`); - - return await Git.config_get(key, repoPath); - } - - getGitUriForFile(fileName: string) { - const cacheKey = this.getCacheEntryKey(fileName); - const entry = this._uriCache.get(cacheKey); - return entry && entry.uri; - } - - async getLogCommit(repoPath: string | undefined, fileName: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise; - async getLogCommit(repoPath: string | undefined, fileName: string, sha: string | undefined, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise; - async getLogCommit(repoPath: string | undefined, fileName: string, shaOrOptions?: string | undefined | { firstIfMissing?: boolean, previous?: boolean }, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise { - let sha: string | undefined = undefined; - if (typeof shaOrOptions === 'string') { - sha = shaOrOptions; - } - else if (!options) { - options = shaOrOptions; - } - - options = options || {}; - - const log = await this.getLogForFile(repoPath, fileName, sha, options.previous ? 2 : 1); - if (!log) return undefined; - - const commit = sha && log.commits.get(sha); - if (!commit && sha && !options.firstIfMissing) return undefined; - - return commit || Iterables.first(log.commits.values()); - } - - async getLogForRepo(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false): Promise { - Logger.log(`getLogForRepo('${repoPath}', ${sha}, ${maxCount})`); - - if (maxCount == null) { - maxCount = this.config.advanced.maxQuickHistory || 0; - } - - try { - const data = await Git.log(repoPath, sha, maxCount, reverse); - return GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined); - } - catch (ex) { - return undefined; - } - } - - async getLogForRepoSearch(repoPath: string, search: string, searchBy: GitRepoSearchBy, maxCount?: number): Promise { - Logger.log(`getLogForRepoSearch('${repoPath}', ${search}, ${searchBy}, ${maxCount})`); - - if (maxCount == null) { - maxCount = this.config.advanced.maxQuickHistory || 0; - } - - let searchArgs: string[] | undefined = undefined; - switch (searchBy) { - case GitRepoSearchBy.Author: - searchArgs = [`--author=${search}`]; - break; - case GitRepoSearchBy.Files: - searchArgs = [`--`, `${search}`]; - break; - case GitRepoSearchBy.Message: - searchArgs = [`--grep=${search}`]; - break; - case GitRepoSearchBy.Sha: - searchArgs = [search]; - maxCount = 1; - break; - } - - try { - const data = await Git.log_search(repoPath, searchArgs, maxCount); - return GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined); - } - catch (ex) { - return undefined; - } - } - - getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise { - Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, ${range && `[${range.start.line}, ${range.end.line}]`}, ${reverse})`); - - let entry: GitCacheEntry | undefined; - if (this.UseCaching && !sha && !range && !maxCount && !reverse) { - const cacheKey = this.getCacheEntryKey(fileName); - entry = this._gitCache.get(cacheKey); - - if (entry !== undefined && entry.log !== undefined) return entry.log.item; - if (entry === undefined) { - entry = new GitCacheEntry(cacheKey); - } - } - - const promise = this._getLogForFile(repoPath, fileName, sha, range, maxCount, reverse, entry); - - if (entry) { - Logger.log(`Add log cache for '${entry.key}'`); - - entry.log = { - //date: new Date(), - item: promise - } as ICachedLog; - - this._gitCache.set(entry.key, entry); - } - - return promise; - } - - private async _getLogForFile(repoPath: string | undefined, fileName: string, sha: string | undefined, range: Range | undefined, maxCount: number | undefined, reverse: boolean, entry: GitCacheEntry | undefined): Promise { - const [file, root] = Git.splitPath(fileName, repoPath, false); - - const ignore = await this._gitignore; - if (ignore && !ignore.filter([file]).length) { - Logger.log(`Skipping log; '${fileName}' is gitignored`); - return await GitService.EmptyPromise as IGitLog; - } - - try { - const data = await Git.log_file(root, file, sha, maxCount, reverse, range && range.start.line + 1, range && range.end.line + 1); - return GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range); - } - catch (ex) { - // Trap and cache expected log errors - if (entry) { - const msg = ex && ex.toString(); - Logger.log(`Replace log cache with empty promise for '${entry.key}'`); - - entry.log = { - //date: new Date(), - item: GitService.EmptyPromise, - errorMessage: msg - } as ICachedLog; - - this._gitCache.set(entry.key, entry); - return await GitService.EmptyPromise as IGitLog; - } - - return undefined; - } - } - - async getLogLocations(uri: GitUri, selectedSha?: string, line?: number): Promise { - Logger.log(`getLogLocations('${uri.repoPath}', '${uri.fsPath}', ${uri.sha}, ${selectedSha}, ${line})`); - - const log = await this.getLogForFile(uri.repoPath, uri.fsPath, uri.sha); - if (!log) return undefined; - - const commitCount = log.commits.size; - - const locations: Array = []; - Iterables.forEach(log.commits.values(), (c, i) => { - if (c.isUncommitted) return; - - const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; - const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration); - locations.push(new Location(uri, new Position(0, 0))); - if (c.sha === selectedSha) { - locations.push(new Location(uri, new Position((line || 0) + 1, 0))); - } - }); - - return locations; - } - - async getRemotes(repoPath: string): Promise { - if (!this.config.insiders) return Promise.resolve([]); - if (!repoPath) return Promise.resolve([]); - - Logger.log(`getRemotes('${repoPath}')`); - - if (this.UseCaching) { - const remotes = this._remotesCache.get(repoPath); - if (remotes !== undefined) return remotes; - } - - const data = await Git.remote(repoPath); - const remotes = data.split('\n').filter(_ => !!_).map(_ => new GitRemote(_)); - if (this.UseCaching) { - this._remotesCache.set(repoPath, remotes); - } - return remotes; - } - - getRepoPath(cwd: string): Promise { +'use strict'; +import { Iterables, Objects } from './system'; +import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextEditor, Uri, workspace } from 'vscode'; +import { CommandContext, setCommandContext } from './commands'; +import { CodeLensVisibility, IConfig } from './configuration'; +import { DocumentSchemes, ExtensionKey } from './constants'; +import { Git, GitBlameParser, GitBranch, GitCommit, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog, IGitStash, IGitStatus } from './git/git'; +import { IGitUriData, GitUri } from './git/gitUri'; +import { GitCodeLensProvider } from './gitCodeLensProvider'; +import { Logger } from './logger'; +import * as fs from 'fs'; +import * as ignore from 'ignore'; +import * as moment from 'moment'; +import * as path from 'path'; + +export { GitUri }; +export * from './git/models/models'; +export { getNameFromRemoteResource, RemoteResource, RemoteProvider } from './git/remotes/provider'; +export * from './git/gitContextTracker'; + +class UriCacheEntry { + + constructor(public uri: GitUri) { } +} + +class GitCacheEntry { + + blame?: ICachedBlame; + log?: ICachedLog; + + get hasErrors(): boolean { + return (this.blame !== undefined && this.blame.errorMessage !== undefined) || + (this.log !== undefined && this.log.errorMessage !== undefined); + } + + constructor(public key: string) { } +} + +interface ICachedItem { + //date: Date; + item: Promise; + errorMessage?: string; +} + +interface ICachedBlame extends ICachedItem { } +interface ICachedLog extends ICachedItem { } + +enum RemoveCacheReason { + DocumentClosed, + DocumentSaved +} + +export type GitRepoSearchBy = 'author' | 'files' | 'message' | 'sha'; +export const GitRepoSearchBy = { + Author: 'author' as GitRepoSearchBy, + Files: 'files' as GitRepoSearchBy, + Message: 'message' as GitRepoSearchBy, + Sha: 'sha' as GitRepoSearchBy +}; + +export class GitService extends Disposable { + + private _onDidChangeGitCache = new EventEmitter(); + get onDidChangeGitCache(): Event { + return this._onDidChangeGitCache.event; + } + + private _onDidBlameFail = new EventEmitter(); + get onDidBlameFail(): Event { + return this._onDidBlameFail.event; + } + + private _gitCache: Map; + private _remotesCache: Map; + private _cacheDisposable: Disposable | undefined; + private _uriCache: Map; + + config: IConfig; + private _codeLensProvider: GitCodeLensProvider | undefined; + private _codeLensProviderDisposable: Disposable | undefined; + private _disposable: Disposable | undefined; + private _fsWatcher: FileSystemWatcher | undefined; + private _gitignore: Promise; + + static EmptyPromise: Promise = Promise.resolve(undefined); + + constructor(private context: ExtensionContext, public repoPath: string) { + super(() => this.dispose()); + + this._gitCache = new Map(); + this._remotesCache = new Map(); + this._uriCache = new Map(); + + this._onConfigurationChanged(); + + const subscriptions: Disposable[] = []; + + subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); + + this._disposable = Disposable.from(...subscriptions); + } + + dispose() { + this._disposable && this._disposable.dispose(); + + this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); + this._codeLensProviderDisposable = undefined; + this._codeLensProvider = undefined; + + this._cacheDisposable && this._cacheDisposable.dispose(); + this._cacheDisposable = undefined; + + this._fsWatcher && this._fsWatcher.dispose(); + this._fsWatcher = undefined; + + this._gitCache.clear(); + this._remotesCache.clear(); + this._uriCache.clear(); + } + + public get UseCaching() { + return this.config.advanced.caching.enabled; + } + + private _onConfigurationChanged() { + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + + const codeLensChanged = !Objects.areEquivalent(cfg.codeLens, this.config && this.config.codeLens); + const advancedChanged = !Objects.areEquivalent(cfg.advanced, this.config && this.config.advanced); + + if (codeLensChanged) { + Logger.log('CodeLens config changed; resetting CodeLens provider'); + if (cfg.codeLens.visibility === CodeLensVisibility.Auto && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) { + if (this._codeLensProvider) { + this._codeLensProvider.reset(); + } + else { + this._codeLensProvider = new GitCodeLensProvider(this.context, this); + this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._codeLensProvider); + } + } + else { + this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); + this._codeLensProviderDisposable = undefined; + this._codeLensProvider = undefined; + } + + setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.visibility !== CodeLensVisibility.Off && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)); + } + + if (advancedChanged) { + if (cfg.advanced.caching.enabled) { + this._cacheDisposable && this._cacheDisposable.dispose(); + + this._fsWatcher = this._fsWatcher || workspace.createFileSystemWatcher('**/.git/index', true, false, true); + + const disposables: Disposable[] = []; + + disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentClosed))); + disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved))); + disposables.push(this._fsWatcher.onDidChange(this._onGitChanged, this)); + + this._cacheDisposable = Disposable.from(...disposables); + } + else { + this._cacheDisposable && this._cacheDisposable.dispose(); + this._cacheDisposable = undefined; + + this._fsWatcher && this._fsWatcher.dispose(); + this._fsWatcher = undefined; + + this._gitCache.clear(); + this._remotesCache.clear(); + } + + this._gitignore = new Promise((resolve, reject) => { + if (!cfg.advanced.gitignore.enabled) { + resolve(undefined); + return; + } + + const gitignorePath = path.join(this.repoPath, '.gitignore'); + fs.exists(gitignorePath, e => { + if (e) { + fs.readFile(gitignorePath, 'utf8', (err, data) => { + if (!err) { + resolve(ignore().add(data)); + return; + } + resolve(undefined); + }); + return; + } + resolve(undefined); + }); + }); + } + + this.config = cfg; + } + + private _onGitChanged() { + this._gitCache.clear(); + + this._onDidChangeGitCache.fire(); + this._codeLensProvider && this._codeLensProvider.reset(); + } + + private _removeCachedEntry(document: TextDocument, reason: RemoveCacheReason) { + if (!this.UseCaching) return; + if (document.uri.scheme !== DocumentSchemes.File) return; + + const cacheKey = this.getCacheEntryKey(document.fileName); + + if (reason === RemoveCacheReason.DocumentSaved) { + // Don't remove broken blame on save (since otherwise we'll have to run the broken blame again) + const entry = this._gitCache.get(cacheKey); + if (entry && entry.hasErrors) return; + } + + if (this._gitCache.delete(cacheKey)) { + Logger.log(`Clear cache entry for '${cacheKey}', reason=${RemoveCacheReason[reason]}`); + + if (reason === RemoveCacheReason.DocumentSaved) { + this._onDidChangeGitCache.fire(); + + // Refresh the codelenses with the updated blame + this._codeLensProvider && this._codeLensProvider.reset(); + } + } + } + + private async _fileExists(repoPath: string, fileName: string): Promise { + return await new Promise((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), e => resolve(e))); + } + + async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise { + let log = await this.getLogForFile(repoPath, fileName, sha, 1, undefined, true); + let commit = log && Iterables.first(log.commits.values()); + if (commit) return commit; + + const nextFileName = await this.findNextFileName(repoPath, fileName, sha); + if (nextFileName) { + log = await this.getLogForFile(repoPath, nextFileName, sha, 1, undefined, true); + commit = log && Iterables.first(log.commits.values()); + } + + return commit; + } + + async findNextFileName(repoPath: string | undefined, fileName: string, sha?: string): Promise { + [fileName, repoPath] = Git.splitPath(fileName, repoPath); + + return (await this._fileExists(repoPath, fileName)) + ? fileName + : await this._findNextFileName(repoPath, fileName, sha); + } + + async _findNextFileName(repoPath: string, fileName: string, sha?: string): Promise { + if (sha === undefined) { + // Get the most recent commit for this file name + const c = await this.getLogCommit(repoPath, fileName); + if (!c) return undefined; + + sha = c.sha; + } + + // Get the full commit (so we can see if there are any matching renames in the file statuses) + const log = await this.getLogForRepo(repoPath, sha, 1); + if (!log) return undefined; + + const c = Iterables.first(log.commits.values()); + const status = c.fileStatuses.find(_ => _.originalFileName === fileName); + if (!status) return undefined; + + return status.fileName; + } + + async findWorkingFileName(commit: GitCommit): Promise; + async findWorkingFileName(repoPath: string | undefined, fileName: string): Promise; + async findWorkingFileName(commitOrRepoPath: GitCommit | string | undefined, fileName?: string): Promise { + let repoPath: string | undefined; + if (commitOrRepoPath === undefined || typeof commitOrRepoPath === 'string') { + repoPath = commitOrRepoPath; + if (fileName === undefined) throw new Error('Invalid fileName'); + + [fileName] = Git.splitPath(fileName, repoPath); + } + else { + const c = commitOrRepoPath; + repoPath = c.repoPath; + if (c.workingFileName && await this._fileExists(repoPath, c.workingFileName)) return c.workingFileName; + fileName = c.fileName; + } + + while (true) { + if (await this._fileExists(repoPath!, fileName)) return fileName; + + fileName = await this._findNextFileName(repoPath!, fileName); + if (fileName === undefined) return undefined; + } + } + + public async getBlameability(uri: GitUri): Promise { + if (!this.UseCaching) return await this.isTracked(uri); + + const cacheKey = this.getCacheEntryKey(uri.fsPath); + const entry = this._gitCache.get(cacheKey); + if (!entry) return await this.isTracked(uri); + + return !entry.hasErrors; + } + + async getBlameForFile(uri: GitUri): Promise { + Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); + + const fileName = uri.fsPath; + + let entry: GitCacheEntry | undefined; + if (this.UseCaching && !uri.sha) { + const cacheKey = this.getCacheEntryKey(fileName); + entry = this._gitCache.get(cacheKey); + + if (entry !== undefined && entry.blame !== undefined) return entry.blame.item; + if (entry === undefined) { + entry = new GitCacheEntry(cacheKey); + } + } + + const promise = this._getBlameForFile(uri, fileName, entry); + + if (entry) { + Logger.log(`Add blame cache for '${entry.key}'`); + + entry.blame = { + //date: new Date(), + item: promise + } as ICachedBlame; + + this._gitCache.set(entry.key, entry); + } + + return promise; + } + + private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined): Promise { + const [file, root] = Git.splitPath(fileName, uri.repoPath, false); + + const ignore = await this._gitignore; + if (ignore && !ignore.filter([file]).length) { + Logger.log(`Skipping blame; '${fileName}' is gitignored`); + if (entry && entry.key) { + this._onDidBlameFail.fire(entry.key); + } + return await GitService.EmptyPromise as IGitBlame; + } + + try { + const data = await Git.blame(root, file, uri.sha); + return GitBlameParser.parse(data, root, file); + } + catch (ex) { + // Trap and cache expected blame errors + if (entry) { + const msg = ex && ex.toString(); + Logger.log(`Replace blame cache with empty promise for '${entry.key}'`); + + entry.blame = { + //date: new Date(), + item: GitService.EmptyPromise, + errorMessage: msg + } as ICachedBlame; + + this._onDidBlameFail.fire(entry.key); + this._gitCache.set(entry.key, entry); + return await GitService.EmptyPromise as IGitBlame; + } + + return undefined; + } + } + + async getBlameForLine(uri: GitUri, line: number): Promise { + Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, ${uri.sha})`); + + if (this.UseCaching && !uri.sha) { + const blame = await this.getBlameForFile(uri); + if (blame === undefined) return undefined; + + const blameLine = blame.lines[line]; + if (blameLine === undefined) return undefined; + + const commit = blame.commits.get(blameLine.sha); + if (commit === undefined) return undefined; + + return { + author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }), + commit: commit, + line: blameLine + } as IGitBlameLine; + } + + const fileName = uri.fsPath; + + try { + const data = await Git.blame(uri.repoPath, fileName, uri.sha, line + 1, line + 1); + const blame = GitBlameParser.parse(data, uri.repoPath, fileName); + if (!blame) return undefined; + + const commit = Iterables.first(blame.commits.values()); + if (uri.repoPath) { + commit.repoPath = uri.repoPath; + } + return { + author: Iterables.first(blame.authors.values()), + commit: commit, + line: blame.lines[line] + } as IGitBlameLine; + } + catch (ex) { + return undefined; + } + } + + async getBlameForRange(uri: GitUri, range: Range): Promise { + Logger.log(`getBlameForRange('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); + + const blame = await this.getBlameForFile(uri); + if (!blame) return undefined; + + return this.getBlameForRangeSync(blame, uri, range); + } + + getBlameForRangeSync(blame: IGitBlame, uri: GitUri, range: Range): IGitBlameLines | undefined { + Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); + + if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame); + + if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { + return Object.assign({ allLines: blame.lines }, blame); + } + + const lines = blame.lines.slice(range.start.line, range.end.line + 1); + const shas: Set = new Set(); + lines.forEach(l => shas.add(l.sha)); + + const authors: Map = new Map(); + const commits: Map = new Map(); + blame.commits.forEach(c => { + if (!shas.has(c.sha)) return; + + const commit: GitCommit = new GitCommit('blame', c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, + c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName); + commits.set(c.sha, commit); + + let author = authors.get(commit.author); + if (author === undefined) { + author = { + name: commit.author, + lineCount: 0 + }; + authors.set(author.name, author); + } + + author.lineCount += commit.lines.length; + }); + + const sortedAuthors: Map = new Map(); + Array.from(authors.values()) + .sort((a, b) => b.lineCount - a.lineCount) + .forEach(a => sortedAuthors.set(a.name, a)); + + return { + authors: sortedAuthors, + commits: commits, + lines: lines, + allLines: blame.lines + } as IGitBlameLines; + } + + async getBlameLocations(uri: GitUri, range: Range, selectedSha?: string, line?: number): Promise { + Logger.log(`getBlameLocations('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); + + const blame = await this.getBlameForRange(uri, range); + if (!blame) return undefined; + + const commitCount = blame.commits.size; + + const locations: Array = []; + Iterables.forEach(blame.commits.values(), (c, i) => { + if (c.isUncommitted) return; + + const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; + const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration); + locations.push(new Location(uri, new Position(0, 0))); + if (c.sha === selectedSha) { + locations.push(new Location(uri, new Position((line || 0) + 1, 0))); + } + }); + + return locations; + } + + async getBranch(repoPath: string): Promise { + Logger.log(`getBranch('${repoPath}')`); + + const data = await Git.branch(repoPath, false); + const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_)); + return branches.find(_ => _.current); + } + + async getBranches(repoPath: string): Promise { + Logger.log(`getBranches('${repoPath}')`); + + const data = await Git.branch(repoPath, true); + const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_)); + return branches; + } + + getCacheEntryKey(fileName: string) { + return Git.normalizePath(fileName).toLowerCase(); + } + + async getConfig(key: string, repoPath?: string): Promise { + Logger.log(`getConfig('${key}', '${repoPath}')`); + + return await Git.config_get(key, repoPath); + } + + getGitUriForFile(fileName: string) { + const cacheKey = this.getCacheEntryKey(fileName); + const entry = this._uriCache.get(cacheKey); + return entry && entry.uri; + } + + async getLogCommit(repoPath: string | undefined, fileName: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise; + async getLogCommit(repoPath: string | undefined, fileName: string, sha: string | undefined, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise; + async getLogCommit(repoPath: string | undefined, fileName: string, shaOrOptions?: string | undefined | { firstIfMissing?: boolean, previous?: boolean }, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise { + let sha: string | undefined = undefined; + if (typeof shaOrOptions === 'string') { + sha = shaOrOptions; + } + else if (!options) { + options = shaOrOptions; + } + + options = options || {}; + + const log = await this.getLogForFile(repoPath, fileName, sha, options.previous ? 2 : 1); + if (!log) return undefined; + + const commit = sha && log.commits.get(sha); + if (!commit && sha && !options.firstIfMissing) return undefined; + + return commit || Iterables.first(log.commits.values()); + } + + async getLogForRepo(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false): Promise { + Logger.log(`getLogForRepo('${repoPath}', ${sha}, ${maxCount})`); + + if (maxCount == null) { + maxCount = this.config.advanced.maxQuickHistory || 0; + } + + try { + const data = await Git.log(repoPath, sha, maxCount, reverse); + return GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined); + } + catch (ex) { + return undefined; + } + } + + async getLogForRepoSearch(repoPath: string, search: string, searchBy: GitRepoSearchBy, maxCount?: number): Promise { + Logger.log(`getLogForRepoSearch('${repoPath}', ${search}, ${searchBy}, ${maxCount})`); + + if (maxCount == null) { + maxCount = this.config.advanced.maxQuickHistory || 0; + } + + let searchArgs: string[] | undefined = undefined; + switch (searchBy) { + case GitRepoSearchBy.Author: + searchArgs = [`--author=${search}`]; + break; + case GitRepoSearchBy.Files: + searchArgs = [`--`, `${search}`]; + break; + case GitRepoSearchBy.Message: + searchArgs = [`--grep=${search}`]; + break; + case GitRepoSearchBy.Sha: + searchArgs = [search]; + maxCount = 1; + break; + } + + try { + const data = await Git.log_search(repoPath, searchArgs, maxCount); + return GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined); + } + catch (ex) { + return undefined; + } + } + + getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise { + Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, ${range && `[${range.start.line}, ${range.end.line}]`}, ${reverse})`); + + let entry: GitCacheEntry | undefined; + if (this.UseCaching && !sha && !range && !maxCount && !reverse) { + const cacheKey = this.getCacheEntryKey(fileName); + entry = this._gitCache.get(cacheKey); + + if (entry !== undefined && entry.log !== undefined) return entry.log.item; + if (entry === undefined) { + entry = new GitCacheEntry(cacheKey); + } + } + + const promise = this._getLogForFile(repoPath, fileName, sha, range, maxCount, reverse, entry); + + if (entry) { + Logger.log(`Add log cache for '${entry.key}'`); + + entry.log = { + //date: new Date(), + item: promise + } as ICachedLog; + + this._gitCache.set(entry.key, entry); + } + + return promise; + } + + private async _getLogForFile(repoPath: string | undefined, fileName: string, sha: string | undefined, range: Range | undefined, maxCount: number | undefined, reverse: boolean, entry: GitCacheEntry | undefined): Promise { + const [file, root] = Git.splitPath(fileName, repoPath, false); + + const ignore = await this._gitignore; + if (ignore && !ignore.filter([file]).length) { + Logger.log(`Skipping log; '${fileName}' is gitignored`); + return await GitService.EmptyPromise as IGitLog; + } + + try { + const data = await Git.log_file(root, file, sha, maxCount, reverse, range && range.start.line + 1, range && range.end.line + 1); + return GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range); + } + catch (ex) { + // Trap and cache expected log errors + if (entry) { + const msg = ex && ex.toString(); + Logger.log(`Replace log cache with empty promise for '${entry.key}'`); + + entry.log = { + //date: new Date(), + item: GitService.EmptyPromise, + errorMessage: msg + } as ICachedLog; + + this._gitCache.set(entry.key, entry); + return await GitService.EmptyPromise as IGitLog; + } + + return undefined; + } + } + + async getLogLocations(uri: GitUri, selectedSha?: string, line?: number): Promise { + Logger.log(`getLogLocations('${uri.repoPath}', '${uri.fsPath}', ${uri.sha}, ${selectedSha}, ${line})`); + + const log = await this.getLogForFile(uri.repoPath, uri.fsPath, uri.sha); + if (!log) return undefined; + + const commitCount = log.commits.size; + + const locations: Array = []; + Iterables.forEach(log.commits.values(), (c, i) => { + if (c.isUncommitted) return; + + const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; + const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration); + locations.push(new Location(uri, new Position(0, 0))); + if (c.sha === selectedSha) { + locations.push(new Location(uri, new Position((line || 0) + 1, 0))); + } + }); + + return locations; + } + + async getRemotes(repoPath: string): Promise { + if (!this.config.insiders) return Promise.resolve([]); + if (!repoPath) return Promise.resolve([]); + + Logger.log(`getRemotes('${repoPath}')`); + + if (this.UseCaching) { + const remotes = this._remotesCache.get(repoPath); + if (remotes !== undefined) return remotes; + } + + const data = await Git.remote(repoPath); + const remotes = data.split('\n').filter(_ => !!_).map(_ => new GitRemote(_)); + if (this.UseCaching) { + this._remotesCache.set(repoPath, remotes); + } + return remotes; + } + + getRepoPath(cwd: string): Promise { return GitService.getRepoPath(cwd); - } - - async getRepoPathFromFile(fileName: string): Promise { - const log = await this.getLogForFile(undefined, fileName, undefined, 1); - return log && log.repoPath; - } - - async getRepoPathFromUri(uri: Uri | undefined): Promise { - if (!(uri instanceof Uri)) return this.repoPath; - - return (await GitUri.fromUri(uri, this)).repoPath || this.repoPath; - } - - async getStashList(repoPath: string): Promise { - Logger.log(`getStash('${repoPath}')`); - - const data = await Git.stash_list(repoPath); - return GitStashParser.parse(data, repoPath); - } - - async getStatusForFile(repoPath: string, fileName: string): Promise { - Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`); - - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; - - const data = await Git.status_file(repoPath, fileName, porcelainVersion); - const status = GitStatusParser.parse(data, repoPath, porcelainVersion); - if (status === undefined || !status.files.length) return undefined; - - return status.files[0]; - } - - async getStatusForRepo(repoPath: string): Promise { - Logger.log(`getStatusForRepo('${repoPath}')`); - - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; - - const data = await Git.status(repoPath, porcelainVersion); - return GitStatusParser.parse(data, repoPath, porcelainVersion); - } - - async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string) { - Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${sha})`); - - const file = await Git.getVersionedFile(repoPath, fileName, sha); - const cacheKey = this.getCacheEntryKey(file); - const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath: repoPath!, fileName })); - this._uriCache.set(cacheKey, entry); - return file; - } - - getVersionedFileText(repoPath: string, fileName: string, sha: string) { - Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`); - - return Git.show(repoPath, fileName, sha); - } - - hasGitUriForFile(editor: TextEditor): boolean; - hasGitUriForFile(fileName: string): boolean; - hasGitUriForFile(fileNameOrEditor: string | TextEditor): boolean { - let fileName: string; - if (typeof fileNameOrEditor === 'string') { - fileName = fileNameOrEditor; - } - else { - if (!fileNameOrEditor || !fileNameOrEditor.document || !fileNameOrEditor.document.uri) return false; - fileName = fileNameOrEditor.document.uri.fsPath; - } - - const cacheKey = this.getCacheEntryKey(fileName); - return this._uriCache.has(cacheKey); - } - - isEditorBlameable(editor: TextEditor): boolean { - return (editor.viewColumn !== undefined || - editor.document.uri.scheme === DocumentSchemes.File || - editor.document.uri.scheme === DocumentSchemes.Git || - this.hasGitUriForFile(editor)); - } - - async isFileUncommitted(uri: GitUri): Promise { - Logger.log(`isFileUncommitted('${uri.repoPath}', '${uri.fsPath}')`); - - const status = await this.getStatusForFile(uri.repoPath!, uri.fsPath); - return !!status; - } - - async isTracked(uri: GitUri): Promise { - Logger.log(`isFileUncommitted('${uri.repoPath}', '${uri.fsPath}')`); - - const result = await Git.ls_files(uri.repoPath === undefined ? '' : uri.repoPath, uri.fsPath); - return !!result; - } - - openDirectoryDiff(repoPath: string, sha1: string, sha2?: string) { - Logger.log(`openDirectoryDiff('${repoPath}', ${sha1}, ${sha2})`); - - 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); - } - - stashSave(repoPath: string, message?: string, unstagedOnly: boolean = false) { - Logger.log(`stashSave('${repoPath}', ${message}, ${unstagedOnly})`); - - return Git.stash_save(repoPath, message, unstagedOnly); - } - - toggleCodeLens(editor: TextEditor) { - if (this.config.codeLens.visibility === CodeLensVisibility.Off || - (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return; - - Logger.log(`toggleCodeLens()`); - if (this._codeLensProviderDisposable) { - this._codeLensProviderDisposable.dispose(); - this._codeLensProviderDisposable = undefined; - return; - } - - this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this)); - } - - static getGitPath(gitPath?: string): Promise { - return Git.getGitPath(gitPath); - } - - static getGitVersion(): string { - return Git.gitInfo().version; - } - - static getRepoPath(cwd: string | undefined): Promise { - return Git.getRepoPath(cwd); - } - - static fromGitContentUri(uri: Uri): IGitUriData { - if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); - return GitService._fromGitContentUri(uri); - } - - private static _fromGitContentUri(uri: Uri): T { - return JSON.parse(uri.query) as T; - } - - static isSha(sha: string): boolean { - return Git.isSha(sha); - } - - static isUncommitted(sha: string): boolean { - return Git.isUncommitted(sha); - } - - static normalizePath(fileName: string, repoPath?: string): string { - return Git.normalizePath(fileName, repoPath); - } - - static toGitContentUri(sha: string, shortSha: string, fileName: string, repoPath: string, originalFileName?: string): Uri; - static toGitContentUri(commit: GitCommit): Uri; - static toGitContentUri(shaOrcommit: string | GitCommit, shortSha?: string, fileName?: string, repoPath?: string, originalFileName?: string): Uri { - let data: IGitUriData; - if (typeof shaOrcommit === 'string') { - data = GitService._toGitUriData({ - sha: shaOrcommit, - fileName: fileName!, - repoPath: repoPath!, - originalFileName: originalFileName - }); - } - else { - data = GitService._toGitUriData(shaOrcommit, undefined, shaOrcommit.originalFileName); - fileName = shaOrcommit.fileName; - shortSha = shaOrcommit.shortSha; - } - - const extension = path.extname(fileName!); - return Uri.parse(`${DocumentSchemes.GitLensGit}:${path.basename(fileName!, extension)}:${shortSha}${extension}?${JSON.stringify(data)}`); - } - - static toReferenceGitContentUri(commit: GitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string): Uri { - return GitService._toReferenceGitContentUri(commit, DocumentSchemes.GitLensGit, commitCount, GitService._toGitUriData(commit, index, originalFileName, decoration)); - } - - private static _toReferenceGitContentUri(commit: GitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData) { - const pad = (n: number) => ('0000000' + n).slice(-('' + commitCount).length); - const ext = path.extname(data.fileName); - const uriPath = `${path.relative(commit.repoPath, data.fileName.slice(0, -ext.length))}/${commit.shortSha}${ext}`; - - let message = commit.message; - if (message.length > 50) { - message = message.substring(0, 49) + '\u2026'; - } - - // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location - return Uri.parse(`${scheme}:${pad(data.index || 0)} \u2022 ${encodeURIComponent(message)} \u2022 ${moment(commit.date).format('MMM D, YYYY hh:MMa')} \u2022 ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`); - } - - private static _toGitUriData(commit: IGitUriData, index?: number, originalFileName?: string, decoration?: string): T { - const fileName = Git.normalizePath(path.resolve(commit.repoPath, commit.fileName)); - const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha, index: index } as T; - if (originalFileName) { - data.originalFileName = Git.normalizePath(path.resolve(commit.repoPath, originalFileName)); - } - if (decoration) { - data.decoration = decoration; - } - return data; - } - - static validateGitVersion(major: number, minor: number): boolean { - const [gitMajor, gitMinor] = this.getGitVersion().split('.'); - return (parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor); - } + } + + async getRepoPathFromFile(fileName: string): Promise { + const log = await this.getLogForFile(undefined, fileName, undefined, 1); + if (log === undefined) return undefined; + + return log.repoPath; + } + + async getRepoPathFromUri(uri: Uri | undefined): Promise { + if (!(uri instanceof Uri)) return this.repoPath; + + const repoPath = (await GitUri.fromUri(uri, this)).repoPath; + if (!repoPath) return this.repoPath; + + return repoPath; + } + + async getStashList(repoPath: string): Promise { + Logger.log(`getStash('${repoPath}')`); + + const data = await Git.stash_list(repoPath); + return GitStashParser.parse(data, repoPath); + } + + async getStatusForFile(repoPath: string, fileName: string): Promise { + Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`); + + const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + + const data = await Git.status_file(repoPath, fileName, porcelainVersion); + const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + if (status === undefined || !status.files.length) return undefined; + + return status.files[0]; + } + + async getStatusForRepo(repoPath: string): Promise { + Logger.log(`getStatusForRepo('${repoPath}')`); + + const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + + const data = await Git.status(repoPath, porcelainVersion); + return GitStatusParser.parse(data, repoPath, porcelainVersion); + } + + async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string) { + Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${sha})`); + + const file = await Git.getVersionedFile(repoPath, fileName, sha); + const cacheKey = this.getCacheEntryKey(file); + const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath: repoPath!, fileName })); + this._uriCache.set(cacheKey, entry); + return file; + } + + getVersionedFileText(repoPath: string, fileName: string, sha: string) { + Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`); + + return Git.show(repoPath, fileName, sha); + } + + hasGitUriForFile(editor: TextEditor): boolean; + hasGitUriForFile(fileName: string): boolean; + hasGitUriForFile(fileNameOrEditor: string | TextEditor): boolean { + let fileName: string; + if (typeof fileNameOrEditor === 'string') { + fileName = fileNameOrEditor; + } + else { + if (!fileNameOrEditor || !fileNameOrEditor.document || !fileNameOrEditor.document.uri) return false; + fileName = fileNameOrEditor.document.uri.fsPath; + } + + const cacheKey = this.getCacheEntryKey(fileName); + return this._uriCache.has(cacheKey); + } + + isEditorBlameable(editor: TextEditor): boolean { + return (editor.viewColumn !== undefined || + editor.document.uri.scheme === DocumentSchemes.File || + editor.document.uri.scheme === DocumentSchemes.Git || + this.hasGitUriForFile(editor)); + } + + async isFileUncommitted(uri: GitUri): Promise { + Logger.log(`isFileUncommitted('${uri.repoPath}', '${uri.fsPath}')`); + + const status = await this.getStatusForFile(uri.repoPath!, uri.fsPath); + return !!status; + } + + async isTracked(uri: GitUri): Promise { + Logger.log(`isFileUncommitted('${uri.repoPath}', '${uri.fsPath}')`); + + const result = await Git.ls_files(uri.repoPath === undefined ? '' : uri.repoPath, uri.fsPath); + return !!result; + } + + openDirectoryDiff(repoPath: string, sha1: string, sha2?: string) { + Logger.log(`openDirectoryDiff('${repoPath}', ${sha1}, ${sha2})`); + + 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); + } + + stashSave(repoPath: string, message?: string, unstagedOnly: boolean = false) { + Logger.log(`stashSave('${repoPath}', ${message}, ${unstagedOnly})`); + + return Git.stash_save(repoPath, message, unstagedOnly); + } + + toggleCodeLens(editor: TextEditor) { + if (this.config.codeLens.visibility === CodeLensVisibility.Off || + (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return; + + Logger.log(`toggleCodeLens()`); + if (this._codeLensProviderDisposable) { + this._codeLensProviderDisposable.dispose(); + this._codeLensProviderDisposable = undefined; + return; + } + + this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this)); + } + + static getGitPath(gitPath?: string): Promise { + return Git.getGitPath(gitPath); + } + + static getGitVersion(): string { + return Git.gitInfo().version; + } + + static async getRepoPath(cwd: string | undefined): Promise { + const repoPath = await Git.getRepoPath(cwd); + if (!repoPath) return ''; + + return repoPath; + } + + static fromGitContentUri(uri: Uri): IGitUriData { + if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); + return GitService._fromGitContentUri(uri); + } + + private static _fromGitContentUri(uri: Uri): T { + return JSON.parse(uri.query) as T; + } + + static isSha(sha: string): boolean { + return Git.isSha(sha); + } + + static isUncommitted(sha: string): boolean { + return Git.isUncommitted(sha); + } + + static normalizePath(fileName: string, repoPath?: string): string { + return Git.normalizePath(fileName, repoPath); + } + + static toGitContentUri(sha: string, shortSha: string, fileName: string, repoPath: string, originalFileName?: string): Uri; + static toGitContentUri(commit: GitCommit): Uri; + static toGitContentUri(shaOrcommit: string | GitCommit, shortSha?: string, fileName?: string, repoPath?: string, originalFileName?: string): Uri { + let data: IGitUriData; + if (typeof shaOrcommit === 'string') { + data = GitService._toGitUriData({ + sha: shaOrcommit, + fileName: fileName!, + repoPath: repoPath!, + originalFileName: originalFileName + }); + } + else { + data = GitService._toGitUriData(shaOrcommit, undefined, shaOrcommit.originalFileName); + fileName = shaOrcommit.fileName; + shortSha = shaOrcommit.shortSha; + } + + const extension = path.extname(fileName!); + return Uri.parse(`${DocumentSchemes.GitLensGit}:${path.basename(fileName!, extension)}:${shortSha}${extension}?${JSON.stringify(data)}`); + } + + static toReferenceGitContentUri(commit: GitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string): Uri { + return GitService._toReferenceGitContentUri(commit, DocumentSchemes.GitLensGit, commitCount, GitService._toGitUriData(commit, index, originalFileName, decoration)); + } + + private static _toReferenceGitContentUri(commit: GitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData) { + const pad = (n: number) => ('0000000' + n).slice(-('' + commitCount).length); + const ext = path.extname(data.fileName); + const uriPath = `${path.relative(commit.repoPath, data.fileName.slice(0, -ext.length))}/${commit.shortSha}${ext}`; + + let message = commit.message; + if (message.length > 50) { + message = message.substring(0, 49) + '\u2026'; + } + + // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location + return Uri.parse(`${scheme}:${pad(data.index || 0)} \u2022 ${encodeURIComponent(message)} \u2022 ${moment(commit.date).format('MMM D, YYYY hh:MMa')} \u2022 ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`); + } + + private static _toGitUriData(commit: IGitUriData, index?: number, originalFileName?: string, decoration?: string): T { + const fileName = Git.normalizePath(path.resolve(commit.repoPath, commit.fileName)); + const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha, index: index } as T; + if (originalFileName) { + data.originalFileName = Git.normalizePath(path.resolve(commit.repoPath, originalFileName)); + } + if (decoration) { + data.decoration = decoration; + } + return data; + } + + static validateGitVersion(major: number, minor: number): boolean { + const [gitMajor, gitMinor] = this.getGitVersion().split('.'); + return (parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor); + } } \ No newline at end of file diff --git a/src/quickPicks/branchHistory.ts b/src/quickPicks/branchHistory.ts index 029efc5..8933acc 100644 --- a/src/quickPicks/branchHistory.ts +++ b/src/quickPicks/branchHistory.ts @@ -1,9 +1,9 @@ 'use strict'; import { Arrays, Iterables } from '../system'; import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; -import { Commands, Keyboard, KeyNoopCommand } from '../commands'; +import { Commands, Keyboard, KeyNoopCommand, ShowCommitSearchCommandArgs, ShowQuickBranchHistoryCommandArgs } from '../commands'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common'; -import { GitService, GitUri, IGitLog } from '../gitService'; +import { GitService, GitUri, IGitLog, RemoteResource } from '../gitService'; import { OpenRemotesCommandQuickPickItem } from './remotes'; export class BranchHistoryQuickPick { @@ -23,17 +23,33 @@ export class BranchHistoryQuickPick { const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to \u00a0$(git-branch) ${branch} history` - }, Commands.ShowQuickBranchHistory, [uri, branch, log.maxCount, goBackCommand, log]); + }, Commands.ShowQuickBranchHistory, [ + uri, + { + branch, + log, + maxCount: log.maxCount, + goBackCommand + } as ShowQuickBranchHistoryCommandArgs + ]); const remotes = Arrays.uniqueBy(await git.getRemotes((uri && uri.repoPath) || git.repoPath), _ => _.url, _ => !!_.provider); if (remotes.length) { - items.splice(0, 0, new OpenRemotesCommandQuickPickItem(remotes, 'branch', branch, currentCommand)); + items.splice(0, 0, new OpenRemotesCommandQuickPickItem(remotes, { + type: 'branch', + branch + } as RemoteResource, currentCommand)); } items.splice(0, 0, new CommandQuickPickItem({ label: `$(search) Show Commit Search`, description: `\u00a0 \u2014 \u00a0\u00a0 search for commits by message, author, files, or commit id` - }, Commands.ShowCommitSearch, [new GitUri(Uri.file(log.repoPath), { fileName: '', repoPath: log.repoPath }), undefined, undefined, currentCommand])); + }, Commands.ShowCommitSearch, [ + new GitUri(Uri.file(log.repoPath), { fileName: '', repoPath: log.repoPath }), + { + goBackCommand: currentCommand + } as ShowCommitSearchCommandArgs + ])); let previousPageCommand: CommandQuickPickItem | undefined = undefined; @@ -44,9 +60,11 @@ export class BranchHistoryQuickPick { description: `\u00a0 \u2014 \u00a0\u00a0 this may take a while` }, Commands.ShowQuickBranchHistory, [ new GitUri(Uri.file(log.repoPath), { fileName: '', repoPath: log.repoPath }), - branch, - 0, - goBackCommand + { + branch, + maxCount: 0, + goBackCommand + } as ShowQuickBranchHistoryCommandArgs ])); } else { @@ -55,9 +73,10 @@ export class BranchHistoryQuickPick { description: `\u00a0 \u2014 \u00a0\u00a0 shows \u00a0$(git-branch) ${branch} history` }, Commands.ShowQuickBranchHistory, [ new GitUri(Uri.file(log.repoPath), { fileName: '', repoPath: log.repoPath }), - branch, - undefined, - currentCommand + { + branch, + goBackCommand: currentCommand + } as ShowQuickBranchHistoryCommandArgs ])); } @@ -69,14 +88,29 @@ export class BranchHistoryQuickPick { const npc = new CommandQuickPickItem({ label: `$(arrow-right) Show Next Commits`, description: `\u00a0 \u2014 \u00a0\u00a0 shows ${log.maxCount} newer commits` - }, Commands.ShowQuickBranchHistory, [uri, branch, log.maxCount, goBackCommand, undefined, nextPageCommand]); + }, Commands.ShowQuickBranchHistory, [ + uri, + { + branch, + maxCount: log.maxCount, + nextPageCommand + } as ShowQuickBranchHistoryCommandArgs + ]); const last = Iterables.last(log.commits.values()); if (last != null) { previousPageCommand = new CommandQuickPickItem({ label: `$(arrow-left) Show Previous Commits`, description: `\u00a0 \u2014 \u00a0\u00a0 shows ${log.maxCount} older commits` - }, Commands.ShowQuickBranchHistory, [new GitUri(uri ? uri : last.uri, last), branch, log.maxCount, goBackCommand, undefined, npc]); + }, Commands.ShowQuickBranchHistory, [ + new GitUri(uri ? uri : last.uri, last), + { + branch, + maxCount: log.maxCount, + goBackCommand, + nextPageCommand: npc + } as ShowQuickBranchHistoryCommandArgs + ]); items.splice(0, 0, previousPageCommand); } diff --git a/src/quickPicks/commitDetails.ts b/src/quickPicks/commitDetails.ts index 220ab56..2a491b5 100644 --- a/src/quickPicks/commitDetails.ts +++ b/src/quickPicks/commitDetails.ts @@ -1,9 +1,9 @@ 'use strict'; import { Arrays, Iterables } from '../system'; import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; -import { Commands, Keyboard, KeyNoopCommand } from '../commands'; +import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandCommandArgs, Keyboard, KeyNoopCommand, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem } from './common'; -import { getGitStatusIcon, GitCommit, GitLogCommit, GitService, GitStashCommit, GitStatusFileStatus, GitUri, IGitLog, IGitStatusFile } from '../gitService'; +import { getGitStatusIcon, GitCommit, GitLogCommit, GitService, GitStashCommit, GitStatusFileStatus, GitUri, IGitLog, IGitStatusFile, RemoteResource } from '../gitService'; import { OpenRemotesCommandQuickPickItem } from './remotes'; import * as moment from 'moment'; import * as path from 'path'; @@ -93,47 +93,93 @@ export class CommitDetailsQuickPick { items.splice(index++, 0, new CommandQuickPickItem({ label: `$(git-pull-request) Apply Stashed Changes`, description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}` - }, Commands.StashApply, [commit as GitStashCommit, true, false, currentCommand])); + }, Commands.StashApply, [ + { + confirm: true, + deleteAfter: false, + stashItem: commit as GitStashCommit, + goBackCommand: currentCommand + } as StashApplyCommandArgs + ])); items.splice(index++, 0, new CommandQuickPickItem({ label: `$(x) Delete Stashed Changes`, description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}` - }, Commands.StashDelete, [commit as GitStashCommit, true, currentCommand])); + }, Commands.StashDelete, [ + { + confirm: true, + stashItem: commit as GitStashCommit, + goBackCommand: currentCommand + } as StashDeleteCommandArgs + ])); } if (!stash) { items.splice(index++, 0, new CommandQuickPickItem({ label: `$(clippy) Copy Commit ID to Clipboard`, description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.shortSha}` - }, Commands.CopyShaToClipboard, [uri, commit.sha])); + }, Commands.CopyShaToClipboard, [ + uri, + { + sha: commit.sha + } as CopyShaToClipboardCommandArgs + ])); } items.splice(index++, 0, new CommandQuickPickItem({ label: `$(clippy) Copy Message to Clipboard`, description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}` - }, Commands.CopyMessageToClipboard, [uri, commit.sha, commit.message])); + }, Commands.CopyMessageToClipboard, [ + uri, + { + message: commit.message, + sha: commit.sha + } as CopyMessageToClipboardCommandArgs + ])); if (!stash) { const remotes = Arrays.uniqueBy(await git.getRemotes(commit.repoPath), _ => _.url, _ => !!_.provider); if (remotes.length) { - items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, 'commit', commit.sha, currentCommand)); + items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, { + type: 'commit', + sha: commit.sha + } as RemoteResource, 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])); + }, Commands.DiffDirectory, [ + commit.uri, + { + shaOrBranch1: commit.previousSha || `${commit.sha}^`, + shaOrBranch2: commit.sha + } as DiffDirectoryCommandCommandArgs + ])); } items.splice(index++, 0, new CommandQuickPickItem({ label: `$(git-compare) Directory Compare with Working Tree`, description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha} \u00a0 $(git-compare) \u00a0 $(file-directory) Working Tree` - }, Commands.DiffDirectory, [uri, commit.sha])); + }, Commands.DiffDirectory, [ + uri, + { + shaOrBranch1: commit.sha + } as DiffDirectoryCommandCommandArgs + ])); items.splice(index++, 0, new CommandQuickPickItem({ label: `Changed Files`, description: commit.getDiffStatus() - }, Commands.ShowQuickCommitDetails, [uri, commit.sha, commit, goBackCommand, repoLog])); + }, Commands.ShowQuickCommitDetails, [ + uri, + { + commit, + repoLog, + sha: commit.sha, + goBackCommand + } as ShowQuickCommitDetailsCommandArgs + ])); items.push(new OpenCommitFilesCommandQuickPickItem(commit)); items.push(new OpenCommitWorkingTreeFilesCommandQuickPickItem(commit)); @@ -146,9 +192,28 @@ export class CommitDetailsQuickPick { let nextCommand: CommandQuickPickItem | (() => Promise) | undefined = undefined; if (!stash) { // If we have the full history, we are good - if (repoLog && !repoLog.truncated && !repoLog.sha) { - previousCommand = commit.previousSha === undefined ? undefined : new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [commit.previousUri, commit.previousSha, undefined, goBackCommand, repoLog]); - nextCommand = commit.nextSha === undefined ? undefined : new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [commit.nextUri, commit.nextSha, undefined, goBackCommand, repoLog]); + if (repoLog !== undefined && !repoLog.truncated && repoLog.sha === undefined) { + previousCommand = commit.previousSha === undefined + ? undefined + : new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ + commit.previousUri, + { + repoLog, + sha: commit.previousSha, + goBackCommand + } as ShowQuickCommitDetailsCommandArgs + ]); + + nextCommand = commit.nextSha === undefined + ? undefined + : new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ + commit.nextUri, + { + repoLog, + sha: commit.nextSha, + goBackCommand + } as ShowQuickCommitDetailsCommandArgs + ]); } else { previousCommand = async () => { @@ -156,7 +221,7 @@ export class CommitDetailsQuickPick { 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) { + if (c === undefined || c.previousSha === undefined) { log = await git.getLogForRepo(commit.repoPath, commit.sha, git.config.advanced.maxQuickHistory); c = log && log.commits.get(commit.sha); @@ -165,8 +230,17 @@ export class CommitDetailsQuickPick { c.nextSha = commit.nextSha; } } - if (!c || !c.previousSha) return KeyNoopCommand; - return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [c.previousUri, c.previousSha, undefined, goBackCommand, log]); + + if (c === undefined || c.previousSha === undefined) return KeyNoopCommand; + + return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ + c.previousUri, + { + repoLog: log, + sha: c.previousSha, + goBackCommand + } as ShowQuickCommitDetailsCommandArgs + ]); }; nextCommand = async () => { @@ -174,20 +248,29 @@ export class CommitDetailsQuickPick { 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) { + if (c === undefined || c.nextSha === undefined) { 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) { + if (next !== undefined && 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 === undefined || c.nextSha === undefined) return KeyNoopCommand; + + return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ + c.nextUri, + { + repoLog: log, + sha: c.nextSha, + goBackCommand + } as ShowQuickCommitDetailsCommandArgs + ]); }; } } diff --git a/src/quickPicks/commitFileDetails.ts b/src/quickPicks/commitFileDetails.ts index 4013a70..d712b62 100644 --- a/src/quickPicks/commitFileDetails.ts +++ b/src/quickPicks/commitFileDetails.ts @@ -1,9 +1,9 @@ 'use strict'; import { Arrays, Iterables } from '../system'; import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; -import { Commands, Keyboard, KeyNoopCommand } from '../commands'; +import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, Keyboard, KeyNoopCommand, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem } from './common'; -import { GitBranch, GitLogCommit, GitService, GitUri, IGitLog } from '../gitService'; +import { GitBranch, GitLogCommit, GitService, GitUri, IGitLog, RemoteResource } from '../gitService'; import { OpenRemotesCommandQuickPickItem } from './remotes'; import * as moment from 'moment'; import * as path from 'path'; @@ -61,13 +61,25 @@ export class CommitFileDetailsQuickPick { 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])); + }, Commands.ShowQuickCommitDetails, [ + new GitUri(commit.uri, commit), + { + commit, + sha: commit.sha, + goBackCommand: currentCommand + } as ShowQuickCommitDetailsCommandArgs + ])); 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])); + }, Commands.DiffWithPrevious, [ + commit.uri, + { + commit + } as DiffWithPreviousCommandArgs + ])); } } @@ -75,19 +87,35 @@ export class CommitFileDetailsQuickPick { items.push(new CommandQuickPickItem({ label: `$(git-compare) Compare with Working Tree`, description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha} \u00a0 $(git-compare) \u00a0 $(file-text) ${workingName}` - }, Commands.DiffWithWorking, [Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), commit])); + }, Commands.DiffWithWorking, [ + Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), + { + commit + } as DiffWithWorkingCommandArgs + ])); } if (!stash) { items.push(new CommandQuickPickItem({ label: `$(clippy) Copy Commit ID to Clipboard`, description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.shortSha}` - }, Commands.CopyShaToClipboard, [uri, commit.sha])); + }, Commands.CopyShaToClipboard, [ + uri, + { + sha: commit.sha + } as CopyShaToClipboardCommandArgs + ])); items.push(new CommandQuickPickItem({ label: `$(clippy) Copy Message to Clipboard`, description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}` - }, Commands.CopyMessageToClipboard, [uri, commit.sha, commit.message])); + }, Commands.CopyMessageToClipboard, [ + uri, + { + message: commit.message, + sha: commit.sha + } as CopyMessageToClipboardCommandArgs + ])); } items.push(new OpenCommitFileCommandQuickPickItem(commit)); @@ -98,11 +126,19 @@ export class CommitFileDetailsQuickPick { const remotes = Arrays.uniqueBy(await git.getRemotes(commit.repoPath), _ => _.url, _ => !!_.provider); if (remotes.length) { if (!stash) { - items.push(new OpenRemotesCommandQuickPickItem(remotes, 'file', commit.fileName, undefined, commit, currentCommand)); + items.push(new OpenRemotesCommandQuickPickItem(remotes, { + type: 'file', + fileName: commit.fileName, + commit + } as RemoteResource, currentCommand)); } if (commit.workingFileName && commit.status !== 'D') { const branch = await git.getBranch(commit.repoPath || git.repoPath) as GitBranch; - items.push(new OpenRemotesCommandQuickPickItem(remotes, 'working-file', commit.workingFileName, branch.name, undefined, currentCommand)); + items.push(new OpenRemotesCommandQuickPickItem(remotes, { + type: 'working-file', + fileName: commit.workingFileName, + branch: branch.name + } as RemoteResource, currentCommand)); } } @@ -110,14 +146,25 @@ export class CommitFileDetailsQuickPick { items.push(new CommandQuickPickItem({ label: `$(history) Show File History`, description: `\u00a0 \u2014 \u00a0\u00a0 of ${path.basename(commit.fileName)}` - }, Commands.ShowQuickFileHistory, [Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), undefined, undefined, currentCommand, fileLog])); + }, Commands.ShowQuickFileHistory, [ + Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), + { + fileLog, + goBackCommand: currentCommand + } as ShowQuickFileHistoryCommandArgs + ])); } if (!stash) { items.push(new CommandQuickPickItem({ label: `$(history) Show ${commit.workingFileName ? 'Previous ' : ''}File History`, description: `\u00a0 \u2014 \u00a0\u00a0 of ${path.basename(commit.fileName)} \u00a0\u2022\u00a0 from \u00a0$(git-commit) ${commit.shortSha}` - }, Commands.ShowQuickFileHistory, [new GitUri(commit.uri, commit), undefined, undefined, currentCommand])); + }, Commands.ShowQuickFileHistory, [ + new GitUri(commit.uri, commit), + { + goBackCommand: currentCommand + } as ShowQuickFileHistoryCommandArgs + ])); } if (goBackCommand) { @@ -128,9 +175,28 @@ export class CommitFileDetailsQuickPick { let nextCommand: CommandQuickPickItem | (() => Promise) | undefined = undefined; if (!stash) { // If we have the full history, we are good - if (fileLog && !fileLog.truncated && !fileLog.sha) { - previousCommand = commit.previousSha === undefined ? undefined : new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [commit.previousUri, commit.previousSha, undefined, goBackCommand, fileLog]); - nextCommand = commit.nextSha === undefined ? undefined : new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [commit.nextUri, commit.nextSha, undefined, goBackCommand, fileLog]); + if (fileLog !== undefined && !fileLog.truncated && fileLog.sha === undefined) { + previousCommand = commit.previousSha === undefined + ? undefined + : new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [ + commit.previousUri, + { + fileLog, + sha: commit.previousSha, + goBackCommand + } as ShowQuickCommitFileDetailsCommandArgs + ]); + + nextCommand = commit.nextSha === undefined + ? undefined + : new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [ + commit.nextUri, + { + fileLog, + sha: commit.nextSha, + goBackCommand + } as ShowQuickCommitFileDetailsCommandArgs + ]); } else { previousCommand = async () => { @@ -138,13 +204,13 @@ export class CommitFileDetailsQuickPick { 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) { + if (c === undefined || c.previousSha === undefined) { log = await git.getLogForFile(commit.repoPath, uri.fsPath, commit.sha, git.config.advanced.maxQuickHistory); if (log === undefined) return KeyNoopCommand; 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) { + if (c === undefined && commit.isMerge) { c = Iterables.first(log.commits.values()); } @@ -154,8 +220,17 @@ export class CommitFileDetailsQuickPick { c.nextFileName = commit.nextFileName; } } - if (!c || !c.previousSha) return KeyNoopCommand; - return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [c.previousUri, c.previousSha, undefined, goBackCommand, log]); + + if (c === undefined || c.previousSha === undefined) return KeyNoopCommand; + + return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [ + c.previousUri, + { + fileLog: log, + sha: c.previousSha, + goBackCommand + } as ShowQuickCommitFileDetailsCommandArgs + ]); }; nextCommand = async () => { @@ -163,20 +238,29 @@ export class CommitFileDetailsQuickPick { 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) { + if (c === undefined || c.nextSha === undefined) { 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) { + if (next !== undefined && 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 === undefined || c.nextSha === undefined) return KeyNoopCommand; + + return new KeyCommandQuickPickItem(Commands.ShowQuickCommitFileDetails, [ + c.nextUri, + { + fileLog: log, + sha: c.nextSha, + goBackCommand + } as ShowQuickCommitFileDetailsCommandArgs + ]); }; } } diff --git a/src/quickPicks/common.ts b/src/quickPicks/common.ts index ae0c444..03ad1db 100644 --- a/src/quickPicks/common.ts +++ b/src/quickPicks/common.ts @@ -1,6 +1,6 @@ 'use strict'; -import { CancellationTokenSource, commands, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; -import { Commands, Keyboard, KeyboardScope, KeyMapping, openEditor } from '../commands'; +import { CancellationTokenSource, commands, Disposable, QuickPickItem, QuickPickOptions, TextDocumentShowOptions, TextEditor, Uri, window, workspace } from 'vscode'; +import { Commands, Keyboard, Keys, KeyboardScope, KeyMapping, openEditor } from '../commands'; import { IAdvancedConfig } from '../configuration'; import { ExtensionKey } from '../constants'; import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService'; @@ -61,20 +61,45 @@ function _getInfiniteCancellablePromise(cancellation: CancellationTokenSource) { }); } +export interface QuickPickItem extends QuickPickItem { + onDidSelect?(): void; + onDidPressKey?(key: Keys): Promise<{} | undefined>; +} + export class CommandQuickPickItem implements QuickPickItem { label: string; description: string; detail?: string | undefined; + protected command: Commands | undefined; + protected args: any[] | undefined; - constructor(item: QuickPickItem, protected command: Commands | undefined, protected args?: any[]) { + constructor(item: QuickPickItem, args?: [Commands, any[]]); + constructor(item: QuickPickItem, command?: Commands, args?: any[]); + constructor(item: QuickPickItem, commandOrArgs?: Commands | [Commands, any[]], args?: any[]) { + if (commandOrArgs === undefined) { + this.command = undefined; + this.args = args; + } + else if (typeof commandOrArgs === 'string') { + this.command = commandOrArgs; + this.args = args; + } + else { + this.command = commandOrArgs[0]; + this.args = commandOrArgs.slice(1); + } Object.assign(this, item); } - execute(): Thenable<{} | undefined> { + execute(): Promise<{} | undefined> { if (this.command === undefined) return Promise.resolve(undefined); - return commands.executeCommand(this.command, ...(this.args || [])); + return commands.executeCommand(this.command, ...(this.args || [])) as Promise<{} | undefined>; + } + + onDidPressKey(key: Keys): Promise<{} | undefined> { + return this.execute(); } } @@ -91,12 +116,22 @@ export class OpenFileCommandQuickPickItem extends CommandQuickPickItem { super(item, undefined, undefined); } - async execute(pinned: boolean = false): Promise<{} | undefined> { - return this.open(pinned); + async execute(options?: TextDocumentShowOptions): Promise { + return openEditor(this.uri, options); } - async open(pinned: boolean = false): Promise<{} | undefined> { - return openEditor(this.uri, pinned); + onDidSelect(): Promise<{} | undefined> { + return this.execute({ + preserveFocus: true, + preview: true + }); + } + + onDidPressKey(key: Keys): Promise<{} | undefined> { + return this.execute({ + preserveFocus: true, + preview: false + }); } } @@ -106,12 +141,19 @@ export class OpenFilesCommandQuickPickItem extends CommandQuickPickItem { super(item, undefined, undefined); } - async execute(): Promise<{} | undefined> { + async execute(options: TextDocumentShowOptions = { preserveFocus: false, preview: false }): Promise<{} | undefined> { for (const uri of this.uris) { - await openEditor(uri, true); + await openEditor(uri, options); } return undefined; } + + async onDidPressKey(key: Keys): Promise<{} | undefined> { + return this.execute({ + preserveFocus: true, + preview: false + }); + } } export class CommitQuickPickItem implements QuickPickItem { diff --git a/src/quickPicks/fileHistory.ts b/src/quickPicks/fileHistory.ts index bc01df1..49e68e5 100644 --- a/src/quickPicks/fileHistory.ts +++ b/src/quickPicks/fileHistory.ts @@ -1,9 +1,9 @@ 'use strict'; import { Arrays, Iterables } from '../system'; import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; -import { Commands, Keyboard, KeyNoopCommand } from '../commands'; +import { Commands, Keyboard, KeyNoopCommand, ShowQuickCurrentBranchHistoryCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common'; -import { GitService, GitUri, IGitLog } from '../gitService'; +import { GitService, GitUri, IGitLog, RemoteResource } from '../gitService'; import { OpenRemotesCommandQuickPickItem } from './remotes'; import * as path from 'path'; @@ -30,7 +30,13 @@ export class FileHistoryQuickPick { items.splice(0, 0, new CommandQuickPickItem({ label: `$(sync) Show All Commits`, description: `\u00a0 \u2014 \u00a0\u00a0 this may take a while` - }, Commands.ShowQuickFileHistory, [Uri.file(uri.fsPath), undefined, 0, goBackCommand])); + }, Commands.ShowQuickFileHistory, [ + Uri.file(uri.fsPath), + { + maxCount: 0, + goBackCommand + } as ShowQuickFileHistoryCommandArgs + ])); } else { const workingFileName = await git.findWorkingFileName(log.repoPath, path.relative(log.repoPath, uri.fsPath)); @@ -40,14 +46,22 @@ export class FileHistoryQuickPick { label: `$(history) Show File History`, description: `\u00a0 \u2014 \u00a0\u00a0 of ${path.basename(workingFileName)}` }, Commands.ShowQuickFileHistory, [ - Uri.file(path.resolve(log.repoPath, workingFileName)), - undefined, - undefined, - new CommandQuickPickItem({ - label: `go back \u21A9`, - description: `\u00a0 \u2014 \u00a0\u00a0 to history of \u00a0$(file-text) ${path.basename(uri.fsPath)}${uri.sha ? ` from \u00a0$(git-commit) ${uri.shortSha}` : ''}` - }, Commands.ShowQuickFileHistory, [uri, log.range, log.maxCount, goBackCommand, log]) - ])); + Uri.file(path.resolve(log.repoPath, workingFileName)), + { + goBackCommand: new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to history of \u00a0$(file-text) ${path.basename(uri.fsPath)}${uri.sha ? ` from \u00a0$(git-commit) ${uri.shortSha}` : ''}` + }, Commands.ShowQuickFileHistory, [ + uri, + { + log: log, + maxCount: log.maxCount, + range: log.range, + goBackCommand + } as ShowQuickFileHistoryCommandArgs + ]) + } as ShowQuickFileHistoryCommandArgs + ])); } } @@ -60,14 +74,28 @@ export class FileHistoryQuickPick { const npc = new CommandQuickPickItem({ label: `$(arrow-right) Show Next Commits`, description: `\u00a0 \u2014 \u00a0\u00a0 shows ${log.maxCount} newer commits` - }, Commands.ShowQuickFileHistory, [uri, undefined, log.maxCount, goBackCommand, undefined, nextPageCommand]); + }, Commands.ShowQuickFileHistory, [ + uri, + { + maxCount: log.maxCount, + goBackCommand, + nextPageCommand + } as ShowQuickFileHistoryCommandArgs + ]); const last = Iterables.last(log.commits.values()); if (last != null) { previousPageCommand = new CommandQuickPickItem({ label: `$(arrow-left) Show Previous Commits`, description: `\u00a0 \u2014 \u00a0\u00a0 shows ${log.maxCount} older commits` - }, Commands.ShowQuickFileHistory, [new GitUri(uri, last), undefined, log.maxCount, goBackCommand, undefined, npc]); + }, Commands.ShowQuickFileHistory, [ + new GitUri(uri, last), + { + maxCount: log.maxCount, + goBackCommand, + nextPageCommand: npc + } as ShowQuickFileHistoryCommandArgs + ]); index++; items.splice(0, 0, previousPageCommand); @@ -80,23 +108,37 @@ export class FileHistoryQuickPick { const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to history of \u00a0$(file-text) ${path.basename(uri.fsPath)}${uri.sha ? ` from \u00a0$(git-commit) ${uri.shortSha}` : ''}` - }, Commands.ShowQuickFileHistory, [uri, log.range, log.maxCount, undefined, log]); + }, Commands.ShowQuickFileHistory, [ + uri, + { + log, + maxCount: log.maxCount, + range: log.range + } as ShowQuickFileHistoryCommandArgs + ]); // Only show the full repo option if we are the root - if (!goBackCommand) { + if (goBackCommand === undefined) { items.splice(index++, 0, new CommandQuickPickItem({ label: `$(history) Show Branch History`, description: `\u00a0 \u2014 \u00a0\u00a0 shows \u00a0$(git-branch) ${branch!.name} history` }, Commands.ShowQuickCurrentBranchHistory, [ undefined, - currentCommand + { + goBackCommand: currentCommand + } as ShowQuickCurrentBranchHistoryCommandArgs ])); } const remotes = Arrays.uniqueBy(await git.getRemotes(uri.repoPath!), _ => _.url, _ => !!_.provider); if (remotes.length) { - items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, 'file', uri.getRelativePath(), branch!.name, uri.sha, currentCommand)); + items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, { + type: 'file', + branch: branch!.name, + fileName: uri.getRelativePath(), + sha: uri.sha + } as RemoteResource, currentCommand)); } if (goBackCommand) { diff --git a/src/quickPicks/remotes.ts b/src/quickPicks/remotes.ts index 22f6911..7b91853 100644 --- a/src/quickPicks/remotes.ts +++ b/src/quickPicks/remotes.ts @@ -1,88 +1,66 @@ 'use strict'; import { QuickPickOptions, window } from 'vscode'; -import { Commands } from '../commands'; +import { Commands, OpenInRemoteCommandArgs } from '../commands'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './common'; -import { getNameFromRemoteOpenType, GitLogCommit, GitRemote, RemoteOpenType } from '../gitService'; +import { getNameFromRemoteResource, GitLogCommit, GitRemote, RemoteResource } from '../gitService'; import * as path from 'path'; export class OpenRemoteCommandQuickPickItem extends CommandQuickPickItem { private remote: GitRemote; - private type: RemoteOpenType; + private resource: RemoteResource; - constructor(remote: GitRemote, type: RemoteOpenType, ...args: string[]) { + constructor(remote: GitRemote, resource: RemoteResource) { super({ - label: `$(link-external) Open ${getNameFromRemoteOpenType(type)} in ${remote.provider!.name}`, + label: `$(link-external) Open ${getNameFromRemoteResource(resource)} in ${remote.provider!.name}`, description: `\u00a0 \u2014 \u00a0\u00a0 $(repo) ${remote.provider!.path}` }, undefined, undefined); this.remote = remote; - this.type = type; - this.args = args; + this.resource = resource; } async execute(): Promise<{}> { - return this.remote.provider!.open(this.type, ...this.args!); + return this.remote.provider!.open(this.resource); } } export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { - constructor(remotes: GitRemote[], type: 'branch', branch: string, goBackCommand?: CommandQuickPickItem); - constructor(remotes: GitRemote[], type: 'commit', sha: string, goBackCommand?: CommandQuickPickItem); - constructor(remotes: GitRemote[], type: 'file', fileName: string, branch?: string, commit?: GitLogCommit, goBackCommand?: CommandQuickPickItem); - constructor(remotes: GitRemote[], type: 'file' | 'working-file', fileName: string, branch?: string, sha?: string, goBackCommand?: CommandQuickPickItem); - constructor(remotes: GitRemote[], type: RemoteOpenType, branchOrShaOrFileName: string, goBackCommandOrFileBranch?: CommandQuickPickItem | string, fileShaOrCommit?: string | GitLogCommit, goBackCommand?: CommandQuickPickItem) { - let fileBranch: string | undefined = undefined; - if (typeof goBackCommandOrFileBranch === 'string') { - fileBranch = goBackCommandOrFileBranch; - } - else if (!goBackCommand) { - goBackCommand = goBackCommandOrFileBranch; - } + constructor(remotes: GitRemote[], resource: RemoteResource, goBackCommand?: CommandQuickPickItem) { + const name = getNameFromRemoteResource(resource); - const name = getNameFromRemoteOpenType(type); - - let fileSha: string | undefined = undefined; - let description: string | undefined = undefined; - let placeHolder: string | undefined = undefined; - switch (type) { + let description: string = ''; + switch (resource.type) { case 'branch': - description = `$(git-branch) ${branchOrShaOrFileName}`; - placeHolder = `open ${branchOrShaOrFileName} ${name.toLowerCase()} in\u2026`; + description = `$(git-branch) ${resource.branch}`; break; + case 'commit': - const shortSha = branchOrShaOrFileName.substring(0, 8); - + const shortSha = resource.sha.substring(0, 8); description = `$(git-commit) ${shortSha}`; - placeHolder = `open ${name.toLowerCase()} ${shortSha} in\u2026`; break; - case 'file': - case 'working-file': - const fileName = path.basename(branchOrShaOrFileName); - if (fileShaOrCommit instanceof GitLogCommit) { - if (fileShaOrCommit.status === 'D') { - fileSha = fileShaOrCommit.previousSha; - description = `$(file-text) ${fileName} in \u00a0$(git-commit) ${fileShaOrCommit.previousShortSha} (deleted in \u00a0$(git-commit) ${fileShaOrCommit.shortSha})`; - placeHolder = `open ${branchOrShaOrFileName} \u00a0\u2022\u00a0 ${fileShaOrCommit.previousShortSha} in\u2026`; + case 'file': + if (resource.commit !== undefined && resource.commit instanceof GitLogCommit) { + if (resource.commit.status === 'D') { + resource.sha = resource.commit.previousSha; + description = `$(file-text) ${path.basename(resource.fileName)} in \u00a0$(git-commit) ${resource.commit.previousShortSha} (deleted in \u00a0$(git-commit) ${resource.commit.shortSha})`; } else { - fileSha = fileShaOrCommit.sha; - - description = `$(file-text) ${fileName} in \u00a0$(git-commit) ${fileShaOrCommit.shortSha}`; - placeHolder = `open ${branchOrShaOrFileName} \u00a0\u2022\u00a0 ${fileShaOrCommit.shortSha} in\u2026`; + resource.sha = resource.commit.sha; + description = `$(file-text) ${path.basename(resource.fileName)} in \u00a0$(git-commit) ${resource.commit.shortSha}`; } } else { - fileSha = fileShaOrCommit; - const shortFileSha = (fileSha && fileSha.substring(0, 8)) || ''; - const shaSuffix = shortFileSha ? ` \u00a0\u2022\u00a0 ${shortFileSha}` : ''; - - description = `$(file-text) ${fileName}${shortFileSha ? ` in \u00a0$(git-commit) ${shortFileSha}` : ''}`; - placeHolder = `open ${branchOrShaOrFileName}${shaSuffix} in\u2026`; + const shortFileSha = resource.sha === undefined ? '' : resource.sha.substring(0, 8); + description = `$(file-text) ${path.basename(resource.fileName)}${shortFileSha ? ` in \u00a0$(git-commit) ${shortFileSha}` : ''}`; } break; + + case 'working-file': + description = `$(file-text) ${path.basename(resource.fileName)}`; + break; } const remote = remotes[0]; @@ -90,7 +68,14 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { super({ label: `$(link-external) Open ${name} in ${remote.provider!.name}`, description: `\u00a0 \u2014 \u00a0\u00a0 $(repo) ${remote.provider!.path} \u00a0\u2022\u00a0 ${description}` - }, Commands.OpenInRemote, [undefined, remotes, type, [branchOrShaOrFileName, fileBranch, fileSha], goBackCommand]); + }, Commands.OpenInRemote, [ + undefined, + { + remotes, + resource, + goBackCommand + } as OpenInRemoteCommandArgs + ]); return; } @@ -102,15 +87,21 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { super({ label: `$(link-external) Open ${name} in ${provider}\u2026`, description: `\u00a0 \u2014 \u00a0\u00a0 ${description}` - }, Commands.OpenInRemote, [undefined, remotes, type, [branchOrShaOrFileName, fileBranch, fileSha], goBackCommand]); + }, Commands.OpenInRemote, [ + undefined, + { + remotes, + resource, + goBackCommand + } as OpenInRemoteCommandArgs + ]); } } export class RemotesQuickPick { - static async show(remotes: GitRemote[], placeHolder: string, type: RemoteOpenType, args: string[], goBackCommand?: CommandQuickPickItem): Promise { - - const items = remotes.map(_ => new OpenRemoteCommandQuickPickItem(_, type, ...args)) as (OpenRemoteCommandQuickPickItem | CommandQuickPickItem)[]; + static async show(remotes: GitRemote[], placeHolder: string, resource: RemoteResource, goBackCommand?: CommandQuickPickItem): Promise { + const items = remotes.map(_ => new OpenRemoteCommandQuickPickItem(_, resource)) as (OpenRemoteCommandQuickPickItem | CommandQuickPickItem)[]; if (goBackCommand) { items.splice(0, 0, goBackCommand); @@ -118,12 +109,11 @@ export class RemotesQuickPick { // const scope = await Keyboard.instance.beginScope({ left: goBackCommand }); - const pick = await window.showQuickPick(items, - { - placeHolder: placeHolder, - ignoreFocusOut: getQuickPickIgnoreFocusOut() - } as QuickPickOptions); - if (!pick) return undefined; + const pick = await window.showQuickPick(items, { + placeHolder: placeHolder, + ignoreFocusOut: getQuickPickIgnoreFocusOut() + } as QuickPickOptions); + if (pick === undefined) return undefined; // await scope.dispose(); diff --git a/src/quickPicks/repoStatus.ts b/src/quickPicks/repoStatus.ts index 505e5dc..bec768d 100644 --- a/src/quickPicks/repoStatus.ts +++ b/src/quickPicks/repoStatus.ts @@ -1,7 +1,7 @@ 'use strict'; import { Iterables } from '../system'; import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; -import { Commands, Keyboard } from '../commands'; +import { Commands, Keyboard, OpenChangedFilesCommandArgs, ShowQuickBranchHistoryCommandArgs, ShowQuickRepoStatusCommandArgs, ShowQuickStashListCommandArgs } from '../commands'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, OpenFileCommandQuickPickItem } from './common'; import { GitService, GitStatusFile, GitUri, IGitStatus } from '../gitService'; import * as path from 'path'; @@ -36,7 +36,12 @@ export class OpenStatusFilesCommandQuickPickItem extends CommandQuickPickItem { label: `$(file-symlink-file) Open Changed Files`, description: '' //detail: `Opens all of the changed files in the repository` - }, Commands.OpenChangedFiles, [undefined, uris]); + }, Commands.OpenChangedFiles, [ + undefined, + { + uris + } as OpenChangedFilesCommandArgs + ]); } } @@ -72,7 +77,12 @@ export class RepoStatusQuickPick { const currentCommand = new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to \u00a0$(git-branch) ${status.branch} status` - }, Commands.ShowQuickRepoStatus, [undefined, goBackCommand]); + }, Commands.ShowQuickRepoStatus, [ + undefined, + { + goBackCommand + } as ShowQuickRepoStatusCommandArgs + ]); if (hasStaged) { let index = 0; @@ -81,7 +91,12 @@ export class RepoStatusQuickPick { items.splice(unstagedIndex, 0, new CommandQuickPickItem({ label: `Unstaged Files`, description: unstagedStatus - }, Commands.ShowQuickRepoStatus, [undefined, goBackCommand])); + }, Commands.ShowQuickRepoStatus, [ + undefined, + { + goBackCommand + } as ShowQuickRepoStatusCommandArgs + ])); items.splice(unstagedIndex, 0, new OpenStatusFilesCommandQuickPickItem(files.filter(_ => _.status !== 'D' && _.staged), { label: `\u00a0\u00a0\u00a0\u00a0 $(file-symlink-file) Open Staged Files`, @@ -97,13 +112,23 @@ export class RepoStatusQuickPick { items.splice(index++, 0, new CommandQuickPickItem({ label: `Staged Files`, description: stagedStatus - }, Commands.ShowQuickRepoStatus, [undefined, goBackCommand])); + }, Commands.ShowQuickRepoStatus, [ + undefined, + { + goBackCommand + } as ShowQuickRepoStatusCommandArgs + ])); } else if (files.some(_ => !_.staged)) { items.splice(0, 0, new CommandQuickPickItem({ label: `Unstaged Files`, description: unstagedStatus - }, Commands.ShowQuickRepoStatus, [undefined, goBackCommand])); + }, Commands.ShowQuickRepoStatus, [ + undefined, + { + goBackCommand + } as ShowQuickRepoStatusCommandArgs + ])); } if (files.length) { @@ -117,13 +142,23 @@ export class RepoStatusQuickPick { items.push(new CommandQuickPickItem({ label: `No changes in the working tree`, description: '' - }, Commands.ShowQuickRepoStatus, [undefined, goBackCommand])); + }, Commands.ShowQuickRepoStatus, [ + undefined, + { + goBackCommand + } as ShowQuickRepoStatusCommandArgs + ])); } items.splice(0, 0, new CommandQuickPickItem({ label: `$(repo-push) Show Stashed Changes`, description: `\u00a0 \u2014 \u00a0\u00a0 shows stashed changes in the repository` - }, Commands.ShowQuickStashList, [new GitUri(Uri.file(status.repoPath), { fileName: '', repoPath: status.repoPath }), currentCommand])); + }, Commands.ShowQuickStashList, [ + new GitUri(Uri.file(status.repoPath), { fileName: '', repoPath: status.repoPath }), + { + goBackCommand: currentCommand + } as ShowQuickStashListCommandArgs + ])); if (status.upstream && status.state.ahead) { items.splice(0, 0, new CommandQuickPickItem({ @@ -131,9 +166,12 @@ export class RepoStatusQuickPick { description: `\u00a0 \u2014 \u00a0\u00a0 shows commits in \u00a0$(git-branch) ${status.branch} but not \u00a0$(git-branch) ${status.upstream}` }, Commands.ShowQuickBranchHistory, [ new GitUri(Uri.file(status.repoPath), { fileName: '', repoPath: status.repoPath, sha: `${status.upstream}..${status.branch}` }), - status.branch, 0, currentCommand - ]) - ); + { + branch: status.branch, + maxCount: 0, + goBackCommand: currentCommand + } as ShowQuickBranchHistoryCommandArgs + ])); } if (status.upstream && status.state.behind) { @@ -142,16 +180,24 @@ export class RepoStatusQuickPick { description: `\u00a0 \u2014 \u00a0\u00a0 shows commits in \u00a0$(git-branch) ${status.upstream} but not \u00a0$(git-branch) ${status.branch}${status.sha ? ` (since \u00a0$(git-commit) ${status.sha.substring(0, 8)})` : ''}` }, Commands.ShowQuickBranchHistory, [ new GitUri(Uri.file(status.repoPath), { fileName: '', repoPath: status.repoPath, sha: `${status.branch}..${status.upstream}` }), - status.upstream, 0, currentCommand - ]) - ); + { + branch: status.upstream, + maxCount: 0, + goBackCommand: currentCommand + } as ShowQuickBranchHistoryCommandArgs + ])); } if (status.upstream && !status.state.ahead && !status.state.behind) { items.splice(0, 0, new CommandQuickPickItem({ label: `$(git-branch) ${status.branch} is up-to-date with \u00a0$(git-branch) ${status.upstream}`, description: '' - }, Commands.ShowQuickRepoStatus, [undefined, goBackCommand])); + }, Commands.ShowQuickRepoStatus, [ + undefined, + { + goBackCommand + } as ShowQuickRepoStatusCommandArgs + ])); } diff --git a/src/quickPicks/stashList.ts b/src/quickPicks/stashList.ts index aff739c..919ff96 100644 --- a/src/quickPicks/stashList.ts +++ b/src/quickPicks/stashList.ts @@ -1,7 +1,7 @@ 'use strict'; import { Iterables } from '../system'; import { QuickPickOptions, window } from 'vscode'; -import { Commands, Keyboard } from '../commands'; +import { Commands, Keyboard, StashSaveCommandArgs } from '../commands'; import { GitService, IGitStash } from '../gitService'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks'; @@ -14,12 +14,22 @@ export class StashListQuickPick { items.splice(0, 0, new CommandQuickPickItem({ label: `$(repo-push) Stash Unstaged Changes`, description: `\u00a0 \u2014 \u00a0\u00a0 stashes only unstaged changes` - }, Commands.StashSave, [undefined, true, currentCommand])); + }, Commands.StashSave, [ + { + unstagedOnly: true, + goBackCommand: currentCommand + } as StashSaveCommandArgs + ])); items.splice(0, 0, new CommandQuickPickItem({ label: `$(repo-push) Stash Changes`, description: `\u00a0 \u2014 \u00a0\u00a0 stashes all changes` - }, Commands.StashSave, [undefined, undefined, currentCommand])); + }, Commands.StashSave, [ + { + unstagedOnly: false, + goBackCommand: currentCommand + } as StashSaveCommandArgs + ])); } if (goBackCommand) {