diff --git a/README.md b/README.md index 4618df7..059ea37 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, - Adds a `Compare Line Commit with Previous` command (`gitlens.diffLineWithPrevious`) with a shortcut of `shift+alt+,` to compare the active file/diff with the previous line commit revision +- Adds a `Compare File with Revision...` command (`gitlens.diffWithRevision`) to compare the active file with the selected revision of the same file + - Adds a `Compare File with Working Tree` command (`gitlens.diffWithWorking`) with a shortcut of `shift+alt+w` to compare the most recent commit revision of the active file/diff with the working tree - Adds a `Compare Line Commit with Working Tree` command (`gitlens.diffLineWithWorking`) with a shortcut of `alt+w` to compare the commit revision of the active line with the working tree diff --git a/package.json b/package.json index 95400a7..20ea9fe 100644 --- a/package.json +++ b/package.json @@ -770,6 +770,11 @@ "category": "GitLens" }, { + "command": "gitlens.diffWithRevision", + "title": "Compare File with Revision...", + "category": "GitLens" + }, + { "command": "gitlens.diffWithWorking", "title": "Compare File with Working Tree", "category": "GitLens" @@ -951,6 +956,10 @@ "when": "gitlens:isBlameable" }, { + "command": "gitlens.diffWithRevision", + "when": "gitlens:isTracked" + }, + { "command": "gitlens.diffWithWorking", "when": "gitlens:isTracked" }, diff --git a/src/commands.ts b/src/commands.ts index c18c7e0..b46c171 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,6 +10,7 @@ export * from './commands/diffLineWithWorking'; export * from './commands/diffWithBranch'; export * from './commands/diffWithNext'; export * from './commands/diffWithPrevious'; +export * from './commands/diffWithRevision'; export * from './commands/diffWithWorking'; export * from './commands/openChangedFiles'; export * from './commands/openBranchInRemote'; diff --git a/src/commands/common.ts b/src/commands/common.ts index 2faf198..7ba146d 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -11,6 +11,7 @@ export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | + 'gitlens.diffWithRevision' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | 'gitlens.openChangedFiles' | @@ -49,6 +50,7 @@ export const Commands = { DiffWithNext: 'gitlens.diffWithNext' as Commands, DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands, DiffLineWithPrevious: 'gitlens.diffLineWithPrevious' as Commands, + DiffWithRevision: 'gitlens.diffWithRevision' as Commands, DiffWithWorking: 'gitlens.diffWithWorking' as Commands, DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands, OpenChangedFiles: 'gitlens.openChangedFiles' as Commands, diff --git a/src/commands/diffWithRevision.ts b/src/commands/diffWithRevision.ts new file mode 100644 index 0000000..d934ef1 --- /dev/null +++ b/src/commands/diffWithRevision.ts @@ -0,0 +1,81 @@ +'use strict'; +import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, CommandContext, Commands, getCommandUri } from './common'; +import { BuiltInCommands, GlyphChars } from '../constants'; +import { GitService, GitUri } from '../gitService'; +import { Logger } from '../logger'; +import { Messages } from '../messages'; +import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; +import * as path from 'path'; + +export interface DiffWithRevisionCommandArgs { + line?: number; + maxCount?: number; + showOptions?: TextDocumentShowOptions; +} + +export class DiffWithRevisionCommand extends ActiveEditorCommand { + + constructor(private git: GitService) { + super(Commands.DiffWithRevision); + } + + async run(context: CommandContext, args: DiffWithRevisionCommandArgs = {}): Promise { + // Since we can change the args and they could be cached -- make a copy + switch (context.type) { + case 'uri': + return this.execute(context.editor, context.uri, { ...args }); + case 'scm-states': + const resource = context.scmResourceStates[0]; + return this.execute(undefined, resource.resourceUri, { ...args }); + case 'scm-groups': + return undefined; + default: + return this.execute(context.editor, undefined, { ...args }); + } + } + + async execute(editor: TextEditor | undefined, uri?: Uri, args: DiffWithRevisionCommandArgs = {}): Promise { + uri = getCommandUri(uri, editor); + if (uri === undefined) return undefined; + + if (args.line === undefined) { + args.line = editor === undefined ? 0 : editor.selection.active.line; + } + + const gitUri = await GitUri.fromUri(uri, this.git); + if (args.maxCount == null) { + args.maxCount = this.git.config.advanced.maxQuickHistory; + } + + const progressCancellation = FileHistoryQuickPick.showProgress(gitUri); + try { + const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { maxCount: args.maxCount }); + if (log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open history compare'); + + if (progressCancellation.token.isCancellationRequested) return undefined; + + const pick = await FileHistoryQuickPick.show(this.git, log, gitUri, progressCancellation, { pickerOnly: true }); + if (pick === undefined) return undefined; + + if (pick instanceof CommandQuickPickItem) return pick.execute(); + + const compare = await this.git.getVersionedFile(gitUri.repoPath, gitUri.fsPath, pick.commit.sha); + + await commands.executeCommand(BuiltInCommands.Diff, + Uri.file(compare), + gitUri.fileUri(), + `${path.basename(gitUri.fsPath)} (${pick.commit.shortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(gitUri.fsPath)}`, + args.showOptions); + + if (args.line === undefined || args.line === 0) return undefined; + + // 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, 'DiffWithRevisionCommand', 'getVersionedFile'); + return window.showErrorMessage(`Unable to open history compare. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index d23391b..6ffba39 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -1,30 +1,30 @@ -'use strict'; -import { Strings } from '../system'; -import { commands, Range, TextEditor, Uri, window } from 'vscode'; +'use strict'; +import { Strings } from '../system'; +import { commands, Range, TextEditor, Uri, window } from 'vscode'; import { ActiveEditorCachedCommand, CommandContext, Commands, getCommandUri } from './common'; -import { GlyphChars } from '../constants'; -import { GitLog, GitService, GitUri } from '../gitService'; -import { Logger } from '../logger'; -import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; -import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails'; -import { Messages } from '../messages'; -import * as path from 'path'; - -export interface ShowQuickFileHistoryCommandArgs { - log?: GitLog; - maxCount?: number; - range?: Range; - - goBackCommand?: CommandQuickPickItem; - nextPageCommand?: CommandQuickPickItem; -} - -export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand { - - constructor(private git: GitService) { - super(Commands.ShowQuickFileHistory); - } - +import { GlyphChars } from '../constants'; +import { GitLog, GitService, GitUri } from '../gitService'; +import { Logger } from '../logger'; +import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; +import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails'; +import { Messages } from '../messages'; +import * as path from 'path'; + +export interface ShowQuickFileHistoryCommandArgs { + log?: GitLog; + maxCount?: number; + range?: Range; + + goBackCommand?: CommandQuickPickItem; + nextPageCommand?: CommandQuickPickItem; +} + +export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand { + + constructor(private git: GitService) { + super(Commands.ShowQuickFileHistory); + } + async run(context: CommandContext, args: ShowQuickFileHistoryCommandArgs = {}): Promise { // Since we can change the args and they could be cached -- make a copy switch (context.type) { @@ -41,53 +41,53 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand { } async execute(editor: TextEditor | undefined, 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 (args.maxCount == null) { - args.maxCount = this.git.config.advanced.maxQuickHistory; - } - - const progressCancellation = FileHistoryQuickPick.showProgress(gitUri); - try { - if (args.log === undefined) { - args.log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { maxCount: args.maxCount, range: args.range }); - if (args.log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show file history'); - } - - if (progressCancellation.token.isCancellationRequested) 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(); - - // Create a command to get back to where we are right now - const currentCommand = new CommandQuickPickItem({ - label: `go back ${GlyphChars.ArrowBack}`, - description: `${Strings.pad(GlyphChars.Dash, 2, 3)} to history of ${GlyphChars.Space}$(file-text) ${path.basename(pick.commit.fileName)}${gitUri.sha ? ` from ${GlyphChars.Space}$(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'); - return window.showErrorMessage(`Unable to show file history. See output channel for more details`); - } - finally { - progressCancellation.dispose(); - } - } + uri = getCommandUri(uri, editor); + if (uri === undefined) return commands.executeCommand(Commands.ShowQuickCurrentBranchHistory); + + const gitUri = await GitUri.fromUri(uri, this.git); + + if (args.maxCount == null) { + args.maxCount = this.git.config.advanced.maxQuickHistory; + } + + const progressCancellation = FileHistoryQuickPick.showProgress(gitUri); + try { + if (args.log === undefined) { + args.log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { maxCount: args.maxCount, range: args.range }); + if (args.log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show file history'); + } + + if (progressCancellation.token.isCancellationRequested) return undefined; + + const pick = await FileHistoryQuickPick.show(this.git, args.log, gitUri, progressCancellation, { goBackCommand: args.goBackCommand, nextPageCommand: args.nextPageCommand }); + if (pick === undefined) return undefined; + + if (pick instanceof CommandQuickPickItem) return pick.execute(); + + // Create a command to get back to where we are right now + const currentCommand = new CommandQuickPickItem({ + label: `go back ${GlyphChars.ArrowBack}`, + description: `${Strings.pad(GlyphChars.Dash, 2, 3)} to history of ${GlyphChars.Space}$(file-text) ${path.basename(pick.commit.fileName)}${gitUri.sha ? ` from ${GlyphChars.Space}$(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'); + return window.showErrorMessage(`Unable to show file history. See output channel for more details`); + } + finally { + progressCancellation.dispose(); + } + } } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 74b191f..b8817da 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import { AnnotationController } from './annotations/annotationController'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; -import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; +import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand} from './commands'; import { ResetSuppressedWarningsCommand } from './commands'; import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleFileRecentChangesCommand, ToggleLineBlameCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; @@ -97,6 +97,7 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new DiffWithBranchCommand(git)); context.subscriptions.push(new DiffWithNextCommand(git)); context.subscriptions.push(new DiffWithPreviousCommand(git)); + context.subscriptions.push(new DiffWithRevisionCommand(git)); context.subscriptions.push(new DiffWithWorkingCommand(git)); context.subscriptions.push(new OpenBranchInRemoteCommand(git)); context.subscriptions.push(new OpenCommitInRemoteCommand(git)); diff --git a/src/quickPicks/fileHistory.ts b/src/quickPicks/fileHistory.ts index 4f19840..709c9c1 100644 --- a/src/quickPicks/fileHistory.ts +++ b/src/quickPicks/fileHistory.ts @@ -20,7 +20,9 @@ export class FileHistoryQuickPick { }); } - static async show(git: GitService, log: GitLog, uri: GitUri, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem, nextPageCommand?: CommandQuickPickItem): Promise { + static async show(git: GitService, log: GitLog, uri: GitUri, progressCancellation: CancellationTokenSource, options: { goBackCommand?: CommandQuickPickItem, nextPageCommand?: CommandQuickPickItem, pickerOnly?: boolean } = {}): Promise { + options = { ...{ pickerOnly: false }, ...options }; + const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c))) as (CommitQuickPickItem | CommandQuickPickItem)[]; let previousPageCommand: CommandQuickPickItem | undefined = undefined; @@ -36,7 +38,7 @@ export class FileHistoryQuickPick { Uri.file(uri.fsPath), { maxCount: 0, - goBackCommand + goBackCommand: options.goBackCommand } as ShowQuickFileHistoryCommandArgs ])); } @@ -59,7 +61,7 @@ export class FileHistoryQuickPick { log: log, maxCount: log.maxCount, range: log.range, - goBackCommand + goBackCommand: options.goBackCommand } as ShowQuickFileHistoryCommandArgs ]) } as ShowQuickFileHistoryCommandArgs @@ -67,9 +69,9 @@ export class FileHistoryQuickPick { } } - if (nextPageCommand) { + if (options.nextPageCommand) { index++; - items.splice(0, 0, nextPageCommand); + items.splice(0, 0, options.nextPageCommand); } if (log.truncated) { @@ -80,8 +82,8 @@ export class FileHistoryQuickPick { uri, { maxCount: log.maxCount, - goBackCommand, - nextPageCommand + goBackCommand: options.goBackCommand, + nextPageCommand: options.nextPageCommand } as ShowQuickFileHistoryCommandArgs ]); @@ -94,7 +96,7 @@ export class FileHistoryQuickPick { new GitUri(uri, last), { maxCount: log.maxCount, - goBackCommand, + goBackCommand: options.goBackCommand, nextPageCommand: npc } as ShowQuickFileHistoryCommandArgs ]); @@ -105,54 +107,56 @@ export class FileHistoryQuickPick { } } - const branch = await git.getBranch(uri.repoPath!); - - const currentCommand = new CommandQuickPickItem({ - label: `go back ${GlyphChars.ArrowBack}`, - description: `${Strings.pad(GlyphChars.Dash, 2, 3)} to history of ${GlyphChars.Space}$(file-text) ${path.basename(uri.fsPath)}${uri.sha ? ` from ${GlyphChars.Space}$(git-commit) ${uri.shortSha}` : ''}` - }, 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 === undefined) { - items.splice(index++, 0, new CommandQuickPickItem({ - label: `$(history) Show Branch History`, - description: `${Strings.pad(GlyphChars.Dash, 2, 3)} shows ${GlyphChars.Space}$(git-branch) ${branch!.name} history` - }, Commands.ShowQuickCurrentBranchHistory, - [ - undefined, + if (!options.pickerOnly) { + const branch = await git.getBranch(uri.repoPath!); + + const currentCommand = new CommandQuickPickItem({ + label: `go back ${GlyphChars.ArrowBack}`, + description: `${Strings.pad(GlyphChars.Dash, 2, 3)} to history of ${GlyphChars.Space}$(file-text) ${path.basename(uri.fsPath)}${uri.sha ? ` from ${GlyphChars.Space}$(git-commit) ${uri.shortSha}` : ''}` + }, Commands.ShowQuickFileHistory, [ + uri, { - goBackCommand: currentCommand - } as ShowQuickCurrentBranchHistoryCommandArgs - ])); - } + log, + maxCount: log.maxCount, + range: log.range + } as ShowQuickFileHistoryCommandArgs + ]); + + // Only show the full repo option if we are the root + if (options.goBackCommand === undefined) { + items.splice(index++, 0, new CommandQuickPickItem({ + label: `$(history) Show Branch History`, + description: `${Strings.pad(GlyphChars.Dash, 2, 3)} shows ${GlyphChars.Space}$(git-branch) ${branch!.name} history` + }, Commands.ShowQuickCurrentBranchHistory, + [ + undefined, + { + 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, { - type: 'file', - branch: branch!.name, - fileName: uri.getRelativePath(), - sha: uri.sha - } as RemoteResource, currentCommand)); - } + const remotes = Arrays.uniqueBy(await git.getRemotes(uri.repoPath!), _ => _.url, _ => !!_.provider); + if (remotes.length) { + items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, { + type: 'file', + branch: branch!.name, + fileName: uri.getRelativePath(), + sha: uri.sha + } as RemoteResource, currentCommand)); + } - if (goBackCommand) { - items.splice(0, 0, goBackCommand); + if (options.goBackCommand) { + items.splice(0, 0, options.goBackCommand); + } } if (progressCancellation.token.isCancellationRequested) return undefined; const scope = await Keyboard.instance.beginScope({ - left: goBackCommand, + left: options.goBackCommand, ',': previousPageCommand, - '.': nextPageCommand + '.': options.nextPageCommand }); const commit = Iterables.first(log.commits.values());