From 480dcb95fb3686a37d79739d23f7fb227bd6332b Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 27 Aug 2017 04:22:49 -0400 Subject: [PATCH] Adds a file history explorer view --- README.md | 10 ++- package.json | 107 +++++++++++++++++++++++++ src/commands/showQuickCommitDetails.ts | 15 +++- src/configuration.ts | 6 ++ src/extension.ts | 2 + src/views/commitNode.ts | 37 ++++++++- src/views/fileHistoryExplorer.ts | 89 ++++++++++++++++++++ src/views/fileHistoryNode.ts | 6 +- 8 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 src/views/fileHistoryExplorer.ts diff --git a/README.md b/README.md index f31f051..c716fd1 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,9 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, ### Navigate and Explore -- Adds a `Git Stashes` explorer to the Explorer activity ([optional](#git-stashes-explorer-settings), off by default) +- Adds a [customizable](#git-file-history-explorer-settings) `Git File History` explorer to the Explorer activity -- currently [insiders](#insiders) only + +- Adds a [customizable](#git-stashes-explorer-settings) `Git Stashes` explorer to the Explorer activity ![Git Stashes explorer](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-git-stashes.png) @@ -289,6 +291,12 @@ GitLens is highly customizable and provides many configuration settings to allow |`gitlens.codeLens.customLocationSymbols`|Specifies the set of document symbols where Git code lens will be shown in the document |`gitlens.codeLens.perLanguageLocations`|Specifies where Git code lens will be shown in the document for the specified languages +### Git File History Explorer Settings + +|Name | Description +|-----|------------ +|`gitlens.fileHistoryExplorer.commitFormat`|Specifies the format of committed changes in the `Git File History` explorer
Available tokens
${id} - commit id
${author} - commit author
${message} - commit message
${ago} - relative commit date (e.g. 1 day ago)
${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)
${authorAgo} - commit author, relative commit date
See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting + ### Git Stashes Explorer Settings |Name | Description diff --git a/package.json b/package.json index 89ae502..6de3747 100644 --- a/package.json +++ b/package.json @@ -413,6 +413,11 @@ "default": null, "description": "Specifies how all absolute dates will be formatted by default\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats" }, + "gitlens.fileHistoryExplorer.commitFormat": { + "type": "string", + "default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0\u2022\u00a0 ${id}", + "description": "Specifies the format of committed changes in the `Git File History` explorer\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" + }, "gitlens.stashExplorer.stashFormat": { "type": "string", "default": "${message}", @@ -979,6 +984,40 @@ "light": "images/light/icon-refresh.svg" } }, + { + "command": "gitlens.fileHistoryExplorer.refresh", + "title": "Refresh", + "category": "GitLens", + "icon": { + "dark": "images/dark/icon-refresh.svg", + "light": "images/light/icon-refresh.svg" + } + }, + { + "command": "gitlens.fileHistoryExplorer.openChanges", + "title": "Open Changes", + "category": "GitLens" + }, + { + "command": "gitlens.fileHistoryExplorer.openFile", + "title": "Open File", + "category": "GitLens" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileRevision", + "title": "Open File Revision", + "category": "GitLens" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileInRemote", + "title": "Open File in Remote", + "category": "GitLens" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileRevisionInRemote", + "title": "Open File Revision in Remote", + "category": "GitLens" + }, { "command": "gitlens.stashExplorer.refresh", "title": "Refresh", @@ -1163,6 +1202,29 @@ "command": "gitlens.gitExplorer.refresh", "when": "false" }, + { + "command": "gitlens.fileHistoryExplorer.refresh", + "when": "false" + }, + { + "command": "gitlens.fileHistoryExplorer.openChanges", + "when": "false" + }, + { + "command": "gitlens.fileHistoryExplorer.openFile", + "when": "false" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileRevision", + "when": "false" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileInRemote", + "when": "false" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileRevisionInRemote", + "when": "false" }, { "command": "gitlens.stashExplorer.refresh", @@ -1374,6 +1436,11 @@ "when": "gitlens:enabled && view == gitlens.gitExplorer", "group": "navigation" }, + { + "command": "gitlens.fileHistoryExplorer.refresh", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer", + "group": "navigation" + }, { "command": "gitlens.stashSave", "when": "gitlens:enabled && view == gitlens.stashExplorer", @@ -1406,6 +1473,41 @@ "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == commit-file", "group": "2_gitlens@2" }, + { + "command": "gitlens.fileHistoryExplorer.openChanges", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "1_gitlens@1" + }, + { + "command": "gitlens.diffWithWorking", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "1_gitlens@2" + }, + { + "command": "gitlens.fileHistoryExplorer.openFile", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "2_gitlens@1" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileRevision", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "2_gitlens@2" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileInRemote", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "2_gitlens@3" + }, + { + "command": "gitlens.fileHistoryExplorer.openFileRevisionInRemote", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "2_gitlens@4" + }, + { + "command": "gitlens.showQuickCommitDetails", + "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "group": "3_gitlens@1" + }, { "command": "gitlens.stashApply", "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == stash-commit", @@ -1537,6 +1639,11 @@ ], "views": { "explorer": [ + { + "id": "gitlens.fileHistoryExplorer", + "name": "Git File History", + "when": "gitlens:enabled && config.gitlens.insiders" + }, { "id": "gitlens.stashExplorer", "name": "Git Stashes", diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index 37d7336..2015a53 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -1,8 +1,9 @@ 'use strict'; import { Strings } from '../system'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; +import { ActiveEditorCachedCommand, CommandContext, Commands, getCommandUri } from './common'; import { GlyphChars } from '../constants'; +import { CommitNode } from '../views/explorerNodes'; import { GitCommit, GitLog, GitLogCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, CommitDetailsQuickPick, CommitWithFileStatusQuickPickItem } from '../quickPicks'; @@ -24,6 +25,18 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { super(Commands.ShowQuickCommitDetails); } + protected async preExecute(context: CommandContext, ...args: any[]): Promise { + if (context.type === 'view') { + if (context.node instanceof CommitNode) { + args = [{ sha: context.node.uri.sha, commit: context.node.commit }]; + } + else { + args = [{ sha: context.node.uri.sha }]; + } + } + return this.execute(context.editor, context.uri, ...args); + } + async execute(editor?: TextEditor, uri?: Uri, args: ShowQuickCommitDetailsCommandArgs = {}) { uri = getCommandUri(uri, editor); if (uri === undefined) return undefined; diff --git a/src/configuration.ts b/src/configuration.ts index 3a4d1cb..fee3e17 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -296,6 +296,12 @@ export interface IConfig { defaultDateFormat: string | null; + fileHistoryExplorer: { + commitFormat: string; + // commitFileFormat: string; + // dateFormat: string | null; + }; + gitExplorer: { commitFormat: string; commitFileFormat: string; diff --git a/src/extension.ts b/src/extension.ts index 5ec6931..f3f92eb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,7 @@ import { CodeLensController } from './codeLensController'; import { CurrentLineController, LineAnnotationType } from './currentLineController'; import { GitContentProvider } from './gitContentProvider'; // import { GitExplorer } from './views/gitExplorer'; +import { FileHistoryExplorer } from './views/fileHistoryExplorer'; import { StashExplorer } from './views/stashExplorer'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; import { GitContextTracker, GitService } from './gitService'; @@ -96,6 +97,7 @@ export async function activate(context: ExtensionContext) { // const explorer = new GitExplorer(context, git); // context.subscriptions.push(window.registerTreeDataProvider('gitlens.gitExplorer', explorer)); + context.subscriptions.push(window.registerTreeDataProvider('gitlens.fileHistoryExplorer', new FileHistoryExplorer(context, git))); context.subscriptions.push(window.registerTreeDataProvider('gitlens.stashExplorer', new StashExplorer(context, git))); context.subscriptions.push(commands.registerTextEditorCommand('gitlens.computingFileAnnotations', () => { })); diff --git a/src/views/commitNode.ts b/src/views/commitNode.ts index 3069866..510b8a9 100644 --- a/src/views/commitNode.ts +++ b/src/views/commitNode.ts @@ -1,6 +1,7 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { CommitFileNode } from './commitFileNode'; import { ExplorerNode, ResourceType } from './explorerNode'; import { CommitFormatter, GitCommit, GitService, GitUri } from '../gitService'; @@ -14,6 +15,8 @@ export class CommitNode extends ExplorerNode { } async getChildren(): Promise { + if (this.commit.type === 'file') Promise.resolve([]); + const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1); if (log === undefined) return []; @@ -24,14 +27,40 @@ export class CommitNode extends ExplorerNode { } getTreeItem(): TreeItem { - const label = CommitFormatter.fromTemplate(this.template, this.commit, this.git.config.defaultDateFormat); + const item = new TreeItem(CommitFormatter.fromTemplate(this.template, this.commit, this.git.config.defaultDateFormat)); + if (this.commit.type === 'file') { + item.collapsibleState = TreeItemCollapsibleState.None; + item.command = this.getCommand(); + item.contextValue = 'commit-file'; + } + else { + item.collapsibleState = TreeItemCollapsibleState.Collapsed; + item.contextValue = this.resourceType; + } - const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); - item.contextValue = this.resourceType; item.iconPath = { dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'), light: this.context.asAbsolutePath('images/light/icon-commit.svg') }; + return item; } + + getCommand(): Command | undefined { + return { + title: 'Compare File with Previous', + command: Commands.DiffWithPrevious, + arguments: [ + new GitUri(this.uri, this.commit), + { + commit: this.commit, + line: 0, + showOptions: { + preserveFocus: true, + preview: true + } + } as DiffWithPreviousCommandArgs + ] + }; + } } \ No newline at end of file diff --git a/src/views/fileHistoryExplorer.ts b/src/views/fileHistoryExplorer.ts new file mode 100644 index 0000000..1487757 --- /dev/null +++ b/src/views/fileHistoryExplorer.ts @@ -0,0 +1,89 @@ +'use strict'; +// import { Arrays } from '../system'; +import { commands, Event, EventEmitter, ExtensionContext, TextEditor, TreeDataProvider, TreeItem, Uri, window } from 'vscode'; +import { Commands, DiffWithPreviousCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands'; +import { UriComparer } from '../comparers'; +import { CommitNode, ExplorerNode, FileHistoryNode, TextExplorerNode } from './explorerNodes'; +import { GitService, GitUri } from '../gitService'; + +export * from './explorerNodes'; + +export class FileHistoryExplorer implements TreeDataProvider { + + private _node?: ExplorerNode; + + private _onDidChangeTreeData = new EventEmitter(); + public get onDidChangeTreeData(): Event { + return this._onDidChangeTreeData.event; + } + + constructor(private context: ExtensionContext, private git: GitService) { + commands.registerCommand('gitlens.fileHistoryExplorer.refresh', this.refresh, this); + commands.registerCommand('gitlens.fileHistoryExplorer.openChanges', this.openChanges, this); + commands.registerCommand('gitlens.fileHistoryExplorer.openFile', this.openFile, this); + commands.registerCommand('gitlens.fileHistoryExplorer.openFileRevision', this.openFileRevision, this); + commands.registerCommand('gitlens.fileHistoryExplorer.openFileInRemote', this.openFileInRemote, this); + commands.registerCommand('gitlens.fileHistoryExplorer.openFileRevisionInRemote', this.openFileRevisionInRemote, this); + + context.subscriptions.push(window.onDidChangeActiveTextEditor(this.onActiveEditorChanged, this)); + + this._node = this.getRootNode(window.activeTextEditor); + } + + async getTreeItem(node: ExplorerNode): Promise { + return node.getTreeItem(); + } + + async getChildren(node?: ExplorerNode): Promise { + if (this._node === undefined) return [new TextExplorerNode('No active file')]; + if (node === undefined) return this._node.getChildren(); + return node.getChildren(); + } + + private getRootNode(editor: TextEditor | undefined): ExplorerNode | undefined { + if (window.visibleTextEditors.length === 0) return undefined; + if (editor === undefined) return this._node; + + const uri = this.git.getGitUriForFile(editor.document.uri) || new GitUri(editor.document.uri, { repoPath: this.git.repoPath, fileName: editor.document.uri.fsPath }); + if (UriComparer.equals(uri, this._node && this._node.uri)) return this._node; + + return new FileHistoryNode(uri, this.context, this.git); + } + + private onActiveEditorChanged(editor: TextEditor | undefined) { + const node = this.getRootNode(editor); + if (node === this._node) return; + + this.refresh(); + } + + refresh(node?: ExplorerNode) { + this._node = node || this.getRootNode(window.activeTextEditor); + this._onDidChangeTreeData.fire(); + } + + private openChanges(node: CommitNode) { + const command = node.getCommand(); + if (command === undefined || command.arguments === undefined) return; + + const [uri, args] = command.arguments as [Uri, DiffWithPreviousCommandArgs]; + args.showOptions!.preview = false; + return commands.executeCommand(command.command, uri, args); + } + + private openFile(node: CommitNode) { + return openEditor(node.uri, { preserveFocus: true, preview: false }); + } + + private openFileRevision(node: CommitNode) { + return openEditor(GitService.toGitContentUri(node.uri), { preserveFocus: true, preview: false }); + } + + private async openFileInRemote(node: CommitNode) { + return commands.executeCommand(Commands.OpenFileInRemote, node.commit.uri, { range: false } as OpenFileInRemoteCommandArgs); + } + + private async openFileRevisionInRemote(node: CommitNode) { + return commands.executeCommand(Commands.OpenFileInRemote, new GitUri(node.commit.uri, node.commit), { range: false } as OpenFileInRemoteCommandArgs); + } +} \ No newline at end of file diff --git a/src/views/fileHistoryNode.ts b/src/views/fileHistoryNode.ts index 6a163ab..f55a42d 100644 --- a/src/views/fileHistoryNode.ts +++ b/src/views/fileHistoryNode.ts @@ -14,11 +14,11 @@ export class FileHistoryNode extends ExplorerNode { super(uri); } - async getChildren(): Promise { + async getChildren(): Promise { const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); - if (log === undefined) return []; + if (log === undefined) return [new TextExplorerNode('No file history')]; - return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; + return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.git.config.fileHistoryExplorer.commitFormat, this.context, this.git))]; } getTreeItem(): TreeItem {