From 9782a81e46ebf5fb398055e8cf7f4e61fedb49ff Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 30 Aug 2017 12:26:30 -0400 Subject: [PATCH] Adds new GitLens custom view --- images/dark/icon-add.svg | 6 +- images/dark/icon-branch.svg | 4 + images/dark/icon-commit.svg | 4 +- images/dark/icon-download.svg | 4 + images/dark/icon-history.svg | 4 + images/dark/icon-refresh.svg | 5 +- images/dark/icon-remote.svg | 4 + images/dark/icon-repo.svg | 4 + images/dark/icon-search.svg | 4 + images/dark/icon-stash.svg | 4 + images/dark/icon-upload.svg | 4 + images/light/icon-add.svg | 6 +- images/light/icon-branch.svg | 4 + images/light/icon-commit.svg | 4 +- images/light/icon-download.svg | 4 + images/light/icon-history.svg | 4 + images/light/icon-refresh.svg | 5 +- images/light/icon-remote.svg | 4 + images/light/icon-repo.svg | 4 + images/light/icon-search.svg | 4 + images/light/icon-stash.svg | 4 + images/light/icon-upload.svg | 4 + package.json | 285 ++++++++++++++++------------- src/commands/common.ts | 13 ++ src/commands/copyMessageToClipboard.ts | 14 +- src/commands/copyShaToClipboard.ts | 14 +- src/commands/diffWithPrevious.ts | 14 +- src/commands/openBranchInRemote.ts | 12 +- src/commands/openCommitInRemote.ts | 5 +- src/commands/openFileInRemote.ts | 12 +- src/commands/openInRemote.ts | 35 ++-- src/commands/showCommitSearch.ts | 7 - src/commands/showFileHistory.ts | 12 +- src/commands/showQuickCommitDetails.ts | 5 +- src/commands/showQuickCommitFileDetails.ts | 14 +- src/commands/stashApply.ts | 15 +- src/commands/stashDelete.ts | 12 +- src/configuration.ts | 12 +- src/constants.ts | 14 +- src/currentLineController.ts | 4 +- src/extension.ts | 10 +- src/git/git.ts | 4 +- src/git/remotes/provider.ts | 26 ++- src/gitContentProvider.ts | 8 +- src/gitRevisionCodeLensProvider.ts | 11 +- src/quickPicks/commitDetails.ts | 33 ++-- src/quickPicks/commitFileDetails.ts | 48 ++--- src/quickPicks/fileHistory.ts | 2 +- src/quickPicks/remotes.ts | 16 +- src/views/branchHistoryNode.ts | 23 ++- src/views/branchesNode.ts | 15 +- src/views/commitFileNode.ts | 22 ++- src/views/commitNode.ts | 28 ++- src/views/explorerNode.ts | 25 ++- src/views/explorerNodes.ts | 9 +- src/views/fileHistoryExplorer.ts | 89 --------- src/views/fileHistoryNode.ts | 17 +- src/views/gitExplorer.ts | 161 ++++++++++++---- src/views/historyNode.ts | 30 +++ src/views/remoteNode.ts | 35 ++++ src/views/remotesNode.ts | 35 ++++ src/views/repositoryNode.ts | 13 +- src/views/stashCommitNode.ts | 46 ----- src/views/stashExplorer.ts | 75 -------- src/views/stashFileNode.ts | 14 ++ src/views/stashNode.ts | 36 ++-- src/views/stashesNode.ts | 34 ++++ src/views/statusNode.ts | 48 +++-- src/views/statusUpstreamNode.ts | 53 ++++++ 69 files changed, 949 insertions(+), 610 deletions(-) create mode 100644 images/dark/icon-branch.svg create mode 100644 images/dark/icon-download.svg create mode 100644 images/dark/icon-history.svg create mode 100644 images/dark/icon-remote.svg create mode 100644 images/dark/icon-repo.svg create mode 100644 images/dark/icon-search.svg create mode 100644 images/dark/icon-stash.svg create mode 100644 images/dark/icon-upload.svg create mode 100644 images/light/icon-branch.svg create mode 100644 images/light/icon-download.svg create mode 100644 images/light/icon-history.svg create mode 100644 images/light/icon-remote.svg create mode 100644 images/light/icon-repo.svg create mode 100644 images/light/icon-search.svg create mode 100644 images/light/icon-stash.svg create mode 100644 images/light/icon-upload.svg delete mode 100644 src/views/fileHistoryExplorer.ts create mode 100644 src/views/historyNode.ts create mode 100644 src/views/remoteNode.ts create mode 100644 src/views/remotesNode.ts delete mode 100644 src/views/stashCommitNode.ts delete mode 100644 src/views/stashExplorer.ts create mode 100644 src/views/stashFileNode.ts create mode 100644 src/views/stashesNode.ts create mode 100644 src/views/statusUpstreamNode.ts diff --git a/images/dark/icon-add.svg b/images/dark/icon-add.svg index 3475c1e..5b11d1f 100644 --- a/images/dark/icon-add.svg +++ b/images/dark/icon-add.svg @@ -1 +1,5 @@ -Layer 1 \ No newline at end of file + + + + + \ No newline at end of file diff --git a/images/dark/icon-branch.svg b/images/dark/icon-branch.svg new file mode 100644 index 0000000..41efea1 --- /dev/null +++ b/images/dark/icon-branch.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-commit.svg b/images/dark/icon-commit.svg index 2e6dc1c..ecd6949 100644 --- a/images/dark/icon-commit.svg +++ b/images/dark/icon-commit.svg @@ -1,4 +1,4 @@ - - + + \ No newline at end of file diff --git a/images/dark/icon-download.svg b/images/dark/icon-download.svg new file mode 100644 index 0000000..12ad689 --- /dev/null +++ b/images/dark/icon-download.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-history.svg b/images/dark/icon-history.svg new file mode 100644 index 0000000..a31288d --- /dev/null +++ b/images/dark/icon-history.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-refresh.svg b/images/dark/icon-refresh.svg index c916dd2..4a3e34c 100644 --- a/images/dark/icon-refresh.svg +++ b/images/dark/icon-refresh.svg @@ -1,5 +1,4 @@ - - - + + \ No newline at end of file diff --git a/images/dark/icon-remote.svg b/images/dark/icon-remote.svg new file mode 100644 index 0000000..bf72ab2 --- /dev/null +++ b/images/dark/icon-remote.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-repo.svg b/images/dark/icon-repo.svg new file mode 100644 index 0000000..b1f8746 --- /dev/null +++ b/images/dark/icon-repo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-search.svg b/images/dark/icon-search.svg new file mode 100644 index 0000000..25db6f2 --- /dev/null +++ b/images/dark/icon-search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-stash.svg b/images/dark/icon-stash.svg new file mode 100644 index 0000000..47b80ce --- /dev/null +++ b/images/dark/icon-stash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/dark/icon-upload.svg b/images/dark/icon-upload.svg new file mode 100644 index 0000000..7520647 --- /dev/null +++ b/images/dark/icon-upload.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-add.svg b/images/light/icon-add.svg index bdecdb0..20a5a3e 100644 --- a/images/light/icon-add.svg +++ b/images/light/icon-add.svg @@ -1 +1,5 @@ -Layer 1 \ No newline at end of file + + + + + \ No newline at end of file diff --git a/images/light/icon-branch.svg b/images/light/icon-branch.svg new file mode 100644 index 0000000..c33314e --- /dev/null +++ b/images/light/icon-branch.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-commit.svg b/images/light/icon-commit.svg index 4724a89..82bf174 100644 --- a/images/light/icon-commit.svg +++ b/images/light/icon-commit.svg @@ -1,4 +1,4 @@ - - + + \ No newline at end of file diff --git a/images/light/icon-download.svg b/images/light/icon-download.svg new file mode 100644 index 0000000..f271a00 --- /dev/null +++ b/images/light/icon-download.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-history.svg b/images/light/icon-history.svg new file mode 100644 index 0000000..2851e5c --- /dev/null +++ b/images/light/icon-history.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-refresh.svg b/images/light/icon-refresh.svg index e11b8f9..f6674b3 100644 --- a/images/light/icon-refresh.svg +++ b/images/light/icon-refresh.svg @@ -1,5 +1,4 @@ - - - + + \ No newline at end of file diff --git a/images/light/icon-remote.svg b/images/light/icon-remote.svg new file mode 100644 index 0000000..da57739 --- /dev/null +++ b/images/light/icon-remote.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-repo.svg b/images/light/icon-repo.svg new file mode 100644 index 0000000..4e2368e --- /dev/null +++ b/images/light/icon-repo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-search.svg b/images/light/icon-search.svg new file mode 100644 index 0000000..a9ac03c --- /dev/null +++ b/images/light/icon-search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-stash.svg b/images/light/icon-stash.svg new file mode 100644 index 0000000..2172d4f --- /dev/null +++ b/images/light/icon-stash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-upload.svg b/images/light/icon-upload.svg new file mode 100644 index 0000000..a0bb4a8 --- /dev/null +++ b/images/light/icon-upload.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/package.json b/package.json index dee7193..949ed56 100644 --- a/package.json +++ b/package.json @@ -413,20 +413,34 @@ "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": { + "gitlens.gitExplorer.view": { + "type": "string", + "default": "repository", + "enum": [ + "history", + "repository" + ], + "description": "Specifies the starting view (mode) of the `GitLens` custom view\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer" + }, + "gitlens.gitExplorer.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" + "description": "Specifies the format of committed changes in the `GitLens` custom view\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.gitExplorer.commitFileFormat": { + "type": "string", + "default": "${filePath}", + "description": "Specifies the format of a committed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path" }, - "gitlens.stashExplorer.stashFormat": { + "gitlens.gitExplorer.stashFormat": { "type": "string", "default": "${message}", - "description": "Specifies the format of stashed changes in the `Git Stashes` 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" + "description": "Specifies the format of stashed changes in the `GitLens` custom view\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.stashFileFormat": { + "gitlens.gitExplorer.stashFileFormat": { "type": "string", "default": "${filePath}", - "description": "Specifies the format of a stashed file in the `Git Stashes` explorer\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path" + "description": "Specifies the format of a stashed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path" }, "gitlens.statusBar.enabled": { "type": "boolean", @@ -772,17 +786,17 @@ }, { "command": "gitlens.diffWithNext", - "title": "Compare File with Next Commit", + "title": "Compare File with Next Revision", "category": "GitLens" }, { "command": "gitlens.diffWithPrevious", - "title": "Compare File with Previous", + "title": "Compare File with Previous Revision", "category": "GitLens" }, { "command": "gitlens.diffLineWithPrevious", - "title": "Compare Line Commit with Previous", + "title": "Compare Line Revision with Previous", "category": "GitLens" }, { @@ -792,12 +806,12 @@ }, { "command": "gitlens.diffWithWorking", - "title": "Compare File with Working Tree", + "title": "Compare File with Working Revision", "category": "GitLens" }, { "command": "gitlens.diffLineWithWorking", - "title": "Compare Line Commit with Working Tree", + "title": "Compare Line Revision with Working", "category": "GitLens" }, { @@ -864,7 +878,11 @@ { "command": "gitlens.showCommitSearch", "title": "Search Commits", - "category": "GitLens" + "category": "GitLens", + "icon": { + "dark": "images/dark/icon-search.svg", + "light": "images/light/icon-search.svg" + } }, { "command": "gitlens.showFileHistory", @@ -985,66 +1003,56 @@ } }, { - "command": "gitlens.fileHistoryExplorer.refresh", - "title": "Refresh", + "command": "gitlens.gitExplorer.switchToHistoryView", + "title": "Switch to History View", "category": "GitLens", "icon": { - "dark": "images/dark/icon-refresh.svg", - "light": "images/light/icon-refresh.svg" + "dark": "images/dark/icon-history.svg", + "light": "images/light/icon-history.svg" } }, { - "command": "gitlens.fileHistoryExplorer.openChanges", - "title": "Open Changes", - "category": "GitLens" + "command": "gitlens.gitExplorer.switchToRepositoryView", + "title": "Switch to Repository View", + "category": "GitLens", + "icon": { + "dark": "images/dark/icon-repo.svg", + "light": "images/light/icon-repo.svg" + } }, { - "command": "gitlens.fileHistoryExplorer.openFile", - "title": "Open File", + "command": "gitlens.gitExplorer.openChanges", + "title": "Open Changes", "category": "GitLens" }, { - "command": "gitlens.fileHistoryExplorer.openFileRevision", - "title": "Open File Revision", + "command": "gitlens.gitExplorer.openChangesWithWorking", + "title": "Open Changes with Working Tree", "category": "GitLens" }, { - "command": "gitlens.fileHistoryExplorer.openFileInRemote", - "title": "Open File in Remote", + "command": "gitlens.gitExplorer.openFile", + "title": "Open File", "category": "GitLens" }, { - "command": "gitlens.fileHistoryExplorer.openFileRevisionInRemote", - "title": "Open File Revision in Remote", + "command": "gitlens.gitExplorer.openFileRevision", + "title": "Open Revision", "category": "GitLens" }, { - "command": "gitlens.stashExplorer.refresh", - "title": "Refresh", - "category": "GitLens", - "icon": { - "dark": "images/dark/icon-refresh.svg", - "light": "images/light/icon-refresh.svg" - } - }, - { - "command": "gitlens.stashExplorer.openChanges", - "title": "Open Changes", + "command": "gitlens.gitExplorer.openFileRevisionInRemote", + "title": "Open Revision in Remote", "category": "GitLens" }, { - "command": "gitlens.stashExplorer.openFile", - "title": "Open File", + "command": "gitlens.gitExplorer.openChangedFiles", + "title": "Open Files", "category": "GitLens" }, { - "command": "gitlens.stashExplorer.openStashedFile", - "title": "Open Stashed File", - "category": "GitLens" - }, - { - "command": "gitlens.stashExplorer.openFileInRemote", - "title": "Open File in Remote", + "command": "gitlens.gitExplorer.openChangedFileRevisions", + "title": "Open Revisions", "category": "GitLens" } ], @@ -1203,47 +1211,39 @@ "when": "false" }, { - "command": "gitlens.fileHistoryExplorer.refresh", - "when": "false" - }, - { - "command": "gitlens.fileHistoryExplorer.openChanges", - "when": "false" + "command": "gitlens.gitExplorer.switchToHistoryView", + "when": "gitlens:gitExplorer:view == repository" }, { - "command": "gitlens.fileHistoryExplorer.openFile", - "when": "false" + "command": "gitlens.gitExplorer.switchToRepositoryView", + "when": "gitlens:gitExplorer:view == history" }, { - "command": "gitlens.fileHistoryExplorer.openFileRevision", + "command": "gitlens.gitExplorer.openChanges", "when": "false" }, { - "command": "gitlens.fileHistoryExplorer.openFileInRemote", + "command": "gitlens.gitExplorer.openChangesWithWorking", "when": "false" }, { - "command": "gitlens.fileHistoryExplorer.openFileRevisionInRemote", + "command": "gitlens.gitExplorer.openFile", "when": "false" }, { - "command": "gitlens.stashExplorer.refresh", + "command": "gitlens.gitExplorer.openFileRevision", "when": "false" }, { - "command": "gitlens.stashExplorer.openChanges", + "command": "gitlens.gitExplorer.openFileRevisionInRemote", "when": "false" }, { - "command": "gitlens.stashExplorer.openFile", + "command": "gitlens.gitExplorer.openChangedFiles", "when": "false" }, { - "command": "gitlens.stashExplorer.openStashedFile", - "when": "false" - }, - { - "command": "gitlens.stashExplorer.openFileInRemote", + "command": "gitlens.gitExplorer.openChangedFileRevisions", "when": "false" } ], @@ -1432,116 +1432,156 @@ ], "view/title": [ { - "command": "gitlens.gitExplorer.refresh", + "command": "gitlens.showCommitSearch", "when": "gitlens:enabled && view == gitlens.gitExplorer", - "group": "navigation" + "group": "navigation@1" }, { - "command": "gitlens.fileHistoryExplorer.refresh", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer", - "group": "navigation" + "command": "gitlens.gitExplorer.switchToHistoryView", + "when": "gitlens:enabled && view == gitlens.gitExplorer && gitlens:gitExplorer:view == repository", + "group": "navigation@2" }, { - "command": "gitlens.stashSave", - "when": "gitlens:enabled && view == gitlens.stashExplorer", - "group": "navigation@1" + "command": "gitlens.gitExplorer.switchToRepositoryView", + "when": "gitlens:enabled && view == gitlens.gitExplorer && gitlens:gitExplorer:view == history", + "group": "navigation@3" }, { - "command": "gitlens.stashExplorer.refresh", - "when": "gitlens:enabled && view == gitlens.stashExplorer", - "group": "navigation@2" + "command": "gitlens.gitExplorer.refresh", + "when": "gitlens:enabled && view == gitlens.gitExplorer", + "group": "navigation@4" } ], "view/item/context": [ { - "command": "gitlens.openCommitInRemote", - "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == commit", + "command": "gitlens.openBranchInRemote", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:branch-history", "group": "1_gitlens@1" }, { - "command": "gitlens.openFileInRemote", - "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == commit-file", + "command": "gitlens.openCommitInRemote", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "group": "1_gitlens@1" }, { - "command": "gitlens.diffWithPrevious", - "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == commit-file", + "command": "gitlens.copyShaToClipboard", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "group": "2_gitlens@1" }, { - "command": "gitlens.diffWithWorking", - "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == commit-file", + "command": "gitlens.copyMessageToClipboard", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "group": "2_gitlens@2" }, { - "command": "gitlens.fileHistoryExplorer.openChanges", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "command": "gitlens.gitExplorer.openChangedFiles", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", + "group": "3_gitlens@1" + }, + { + "command": "gitlens.gitExplorer.openChangedFileRevisions", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", + "group": "3_gitlens@2" + }, + { + "command": "gitlens.showQuickCommitDetails", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", + "group": "4_gitlens@1" + }, + { + "command": "gitlens.gitExplorer.openChanges", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "group": "1_gitlens@1" }, { - "command": "gitlens.diffWithWorking", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "command": "gitlens.gitExplorer.openChangesWithWorking", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "group": "1_gitlens@2" }, { - "command": "gitlens.fileHistoryExplorer.openFile", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "command": "gitlens.gitExplorer.openFile", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "group": "2_gitlens@1" }, { - "command": "gitlens.fileHistoryExplorer.openFileRevision", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", + "command": "gitlens.gitExplorer.openFileRevision", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens: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.openFileInRemote", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", + "group": "3_gitlens@1" }, { - "command": "gitlens.fileHistoryExplorer.openFileRevisionInRemote", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", - "group": "2_gitlens@4" + "command": "gitlens.gitExplorer.openFileRevisionInRemote", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", + "group": "3_gitlens@2" }, { - "command": "gitlens.showQuickCommitDetails", - "when": "gitlens:enabled && view == gitlens.fileHistoryExplorer && viewItem == commit-file", - "group": "3_gitlens@1" + "command": "gitlens.showQuickFileHistory", + "when": "gitlens:isTracked && view == gitlens.gitExplorer && viewItem == gitlens:commit-file && gitlens:gitExplorer:view == repository", + "group": "4_gitlens@1" + }, + { + "command": "gitlens.showQuickCommitFileDetails", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", + "group": "4_gitlens@2" }, { "command": "gitlens.stashApply", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == stash-commit", - "group": "3_gitlens@1" + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", + "group": "1_gitlens@1" }, { "command": "gitlens.stashDelete", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == stash-commit", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", + "group": "1_gitlens@2" + }, + { + "command": "gitlens.copyMessageToClipboard", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", + "group": "2_gitlens@1" + }, + { + "command": "gitlens.gitExplorer.openChangedFiles", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", "group": "3_gitlens@1" }, { - "command": "gitlens.stashExplorer.openChanges", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == commit-file", + "command": "gitlens.gitExplorer.openChangedFileRevisions", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", + "group": "3_gitlens@2" + }, + { + "command": "gitlens.gitExplorer.openChanges", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", "group": "1_gitlens@1" }, { - "command": "gitlens.stashExplorer.openFile", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == commit-file", + "command": "gitlens.gitExplorer.openChangesWithWorking", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", "group": "1_gitlens@2" }, { - "command": "gitlens.stashExplorer.openStashedFile", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == commit-file", - "group": "1_gitlens@3" + "command": "gitlens.gitExplorer.openFile", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", + "group": "2_gitlens@1" }, { - "command": "gitlens.stashExplorer.openFileInRemote", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == commit-file", - "group": "1_gitlens@4" + "command": "gitlens.gitExplorer.openFileRevision", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", + "group": "2_gitlens@2" }, { - "command": "gitlens.diffWithWorking", - "when": "gitlens:enabled && view == gitlens.stashExplorer && viewItem == commit-file", - "group": "2_gitlens@2" + "command": "gitlens.openFileInRemote", + "when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", + "group": "3_gitlens@1" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "gitlens:isTracked && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", + "group": "4_gitlens@1" } ] }, @@ -1640,13 +1680,8 @@ "views": { "explorer": [ { - "id": "gitlens.fileHistoryExplorer", - "name": "Git File History", - "when": "gitlens:enabled && config.gitlens.insiders" - }, - { - "id": "gitlens.stashExplorer", - "name": "Git Stashes", + "id": "gitlens.gitExplorer", + "name": "GitLens", "when": "gitlens:enabled" } ] diff --git a/src/commands/common.ts b/src/commands/common.ts index 5b0a95a..2ef0cad 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -1,6 +1,7 @@ 'use strict'; import { commands, Disposable, SourceControlResourceGroup, SourceControlResourceState, TextDocumentShowOptions, TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; import { ExplorerNode } from '../views/explorerNodes'; +import { GitBranch, GitCommit } from '../gitService'; import { Logger } from '../logger'; import { Telemetry } from '../telemetry'; @@ -125,6 +126,18 @@ export interface CommandViewContext extends CommandBaseContext { node: ExplorerNode; } +export function isCommandViewContextWithBranch(context: CommandContext): context is CommandViewContext & { node: (ExplorerNode & { branch: GitBranch }) } { + return context.type === 'view' && (context.node as any).branch && (context.node as any).branch instanceof GitBranch; +} + +interface ICommandViewContextWithCommit extends CommandViewContext { + node: (ExplorerNode & { commit: T }); +} + +export function isCommandViewContextWithCommit(context: CommandContext): context is ICommandViewContextWithCommit { + return context.type === 'view' && (context.node as any).commit && (context.node as any).commit instanceof GitCommit; +} + export type CommandContext = CommandScmGroupsContext | CommandScmStatesContext | CommandUnknownContext | CommandUriContext | CommandViewContext; function isScmResourceGroup(group: any): group is SourceControlResourceGroup { diff --git a/src/commands/copyMessageToClipboard.ts b/src/commands/copyMessageToClipboard.ts index 141c607..e18e734 100644 --- a/src/commands/copyMessageToClipboard.ts +++ b/src/commands/copyMessageToClipboard.ts @@ -1,7 +1,7 @@ 'use strict'; import { Iterables } from '../system'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands, getCommandUri } from './common'; +import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { copy } from 'copy-paste'; @@ -17,6 +17,16 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand { super(Commands.CopyMessageToClipboard); } + protected async preExecute(context: CommandContext, args: CopyMessageToClipboardCommandArgs = {}): Promise { + if (isCommandViewContextWithCommit(context)) { + args = { ...args }; + args.sha = context.node.commit.sha; + return this.execute(context.editor, context.node.commit.uri, args); + } + + return this.execute(context.editor, context.uri, args); + } + async execute(editor?: TextEditor, uri?: Uri, args: CopyMessageToClipboardCommandArgs = {}): Promise { uri = getCommandUri(uri, editor); @@ -64,7 +74,7 @@ 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, args.sha); - if (!commit) return undefined; + if (commit === undefined) return undefined; args.message = commit.message; } diff --git a/src/commands/copyShaToClipboard.ts b/src/commands/copyShaToClipboard.ts index a47aba7..d226ff5 100644 --- a/src/commands/copyShaToClipboard.ts +++ b/src/commands/copyShaToClipboard.ts @@ -1,7 +1,7 @@ 'use strict'; import { Iterables } from '../system'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands, getCommandUri } from './common'; +import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { copy } from 'copy-paste'; @@ -16,6 +16,16 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand { super(Commands.CopyShaToClipboard); } + protected async preExecute(context: CommandContext, args: CopyShaToClipboardCommandArgs = {}): Promise { + if (isCommandViewContextWithCommit(context)) { + args = { ...args }; + args.sha = context.node.commit.sha; + return this.execute(context.editor, context.node.commit.uri, args); + } + + return this.execute(context.editor, context.uri, args); + } + async execute(editor?: TextEditor, uri?: Uri, args: CopyShaToClipboardCommandArgs = {}): Promise { uri = getCommandUri(uri, editor); @@ -45,7 +55,7 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand { try { const blame = await this.git.getBlameForLine(gitUri, blameline); - if (!blame) return undefined; + if (blame === undefined) return undefined; args.sha = blame.commit.sha; } diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index c7ea727..564587e 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -2,7 +2,7 @@ import { Iterables } from '../system'; import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { ActiveEditorCommand, Commands, getCommandUri } from './common'; -import { BuiltInCommands, GlyphChars } from '../constants'; +import { BuiltInCommands, FakeSha, GlyphChars } from '../constants'; import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { GitCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; @@ -14,6 +14,10 @@ export interface DiffWithPreviousCommandArgs { line?: number; range?: Range; showOptions?: TextDocumentShowOptions; + + allowMissingPrevious?: boolean; + leftTitlePrefix?: string; + rightTitlePrefix?: string; } export class DiffWithPreviousCommand extends ActiveEditorCommand { @@ -51,12 +55,12 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand { } } - if (args.commit.previousSha === undefined) return Messages.showCommitHasNoPreviousCommitWarningMessage(args.commit); + if (args.commit.previousSha === undefined && !args.allowMissingPrevious) return Messages.showCommitHasNoPreviousCommitWarningMessage(args.commit); try { const [rhs, lhs] = await Promise.all([ 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) + this.git.getVersionedFile(args.commit.repoPath, args.commit.previousUri.fsPath, args.commit.previousSha === undefined ? FakeSha : args.commit.previousSha) ]); if (args.line !== undefined && args.line !== 0) { @@ -69,7 +73,9 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand { await commands.executeCommand(BuiltInCommands.Diff, Uri.file(lhs), Uri.file(rhs), - `${path.basename(args.commit.previousUri.fsPath)} (${args.commit.previousShortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha})`, + args.commit.previousShortSha === undefined + ? `${path.basename(args.commit.uri.fsPath)} (${args.rightTitlePrefix || ''}${args.commit.shortSha})` + : `${path.basename(args.commit.previousUri.fsPath)} (${args.leftTitlePrefix || ''}${args.commit.previousShortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(args.commit.uri.fsPath)} (${args.rightTitlePrefix || ''}${args.commit.shortSha})`, args.showOptions); } catch (ex) { diff --git a/src/commands/openBranchInRemote.ts b/src/commands/openBranchInRemote.ts index 98b6834..8d4486e 100644 --- a/src/commands/openBranchInRemote.ts +++ b/src/commands/openBranchInRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { Arrays } from '../system'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands, getCommandUri } from './common'; +import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithBranch } from './common'; import { GlyphChars } from '../constants'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; @@ -18,6 +18,16 @@ export class OpenBranchInRemoteCommand extends ActiveEditorCommand { super(Commands.OpenBranchInRemote); } + protected async preExecute(context: CommandContext, args: OpenBranchInRemoteCommandArgs = {}): Promise { + if (isCommandViewContextWithBranch(context)) { + args = { ...args }; + args.branch = context.node.branch.name; + return this.execute(context.editor, context.uri, args); + } + + return this.execute(context.editor, context.uri, args); + } + async execute(editor?: TextEditor, uri?: Uri, args: OpenBranchInRemoteCommandArgs = {}) { uri = getCommandUri(uri, editor); diff --git a/src/commands/openCommitInRemote.ts b/src/commands/openCommitInRemote.ts index 707b005..efae8b9 100644 --- a/src/commands/openCommitInRemote.ts +++ b/src/commands/openCommitInRemote.ts @@ -1,12 +1,11 @@ 'use strict'; import { Arrays } from '../system'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, CommandContext, Commands, getCommandUri } from './common'; +import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { GitBlameCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { OpenInRemoteCommandArgs } from './openInRemote'; -import { CommitNode } from '../views/explorerNodes'; export interface OpenCommitInRemoteCommandArgs { sha?: string; @@ -19,7 +18,7 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand { } protected async preExecute(context: CommandContext, args: OpenCommitInRemoteCommandArgs = {}): Promise { - if (context.type === 'view' && context.node instanceof CommitNode) { + if (isCommandViewContextWithCommit(context)) { args = { ...args }; args.sha = context.node.commit.sha; return this.execute(context.editor, context.node.commit.uri, args); diff --git a/src/commands/openFileInRemote.ts b/src/commands/openFileInRemote.ts index 03624b1..0d2ad06 100644 --- a/src/commands/openFileInRemote.ts +++ b/src/commands/openFileInRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { Arrays } from '../system'; import { commands, Range, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCommand, Commands, getCommandUri } from './common'; +import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { OpenInRemoteCommandArgs } from './openInRemote'; @@ -16,6 +16,16 @@ export class OpenFileInRemoteCommand extends ActiveEditorCommand { super(Commands.OpenFileInRemote); } + protected async preExecute(context: CommandContext, args: OpenFileInRemoteCommandArgs = {}): Promise { + if (isCommandViewContextWithCommit(context)) { + args = { ...args }; + args.range = false; + return this.execute(context.editor, context.node.commit.uri, args); + } + + return this.execute(context.editor, context.uri, args); + } + async execute(editor?: TextEditor, uri?: Uri, args: OpenFileInRemoteCommandArgs = { range: true }) { uri = getCommandUri(uri, editor); if (uri === undefined) return undefined; diff --git a/src/commands/openInRemote.ts b/src/commands/openInRemote.ts index 8c279fe..a5bc29a 100644 --- a/src/commands/openInRemote.ts +++ b/src/commands/openInRemote.ts @@ -28,6 +28,7 @@ export class OpenInRemoteCommand extends ActiveEditorCommand { try { if (args.remotes.length === 1) { + this.ensureRemoteBranchName(args); const command = new OpenRemoteCommandQuickPickItem(args.remotes[0], args.resource); return command.execute(); } @@ -35,16 +36,7 @@ export class OpenInRemoteCommand extends ActiveEditorCommand { let placeHolder = ''; switch (args.resource.type) { case 'branch': - // Check to see if the remote is in the branch - const index = args.resource.branch.indexOf('/'); - if (index >= 0) { - const remoteName = args.resource.branch.substring(0, index); - const remote = args.remotes.find(r => r.name === remoteName); - if (remote !== undefined) { - args.resource.branch = args.resource.branch.substring(index + 1); - args.remotes = [remote]; - } - } + this.ensureRemoteBranchName(args); placeHolder = `open ${args.resource.branch} branch in${GlyphChars.Ellipsis}`; break; @@ -54,6 +46,10 @@ export class OpenInRemoteCommand extends ActiveEditorCommand { break; case 'file': + placeHolder = `open ${args.resource.fileName} in${GlyphChars.Ellipsis}`; + break; + + case 'revision': if (args.resource.commit !== undefined && args.resource.commit instanceof GitLogCommit) { if (args.resource.commit.status === 'D') { args.resource.sha = args.resource.commit.previousSha; @@ -71,10 +67,6 @@ export class OpenInRemoteCommand extends ActiveEditorCommand { placeHolder = `open ${args.resource.fileName}${shaSuffix} in${GlyphChars.Ellipsis}`; } break; - - case 'working-file': - placeHolder = `open ${args.resource.fileName} in${GlyphChars.Ellipsis}`; - break; } if (args.remotes.length === 1) { @@ -93,4 +85,19 @@ export class OpenInRemoteCommand extends ActiveEditorCommand { return window.showErrorMessage(`Unable to open in remote provider. See output channel for more details`); } } + + private ensureRemoteBranchName(args: OpenInRemoteCommandArgs) { + if (args.remotes === undefined || args.resource === undefined || args.resource.type !== 'branch') return; + + // Check to see if the remote is in the branch + const index = args.resource.branch.indexOf('/'); + if (index >= 0) { + const remoteName = args.resource.branch.substring(0, index); + const remote = args.remotes.find(r => r.name === remoteName); + if (remote !== undefined) { + args.resource.branch = args.resource.branch.substring(index + 1); + args.remotes = [remote]; + } + } + } } \ No newline at end of file diff --git a/src/commands/showCommitSearch.ts b/src/commands/showCommitSearch.ts index 8ded360..68f6f6a 100644 --- a/src/commands/showCommitSearch.ts +++ b/src/commands/showCommitSearch.ts @@ -8,7 +8,6 @@ import { Logger } from '../logger'; import { Messages } from '../messages'; import { CommandQuickPickItem, CommitsQuickPick } from '../quickPicks'; import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; -import { paste } from 'copy-paste'; const searchByRegex = /^([@:#])/; const searchByMap = new Map([ @@ -49,12 +48,6 @@ export class ShowCommitSearchCommand extends ActiveEditorCachedCommand { args.search = `#${blameLine.commit.shortSha}`; } } - - if (!args.search) { - args.search = await new Promise((resolve, reject) => { - paste((err: Error, content: string) => resolve(err ? '' : content)); - }); - } } } catch (ex) { diff --git a/src/commands/showFileHistory.ts b/src/commands/showFileHistory.ts index 19df42b..9b898b0 100644 --- a/src/commands/showFileHistory.ts +++ b/src/commands/showFileHistory.ts @@ -2,7 +2,7 @@ import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { Commands, EditorCommand, getCommandUri } from './common'; import { BuiltInCommands } from '../constants'; -import { GitExplorer } from '../views/gitExplorer'; +// import { GitExplorer } from '../views/gitExplorer'; import { GitService, GitUri } from '../gitService'; import { Messages } from '../messages'; import { Logger } from '../logger'; @@ -15,7 +15,7 @@ export interface ShowFileHistoryCommandArgs { export class ShowFileHistoryCommand extends EditorCommand { - constructor(private git: GitService, private explorer?: GitExplorer) { + constructor(private git: GitService) { super(Commands.ShowFileHistory); } @@ -33,10 +33,10 @@ export class ShowFileHistoryCommand extends EditorCommand { const gitUri = await GitUri.fromUri(uri, this.git); try { - if (this.explorer !== undefined) { - this.explorer.addHistory(gitUri); - return undefined; - } + // if (this.explorer !== undefined) { + // this.explorer.addHistory(gitUri); + // return undefined; + // } const locations = await this.git.getLogLocations(gitUri, args.sha, args.line); if (locations === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show file history'); diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index 2015a53..bc2e67f 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -1,9 +1,8 @@ 'use strict'; import { Strings } from '../system'; import { commands, TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, CommandContext, Commands, getCommandUri } from './common'; +import { ActiveEditorCachedCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } 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'; @@ -27,7 +26,7 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { protected async preExecute(context: CommandContext, ...args: any[]): Promise { if (context.type === 'view') { - if (context.node instanceof CommitNode) { + if (isCommandViewContextWithCommit(context)) { args = [{ sha: context.node.uri.sha, commit: context.node.commit }]; } else { diff --git a/src/commands/showQuickCommitFileDetails.ts b/src/commands/showQuickCommitFileDetails.ts index 1261c78..f2fb799 100644 --- a/src/commands/showQuickCommitFileDetails.ts +++ b/src/commands/showQuickCommitFileDetails.ts @@ -1,7 +1,7 @@ 'use strict'; import { Strings } from '../system'; import { TextEditor, Uri, window } from 'vscode'; -import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; +import { ActiveEditorCachedCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { GlyphChars } from '../constants'; import { GitCommit, GitLog, GitLogCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; @@ -24,6 +24,18 @@ export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand super(Commands.ShowQuickCommitFileDetails); } + protected async preExecute(context: CommandContext, ...args: any[]): Promise { + if (context.type === 'view') { + if (isCommandViewContextWithCommit(context)) { + 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: ShowQuickCommitFileDetailsCommandArgs = {}) { uri = getCommandUri(uri, editor); if (uri === undefined) return undefined; diff --git a/src/commands/stashApply.ts b/src/commands/stashApply.ts index 007355a..39ce5d8 100644 --- a/src/commands/stashApply.ts +++ b/src/commands/stashApply.ts @@ -1,13 +1,11 @@ 'use strict'; import { Strings } from '../system'; import { MessageItem, window } from 'vscode'; -import { GitService, GitStashCommit } from '../gitService'; -import { Command, CommandContext, Commands } from './common'; +import { Command, CommandContext, Commands, isCommandViewContextWithCommit } from './common'; import { GlyphChars } from '../constants'; -import { CommitQuickPickItem, StashListQuickPick } from '../quickPicks'; +import { GitService, GitStashCommit } from '../gitService'; import { Logger } from '../logger'; -import { CommandQuickPickItem } from '../quickPicks'; -import { StashCommitNode } from '../views/stashCommitNode'; +import { CommandQuickPickItem, CommitQuickPickItem, StashListQuickPick } from '../quickPicks'; export interface StashApplyCommandArgs { confirm?: boolean; @@ -24,12 +22,9 @@ export class StashApplyCommand extends Command { } protected async preExecute(context: CommandContext, args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) { - if (context.type === 'view' && context.node instanceof StashCommitNode) { + if (isCommandViewContextWithCommit(context)) { args = { ...args }; - - const stash = context.node.commit; - args.stashItem = { stashName: stash.stashName, message: stash.message }; - + args.stashItem = { stashName: context.node.commit.stashName, message: context.node.commit.message }; return this.execute(args); } diff --git a/src/commands/stashDelete.ts b/src/commands/stashDelete.ts index bc45153..9d447c3 100644 --- a/src/commands/stashDelete.ts +++ b/src/commands/stashDelete.ts @@ -1,11 +1,10 @@ 'use strict'; import { MessageItem, window } from 'vscode'; -import { Command, CommandContext, Commands } from './common'; +import { Command, CommandContext, Commands, isCommandViewContextWithCommit } from './common'; import { GlyphChars } from '../constants'; -import { GitService } from '../gitService'; +import { GitService, GitStashCommit } from '../gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem } from '../quickPicks'; -import { StashCommitNode } from '../views/stashCommitNode'; export interface StashDeleteCommandArgs { confirm?: boolean; @@ -21,12 +20,9 @@ export class StashDeleteCommand extends Command { } protected async preExecute(context: CommandContext, args: StashDeleteCommandArgs = { confirm: true }) { - if (context.type === 'view' && context.node instanceof StashCommitNode) { + if (isCommandViewContextWithCommit(context)) { args = { ...args }; - - const stash = context.node.commit; - args.stashItem = { stashName: stash.stashName, message: stash.message }; - + args.stashItem = { stashName: context.node.commit.stashName, message: context.node.commit.message }; return this.execute(args); } diff --git a/src/configuration.ts b/src/configuration.ts index fee3e17..c2b17fc 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -2,6 +2,7 @@ import { FileAnnotationType } from './annotations/annotationController'; import { Commands } from './commands'; import { LineAnnotationType } from './currentLineController'; +import { GitExplorerView } from './views/gitExplorer'; import { OutputLevel } from './logger'; export { ExtensionKey } from './constants'; @@ -296,19 +297,10 @@ export interface IConfig { defaultDateFormat: string | null; - fileHistoryExplorer: { - commitFormat: string; - // commitFileFormat: string; - // dateFormat: string | null; - }; - gitExplorer: { + view: GitExplorerView; commitFormat: string; commitFileFormat: string; - // dateFormat: string | null; - }; - - stashExplorer: { stashFormat: string; stashFileFormat: string; // dateFormat: string | null; diff --git a/src/constants.ts b/src/constants.ts index 83690f5..1c48be5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,6 +8,8 @@ export const QualifiedExtensionId = `eamodio.${ExtensionId}`; export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679'; +export const FakeSha = 'ffffffffffffffffffffffffffffffffffffffff'; + export type BuiltInCommands = 'cursorMove' | 'editor.action.showReferences' | 'editor.action.toggleRenderWhitespace' | @@ -40,23 +42,25 @@ export const BuiltInCommands = { }; export type CommandContext = + 'gitlens:annotationStatus' | 'gitlens:canToggleCodeLens' | 'gitlens:enabled' | 'gitlens:hasRemotes' | + 'gitlens:gitExplorer:view' | 'gitlens:isBlameable' | 'gitlens:isRepository' | 'gitlens:isTracked' | - 'gitlens:key' | - 'gitlens:annotationStatus'; + 'gitlens:key'; export const CommandContext = { + AnnotationStatus: 'gitlens:annotationStatus' as CommandContext, CanToggleCodeLens: 'gitlens:canToggleCodeLens' as CommandContext, Enabled: 'gitlens:enabled' as CommandContext, + GitExplorerView: 'gitlens:gitExplorer:view' as CommandContext, HasRemotes: 'gitlens:hasRemotes' as CommandContext, IsBlameable: 'gitlens:isBlameable' as CommandContext, IsRepository: 'gitlens:isRepository' as CommandContext, IsTracked: 'gitlens:isTracked' as CommandContext, - Key: 'gitlens:key' as CommandContext, - AnnotationStatus: 'gitlens:annotationStatus' as CommandContext + Key: 'gitlens:key' as CommandContext }; export function setCommandContext(key: CommandContext | string, value: any) { @@ -77,6 +81,7 @@ export type GlyphChars = '\u21a9' | '\u2194' | '\u21e8' | '\u2191' | + '\u2713' | '\u2014' | '\u2022' | '\u2026' | @@ -90,6 +95,7 @@ export const GlyphChars = { ArrowLeftRight: '\u2194' as GlyphChars, ArrowRightHollow: '\u21e8' as GlyphChars, ArrowUp: '\u2191' as GlyphChars, + Check: '\u2713' as GlyphChars, Dash: '\u2014' as GlyphChars, Dot: '\u2022' as GlyphChars, Ellipsis: '\u2026' as GlyphChars, diff --git a/src/currentLineController.ts b/src/currentLineController.ts index 838cd01..52f1e2d 100644 --- a/src/currentLineController.ts +++ b/src/currentLineController.ts @@ -476,11 +476,11 @@ export class CurrentLineController extends Disposable { break; case StatusBarCommand.DiffWithPrevious: this._statusBarItem.command = Commands.DiffLineWithPrevious; - this._statusBarItem.tooltip = 'Compare Line Commit with Previous'; + this._statusBarItem.tooltip = 'Compare Line Revision with Previous'; break; case StatusBarCommand.DiffWithWorking: this._statusBarItem.command = Commands.DiffLineWithWorking; - this._statusBarItem.tooltip = 'Compare Line Commit with Working Tree'; + this._statusBarItem.tooltip = 'Compare Line Revision with Working'; break; case StatusBarCommand.ToggleCodeLens: this._statusBarItem.tooltip = 'Toggle Git CodeLens'; diff --git a/src/extension.ts b/src/extension.ts index f3f92eb..5164786 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,9 +20,7 @@ import { ApplicationInsightsKey, CommandContext, ExtensionKey, QualifiedExtensio 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 { GitExplorer } from './views/gitExplorer'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; import { GitContextTracker, GitService } from './gitService'; import { Keyboard } from './keyboard'; @@ -94,11 +92,7 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new Keyboard()); - // 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(window.registerTreeDataProvider('gitlens.gitExplorer', new GitExplorer(context, git))); context.subscriptions.push(commands.registerTextEditorCommand('gitlens.computingFileAnnotations', () => { })); diff --git a/src/git/git.ts b/src/git/git.ts index e22d7d9..98ef823 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -31,7 +31,9 @@ const GitWarnings = [ /Not a git repository/, /is outside repository/, /no such path/, - /does not have any commits/ + /does not have any commits/, + /Path \'.*?\' does not exist in/, + /Path \'.*?\' exists on disk, but not in/ ]; async function gitCommand(options: { cwd: string, encoding?: string }, ...args: any[]) { diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index b4a48f8..c17f5a1 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -3,12 +3,13 @@ import { commands, Range, Uri } from 'vscode'; import { BuiltInCommands } from '../../constants'; import { GitLogCommit } from '../../gitService'; -export type RemoteResourceType = 'branch' | 'commit' | 'file' | 'repo' | 'working-file'; -export type RemoteResource = { type: 'branch', branch: string } | +export type RemoteResourceType = 'branch' | 'commit' | 'file' | 'repo' | 'revision'; +export type RemoteResource = + { type: 'branch', branch: string } | { type: 'commit', sha: string } | - { type: 'file', branch?: string, commit?: GitLogCommit, fileName: string, range?: Range, sha?: string } | + { type: 'file', branch?: string, fileName: string, range?: Range } | { type: 'repo' } | - { type: 'working-file', branch?: string, fileName: string, range?: Range }; + { type: 'revision', branch?: string, commit?: GitLogCommit, fileName: string, range?: Range, sha?: string }; export function getNameFromRemoteResource(resource: RemoteResource) { switch (resource.type) { @@ -16,7 +17,7 @@ export function getNameFromRemoteResource(resource: RemoteResource) { case 'commit': return 'Commit'; case 'file': return 'File'; case 'repo': return 'Repository'; - case 'working-file': return 'Working File'; + case 'revision': return 'Revision'; default: return ''; } } @@ -43,16 +44,11 @@ export abstract class RemoteProvider { open(resource: RemoteResource): Promise<{} | undefined> { switch (resource.type) { - case 'branch': - return this.openBranch(resource.branch); - case 'commit': - return this.openCommit(resource.sha); - case 'file': - return this.openFile(resource.fileName, resource.branch, resource.sha, resource.range); - case 'repo': - return this.openRepo(); - case 'working-file': - return this.openFile(resource.fileName, resource.branch, undefined, resource.range); + case 'branch': return this.openBranch(resource.branch); + case 'commit': return this.openCommit(resource.sha); + case 'file': return this.openFile(resource.fileName, resource.branch, undefined, resource.range); + case 'repo': return this.openRepo(); + case 'revision': return this.openFile(resource.fileName, resource.branch, resource.sha, resource.range); } } diff --git a/src/gitContentProvider.ts b/src/gitContentProvider.ts index 19ae9ea..1853f87 100644 --- a/src/gitContentProvider.ts +++ b/src/gitContentProvider.ts @@ -1,5 +1,5 @@ 'use strict'; -import { ExtensionContext, TextDocumentContentProvider, Uri, window } from 'vscode'; +import { CancellationToken, ExtensionContext, TextDocumentContentProvider, Uri, window } from 'vscode'; import { DocumentSchemes } from './constants'; import { GitService } from './gitService'; import { Logger } from './logger'; @@ -11,7 +11,7 @@ export class GitContentProvider implements TextDocumentContentProvider { constructor(context: ExtensionContext, private git: GitService) { } - async provideTextDocumentContent(uri: Uri): Promise { + async provideTextDocumentContent(uri: Uri, token: CancellationToken): Promise { const data = GitService.fromGitContentUri(uri); const fileName = data.originalFileName || data.fileName; try { @@ -23,8 +23,8 @@ export class GitContentProvider implements TextDocumentContentProvider { } catch (ex) { Logger.error(ex, 'GitContentProvider', 'getVersionedFileText'); - await window.showErrorMessage(`Unable to show Git revision ${data.sha.substring(0, 8)} of '${path.relative(data.repoPath, fileName)}'`); - return ''; + window.showErrorMessage(`Unable to show Git revision ${data.sha.substring(0, 8)} of '${path.relative(data.repoPath, fileName)}'`); + return undefined; } } } \ No newline at end of file diff --git a/src/gitRevisionCodeLensProvider.ts b/src/gitRevisionCodeLensProvider.ts index 6658b4c..0bbd3af 100644 --- a/src/gitRevisionCodeLensProvider.ts +++ b/src/gitRevisionCodeLensProvider.ts @@ -32,13 +32,12 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { const lenses: CodeLens[] = []; const commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { firstIfMissing: true, previous: true }); - if (!commit) return lenses; - - lenses.push(new GitDiffWithWorkingCodeLens(this.git, commit.uri.fsPath, commit, new Range(0, 0, 0, 1))); + if (commit === undefined) return lenses; if (commit.previousSha) { - lenses.push(new GitDiffWithPreviousCodeLens(this.git, commit.previousUri.fsPath, commit, new Range(0, 1, 0, 2))); + lenses.push(new GitDiffWithPreviousCodeLens(this.git, commit.previousUri.fsPath, commit, new Range(0, 0, 0, 1))); } + lenses.push(new GitDiffWithWorkingCodeLens(this.git, commit.uri.fsPath, commit, new Range(0, 1, 0, 2))); return lenses; } @@ -51,7 +50,7 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { _resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingCodeLens, token: CancellationToken): Thenable { lens.command = { - title: `Compare ${lens.commit.shortSha} with Working Tree`, + title: `Compare Revision (${lens.commit.shortSha}) with Working`, command: Commands.DiffWithWorking, arguments: [ Uri.file(lens.fileName), @@ -66,7 +65,7 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider { _resolveGitDiffWithPreviousCodeLens(lens: GitDiffWithPreviousCodeLens, token: CancellationToken): Thenable { lens.command = { - title: `Compare ${lens.commit.shortSha} with Previous ${lens.commit.previousShortSha}`, + title: `Compare Revision (${lens.commit.shortSha}) with Previous (${lens.commit.previousShortSha})`, command: Commands.DiffWithPrevious, arguments: [ Uri.file(lens.fileName), diff --git a/src/quickPicks/commitDetails.ts b/src/quickPicks/commitDetails.ts index 722dd1b..ea0b38a 100644 --- a/src/quickPicks/commitDetails.ts +++ b/src/quickPicks/commitDetails.ts @@ -69,28 +69,31 @@ export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickI export class OpenCommitFilesCommandQuickPickItem extends OpenFilesCommandQuickPickItem { - constructor(commit: GitLogCommit, item?: QuickPickItem) { - const uris = commit.fileStatuses.map(s => (s.status === 'D') - ? GitService.toGitContentUri(commit.previousSha!, commit.previousShortSha!, s.fileName, commit.repoPath, s.originalFileName) - : GitService.toGitContentUri(commit.sha, commit.shortSha, s.fileName, commit.repoPath, s.originalFileName)); + constructor(commit: GitLogCommit, versioned: boolean = false, item?: QuickPickItem) { + const repoPath = commit.repoPath; + const uris = commit.fileStatuses + .filter(s => s.status !== 'D') + .map(s => GitUri.fromFileStatus(s, repoPath)); super(uris, item || { label: `$(file-symlink-file) Open Changed Files`, - description: `${Strings.pad(GlyphChars.Dash, 2, 3)} in ${GlyphChars.Space}$(git-commit) ${commit.shortSha}` - // detail: `Opens all of the changed files in $(git-commit) ${commit.shortSha}` + description: '' + // detail: `Opens all of the changed file in the working tree` }); } } -export class OpenCommitWorkingTreeFilesCommandQuickPickItem extends OpenFilesCommandQuickPickItem { +export class OpenCommitFileRevisionsCommandQuickPickItem extends OpenFilesCommandQuickPickItem { + + constructor(commit: GitLogCommit, item?: QuickPickItem) { + const uris = commit.fileStatuses + .filter(s => s.status !== 'D') + .map(s => GitService.toGitContentUri(commit.sha, commit.shortSha, s.fileName, commit.repoPath, s.originalFileName)); - constructor(commit: GitLogCommit, versioned: boolean = false, item?: QuickPickItem) { - const repoPath = commit.repoPath; - const uris = commit.fileStatuses.filter(_ => _.status !== 'D').map(_ => GitUri.fromFileStatus(_, repoPath)); super(uris, item || { - label: `$(file-symlink-file) Open Changed Working Files`, - description: '' - // detail: `Opens all of the changed file in the working tree` + label: `$(file-symlink-file) Open Changed Revisions`, + description: `${Strings.pad(GlyphChars.Dash, 2, 3)} in ${GlyphChars.Space}$(git-commit) ${commit.shortSha}` + // detail: `Opens all of the changed files in $(git-commit) ${commit.shortSha}` }); } } @@ -196,8 +199,8 @@ export class CommitDetailsQuickPick { } as ShowQuickCommitDetailsCommandArgs ])); - items.push(new OpenCommitFilesCommandQuickPickItem(commit)); - items.push(new OpenCommitWorkingTreeFilesCommandQuickPickItem(commit)); + items.push(new OpenCommitFilesCommandQuickPickItem(commit)); + items.push(new OpenCommitFileRevisionsCommandQuickPickItem(commit)); if (goBackCommand) { items.splice(0, 0, goBackCommand); diff --git a/src/quickPicks/commitFileDetails.ts b/src/quickPicks/commitFileDetails.ts index 95ed618..91e4c47 100644 --- a/src/quickPicks/commitFileDetails.ts +++ b/src/quickPicks/commitFileDetails.ts @@ -13,6 +13,17 @@ import * as path from 'path'; export class OpenCommitFileCommandQuickPickItem extends OpenFileCommandQuickPickItem { constructor(commit: GitLogCommit, item?: QuickPickItem) { + const uri = Uri.file(path.resolve(commit.repoPath, commit.fileName)); + super(uri, item || { + label: `$(file-symlink-file) Open File`, + description: `${Strings.pad(GlyphChars.Dash, 2, 3)} ${path.basename(commit.fileName)}` + }); + } +} + +export class OpenCommitFileRevisionCommandQuickPickItem extends OpenFileCommandQuickPickItem { + + constructor(commit: GitLogCommit, item?: QuickPickItem) { let description: string; let uri: Uri; if (commit.status === 'D') { @@ -24,23 +35,12 @@ export class OpenCommitFileCommandQuickPickItem extends OpenFileCommandQuickPick description = `${Strings.pad(GlyphChars.Dash, 2, 3)} ${path.basename(commit.fileName)} in ${GlyphChars.Space}$(git-commit) ${commit.shortSha}`; } super(uri, item || { - label: `$(file-symlink-file) Open File`, + label: `$(file-symlink-file) Open Revision`, description: description }); } } -export class OpenCommitWorkingTreeFileCommandQuickPickItem extends OpenFileCommandQuickPickItem { - - constructor(commit: GitLogCommit, item?: QuickPickItem) { - const uri = Uri.file(path.resolve(commit.repoPath, commit.fileName)); - super(uri, item || { - label: `$(file-symlink-file) Open Working File`, - description: `${Strings.pad(GlyphChars.Dash, 2, 3)} ${path.basename(commit.fileName)}` - }); - } -} - export class CommitFileDetailsQuickPick { static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, fileLog?: GitLog): Promise { @@ -74,7 +74,7 @@ export class CommitFileDetailsQuickPick { if (commit.previousSha) { items.push(new CommandQuickPickItem({ - label: `$(git-compare) Compare File with Previous`, + label: `$(git-compare) Compare File with Previous Revision`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} $(git-commit) ${commit.previousShortSha} ${GlyphChars.Space} $(git-compare) ${GlyphChars.Space} $(git-commit) ${commit.shortSha}` }, Commands.DiffWithPrevious, [ commit.uri, @@ -87,7 +87,7 @@ export class CommitFileDetailsQuickPick { if (commit.workingFileName) { items.push(new CommandQuickPickItem({ - label: `$(git-compare) Compare File with Working Tree`, + label: `$(git-compare) Compare File with Working Revision`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} $(git-commit) ${commit.shortSha} ${GlyphChars.Space} $(git-compare) ${GlyphChars.Space} $(file-text) ${workingName}` }, Commands.DiffWithWorking, [ Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), @@ -120,28 +120,28 @@ export class CommitFileDetailsQuickPick { ])); } - items.push(new OpenCommitFileCommandQuickPickItem(commit)); if (commit.workingFileName && commit.status !== 'D') { - items.push(new OpenCommitWorkingTreeFileCommandQuickPickItem(commit)); + items.push(new OpenCommitFileCommandQuickPickItem(commit)); } + items.push(new OpenCommitFileRevisionCommandQuickPickItem(commit)); const remotes = Arrays.uniqueBy(await git.getRemotes(commit.repoPath), _ => _.url, _ => !!_.provider); if (remotes.length) { - if (!stash) { - 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, { - type: 'working-file', + type: 'file', fileName: commit.workingFileName, branch: branch.name } as RemoteResource, currentCommand)); } + if (!stash) { + items.push(new OpenRemotesCommandQuickPickItem(remotes, { + type: 'revision', + fileName: commit.fileName, + commit + } as RemoteResource, currentCommand)); + } } if (commit.workingFileName) { diff --git a/src/quickPicks/fileHistory.ts b/src/quickPicks/fileHistory.ts index 709c9c1..d2d5216 100644 --- a/src/quickPicks/fileHistory.ts +++ b/src/quickPicks/fileHistory.ts @@ -139,7 +139,7 @@ export class FileHistoryQuickPick { const remotes = Arrays.uniqueBy(await git.getRemotes(uri.repoPath!), _ => _.url, _ => !!_.provider); if (remotes.length) { items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, { - type: 'file', + type: 'revision', branch: branch!.name, fileName: uri.getRelativePath(), sha: uri.sha diff --git a/src/quickPicks/remotes.ts b/src/quickPicks/remotes.ts index 01464b4..7777b74 100644 --- a/src/quickPicks/remotes.ts +++ b/src/quickPicks/remotes.ts @@ -44,6 +44,14 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { break; case 'file': + description = `$(file-text) ${path.basename(resource.fileName)}`; + break; + + case 'repo': + description = `$(repo) Repository`; + break; + + case 'revision': if (resource.commit !== undefined && resource.commit instanceof GitLogCommit) { if (resource.commit.status === 'D') { resource.sha = resource.commit.previousSha; @@ -59,14 +67,6 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { description = `$(file-text) ${path.basename(resource.fileName)}${shortFileSha ? ` in ${GlyphChars.Space}$(git-commit) ${shortFileSha}` : ''}`; } break; - - case 'repo': - description = `$(repo) Repository`; - break; - - case 'working-file': - description = `$(file-text) ${path.basename(resource.fileName)}`; - break; } const remote = remotes[0]; diff --git a/src/views/branchHistoryNode.ts b/src/views/branchHistoryNode.ts index 58f4aec..266bd66 100644 --- a/src/views/branchHistoryNode.ts +++ b/src/views/branchHistoryNode.ts @@ -4,26 +4,35 @@ import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { CommitNode } from './commitNode'; import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitBranch, GitService, GitUri } from '../gitService'; +import { GitBranch, GitRemote, GitService, GitUri } from '../gitService'; export class BranchHistoryNode extends ExplorerNode { - readonly resourceType: ResourceType = 'branch-history'; + readonly resourceType: ResourceType = 'gitlens:branch-history'; - constructor(public readonly branch: GitBranch, uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor(public readonly branch: GitBranch, private readonly remote: GitRemote | undefined, uri: GitUri, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(uri); } - async getChildren(): Promise { + async getChildren(): Promise { const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name); if (log === undefined) return []; - 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.template, this.context, this.git))]; } - getTreeItem(): TreeItem { - const item = new TreeItem(`${this.branch.name}${this.branch.current ? ` ${GlyphChars.Dash} current` : ''}`, TreeItemCollapsibleState.Collapsed); + async getTreeItem(): Promise { + const name = this.remote !== undefined + ? this.branch.name.substring(this.remote.name.length + 1) + : this.branch.name; + const item = new TreeItem(`${name}${this.branch!.current ? ` ${GlyphChars.Space} ${GlyphChars.Check}` : ''}`, TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'), + light: this.context.asAbsolutePath('images/light/icon-branch.svg') + }; + return item; } } diff --git a/src/views/branchesNode.ts b/src/views/branchesNode.ts index 4cc3e87..897668f 100644 --- a/src/views/branchesNode.ts +++ b/src/views/branchesNode.ts @@ -7,22 +7,29 @@ import { GitService, GitUri } from '../gitService'; export class BranchesNode extends ExplorerNode { - readonly resourceType: ResourceType = 'branches'; + readonly resourceType: ResourceType = 'gitlens:branches'; constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(uri); } - async getChildren(): Promise { + async getChildren(): Promise { const branches = await this.git.getBranches(this.uri.repoPath!); if (branches === undefined) return []; - return [...Iterables.filterMap(branches.sort(_ => _.current ? 0 : 1), b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))]; + branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name)); + return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, undefined, this.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; } getTreeItem(): TreeItem { - const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Collapsed); + const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Expanded); item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'), + light: this.context.asAbsolutePath('images/light/icon-branch.svg') + }; + return item; } } diff --git a/src/views/commitFileNode.ts b/src/views/commitFileNode.ts index ddfba5f..137e3c6 100644 --- a/src/views/commitFileNode.ts +++ b/src/views/commitFileNode.ts @@ -7,9 +7,9 @@ import * as path from 'path'; export class CommitFileNode extends ExplorerNode { - readonly resourceType: ResourceType = 'commit-file'; + readonly resourceType: ResourceType = 'gitlens:commit-file'; - constructor(public readonly status: IGitStatusFile, public commit: GitCommit, private template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor(public readonly status: IGitStatusFile, public commit: GitCommit, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha })); } @@ -25,7 +25,7 @@ export class CommitFileNode extends ExplorerNode { } } - const item = new TreeItem(StatusFileFormatter.fromTemplate(this.template, this.status), TreeItemCollapsibleState.None); + const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.None); item.contextValue = this.resourceType; const icon = getGitStatusIcon(this.status.status); @@ -40,8 +40,18 @@ export class CommitFileNode extends ExplorerNode { } getCommand(): Command | undefined { + let allowMissingPrevious = false; + let prefix = undefined; + if (this.status.status === 'A') { + allowMissingPrevious = true; + prefix = 'added in '; + } + else if (this.status.status === 'D') { + prefix = 'deleted in '; + } + return { - title: 'Compare File with Previous', + title: 'Compare File with Previous Revision', command: Commands.DiffWithPrevious, arguments: [ GitUri.fromFileStatus(this.status, this.commit.repoPath), @@ -51,7 +61,9 @@ export class CommitFileNode extends ExplorerNode { showOptions: { preserveFocus: true, preview: true - } + }, + allowMissingPrevious: allowMissingPrevious, + rightTitlePrefix: prefix } as DiffWithPreviousCommandArgs ] }; diff --git a/src/views/commitNode.ts b/src/views/commitNode.ts index 510b8a9..6c5b578 100644 --- a/src/views/commitNode.ts +++ b/src/views/commitNode.ts @@ -4,13 +4,13 @@ import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'v import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { CommitFileNode } from './commitFileNode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { CommitFormatter, GitCommit, GitService, GitUri } from '../gitService'; +import { CommitFormatter, GitLogCommit, GitService, GitUri } from '../gitService'; export class CommitNode extends ExplorerNode { - readonly resourceType: ResourceType = 'commit'; + readonly resourceType: ResourceType = 'gitlens:commit'; - constructor(public readonly commit: GitCommit, private template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor(public readonly commit: GitLogCommit, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(new GitUri(commit.uri, commit)); } @@ -23,7 +23,7 @@ export class CommitNode extends ExplorerNode { const commit = Iterables.first(log.commits.values()); if (commit === undefined) return []; - return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.git.config.gitExplorer.commitFileFormat, this.context, this.git))]; + return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git))]; } getTreeItem(): TreeItem { @@ -31,7 +31,8 @@ export class CommitNode extends ExplorerNode { if (this.commit.type === 'file') { item.collapsibleState = TreeItemCollapsibleState.None; item.command = this.getCommand(); - item.contextValue = 'commit-file'; + const resourceType: ResourceType = 'gitlens:commit-file'; + item.contextValue = resourceType; } else { item.collapsibleState = TreeItemCollapsibleState.Collapsed; @@ -47,8 +48,19 @@ export class CommitNode extends ExplorerNode { } getCommand(): Command | undefined { + let allowMissingPrevious = false; + let prefix = undefined; + const status = this.commit.fileStatuses[0]; + if (status.status === 'A') { + allowMissingPrevious = true; + prefix = 'added in '; + } + else if (status.status === 'D') { + prefix = 'deleted in '; + } + return { - title: 'Compare File with Previous', + title: 'Compare File with Previous Revision', command: Commands.DiffWithPrevious, arguments: [ new GitUri(this.uri, this.commit), @@ -58,7 +70,9 @@ export class CommitNode extends ExplorerNode { showOptions: { preserveFocus: true, preview: true - } + }, + allowMissingPrevious: allowMissingPrevious, + rightTitlePrefix: prefix } as DiffWithPreviousCommandArgs ] }; diff --git a/src/views/explorerNode.ts b/src/views/explorerNode.ts index 8a958f0..abad231 100644 --- a/src/views/explorerNode.ts +++ b/src/views/explorerNode.ts @@ -2,7 +2,22 @@ import { Command, Event, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GitUri } from '../gitService'; -export declare type ResourceType = 'text' | 'status' | 'branches' | 'repository' | 'branch-history' | 'file-history' | 'stash-history' | 'commit' | 'stash-commit' | 'commit-file'; +export declare type ResourceType = + 'gitlens:branches' | + 'gitlens:branch-history' | + 'gitlens:commit' | + 'gitlens:commit-file' | + 'gitlens:file-history' | + 'gitlens:history' | + 'gitlens:message' | + 'gitlens:remote' | + 'gitlens:remotes' | + 'gitlens:repository' | + 'gitlens:stash' | + 'gitlens:stash-file' | + 'gitlens:stashes' | + 'gitlens:status' | + 'gitlens:status-upstream'; export abstract class ExplorerNode { @@ -22,11 +37,11 @@ export abstract class ExplorerNode { refresh?(): void; } -export class TextExplorerNode extends ExplorerNode { +export class MessageNode extends ExplorerNode { - readonly resourceType: ResourceType = 'text'; + readonly resourceType: ResourceType = 'gitlens:message'; - constructor(private text: string) { + constructor(private message: string) { super(new GitUri()); } @@ -35,7 +50,7 @@ export class TextExplorerNode extends ExplorerNode { } getTreeItem(): TreeItem | Promise { - const item = new TreeItem(this.text, TreeItemCollapsibleState.None); + const item = new TreeItem(this.message, TreeItemCollapsibleState.None); item.contextValue = this.resourceType; return item; } diff --git a/src/views/explorerNodes.ts b/src/views/explorerNodes.ts index e715c08..e08134e 100644 --- a/src/views/explorerNodes.ts +++ b/src/views/explorerNodes.ts @@ -6,7 +6,12 @@ export * from './branchHistoryNode'; export * from './commitFileNode'; export * from './commitNode'; export * from './fileHistoryNode'; +export * from './historyNode'; +export * from './remoteNode'; +export * from './remotesNode'; export * from './repositoryNode'; -export * from './stashCommitNode'; +export * from './stashFileNode'; export * from './stashNode'; -export * from './statusNode'; \ No newline at end of file +export * from './stashesNode'; +export * from './statusNode'; +export * from './statusUpstreamNode'; \ No newline at end of file diff --git a/src/views/fileHistoryExplorer.ts b/src/views/fileHistoryExplorer.ts deleted file mode 100644 index 1487757..0000000 --- a/src/views/fileHistoryExplorer.ts +++ /dev/null @@ -1,89 +0,0 @@ -'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 f55a42d..8497d59 100644 --- a/src/views/fileHistoryNode.ts +++ b/src/views/fileHistoryNode.ts @@ -2,13 +2,12 @@ import { Iterables } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { CommitNode } from './commitNode'; -import { ExplorerNode, ResourceType, TextExplorerNode } from './explorerNode'; +import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; import { GitService, GitUri } from '../gitService'; export class FileHistoryNode extends ExplorerNode { - static readonly rootType: ResourceType = 'file-history'; - readonly resourceType: ResourceType = 'file-history'; + readonly resourceType: ResourceType = 'gitlens:file-history'; constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(uri); @@ -16,14 +15,20 @@ export class FileHistoryNode extends ExplorerNode { async getChildren(): Promise { const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); - if (log === undefined) return [new TextExplorerNode('No file history')]; + if (log === undefined) return [new MessageNode('No file history')]; - return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.git.config.fileHistoryExplorer.commitFormat, this.context, this.git))]; + return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; } getTreeItem(): TreeItem { - const item = new TreeItem(`History of ${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded); + const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded); item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-history.svg'), + light: this.context.asAbsolutePath('images/light/icon-history.svg') + }; + return item; } } \ No newline at end of file diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 2ebdceb..f405471 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -1,73 +1,162 @@ 'use strict'; -// import { Functions } from '../system'; -import { commands, Event, EventEmitter, ExtensionContext, TreeDataProvider, TreeItem, Uri } from 'vscode'; +import { Functions } from '../system'; +import { commands, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window } from 'vscode'; +import { Commands, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands'; import { UriComparer } from '../comparers'; -import { ExplorerNode, FileHistoryNode, RepositoryNode, ResourceType, StashNode } from './explorerNodes'; +import { CommandContext, setCommandContext } from '../constants'; +import { CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoryNode, StashNode } from './explorerNodes'; import { GitService, GitUri } from '../gitService'; export * from './explorerNodes'; +export type GitExplorerView = + 'history' | + 'repository'; +export const GitExplorerView = { + History: 'history' as GitExplorerView, + Repository: 'repository' as GitExplorerView +}; + +export interface OpenFileRevisionCommandArgs { + uri?: Uri; + showOptions?: TextDocumentShowOptions; +} + export class GitExplorer implements TreeDataProvider { - // private _refreshDebounced: () => void; + private _root?: ExplorerNode; + private _view: GitExplorerView = GitExplorerView.Repository; private _onDidChangeTreeData = new EventEmitter(); public get onDidChangeTreeData(): Event { return this._onDidChangeTreeData.event; } - private _roots: ExplorerNode[] = []; + constructor(private readonly context: ExtensionContext, private readonly git: GitService) { + commands.registerCommand('gitlens.gitExplorer.switchToHistoryView', () => this.switchTo(GitExplorerView.History), this); + commands.registerCommand('gitlens.gitExplorer.switchToRepositoryView', () => this.switchTo(GitExplorerView.Repository), this); + commands.registerCommand('gitlens.gitExplorer.refresh', this.refresh, this); + commands.registerCommand('gitlens.gitExplorer.openChanges', this.openChanges, this); + commands.registerCommand('gitlens.gitExplorer.openChangesWithWorking', this.openChangesWithWorking, this); + commands.registerCommand('gitlens.gitExplorer.openFile', this.openFile, this); + commands.registerCommand('gitlens.gitExplorer.openFileRevision', this.openFileRevision, this); + commands.registerCommand('gitlens.gitExplorer.openFileRevisionInRemote', this.openFileRevisionInRemote, this); + commands.registerCommand('gitlens.gitExplorer.openChangedFiles', this.openChangedFiles, this); + commands.registerCommand('gitlens.gitExplorer.openChangedFileRevisions', this.openChangedFileRevisions, this); + + const fn = Functions.debounce(this.onActiveEditorChanged, 500); + context.subscriptions.push(window.onDidChangeActiveTextEditor(fn, this)); + + this._view = this.git.config.gitExplorer.view; + setCommandContext(CommandContext.GitExplorerView, this._view); + this._root = this.getRootNode(); + } - constructor(private context: ExtensionContext, private git: GitService) { - commands.registerCommand('gitlens.gitExplorer.refresh', () => this.refresh()); + async getTreeItem(node: ExplorerNode): Promise { + return node.getTreeItem(); + } + + async getChildren(node?: ExplorerNode): Promise { + if (this._root === undefined) { + if (this._view === GitExplorerView.History) return [new MessageNode('No active file; no history to show')]; + return []; + } - // this._refreshDebounced = Functions.debounce(this.refresh.bind(this), 250); + if (node === undefined) return this._root.getChildren(); + return node.getChildren(); + } - // const editor = window.activeTextEditor; + private getRootNode(editor?: TextEditor): ExplorerNode | undefined { + const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath }); - // const uri = (editor !== undefined && editor.document !== undefined) - // ? new GitUri(editor.document.uri, { repoPath: git.repoPath, fileName: editor.document.uri.fsPath }) - // : new GitUri(Uri.file(git.repoPath), { repoPath: git.repoPath, fileName: git.repoPath }); + switch (this._view) { + case GitExplorerView.History: return this.getHistoryNode(editor || window.activeTextEditor); + case GitExplorerView.Repository: return new RepositoryNode(uri, this.context, this.git); + } - const uri = new GitUri(Uri.file(git.repoPath), { repoPath: git.repoPath, fileName: git.repoPath }); - this._roots.push(new RepositoryNode(uri, context, git)); + return undefined; } - async getTreeItem(node: ExplorerNode): Promise { - // if (node.onDidChangeTreeData !== undefined) { - // node.onDidChangeTreeData(() => setTimeout(this._refreshDebounced, 1)); - // } - return node.getTreeItem(); + private getHistoryNode(editor: TextEditor | undefined): ExplorerNode | undefined { + if (window.visibleTextEditors.length === 0) return undefined; + if (editor === undefined) return this._root; + + 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._root && this._root.uri)) return this._root; + + return new HistoryNode(uri, this.context, this.git); } - async getChildren(node?: ExplorerNode): Promise { - if (this._roots.length === 0) return []; - if (node === undefined) return this._roots; + private onActiveEditorChanged(editor: TextEditor | undefined) { + if (this._view !== GitExplorerView.History) return; + const root = this.getRootNode(editor); + if (root === this._root) return; - return node.getChildren(); + this.refresh(root); } - addHistory(uri: GitUri) { - this._add(uri, FileHistoryNode); + refresh(root?: ExplorerNode) { + this._root = root || this.getRootNode(); + this._onDidChangeTreeData.fire(); + } + + switchTo(view: GitExplorerView) { + if (this._view === view) return; + + this._view = view; + setCommandContext(CommandContext.GitExplorerView, this._view); + + this._root = undefined; + this.refresh(); + } + + private openChanges(node: CommitNode | StashNode) { + 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); } - addStash(uri: GitUri) { - this._add(uri, StashNode); + private openChangesWithWorking(node: CommitNode | StashNode) { + const args: DiffWithWorkingCommandArgs = { + commit: node.commit, + showOptions: { + preserveFocus: true, + preview: false + + } + }; + return commands.executeCommand(Commands.DiffWithWorking, new GitUri(node.commit.uri, node.commit), args); + } + + private openFile(node: CommitNode | StashNode) { + return openEditor(node.uri, { preserveFocus: true, preview: false }); + } + + private openFileRevision(node: CommitNode | StashNode | CommitFileNode, options: OpenFileRevisionCommandArgs = { showOptions: { preserveFocus: true, preview: false } }) { + return openEditor(options.uri || GitService.toGitContentUri(node.uri), options.showOptions || { preserveFocus: true, preview: false }); } - private _add(uri: GitUri, type: { new (uri: GitUri, context: ExtensionContext, git: GitService): T, rootType: ResourceType }) { - if (!this._roots.some(_ => _.resourceType === type.rootType && UriComparer.equals(uri, _.uri))) { - this._roots.push(new type(uri, this.context, this.git)); + private async openChangedFiles(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { + const repoPath = node.commit.repoPath; + const uris = node.commit.fileStatuses.filter(s => s.status !== 'D').map(s => GitUri.fromFileStatus(s, repoPath)); + for (const uri of uris) { + await openEditor(uri, options); } - this.refresh(); } - clear() { - this._roots = []; - this.refresh(); + private async openChangedFileRevisions(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { + const uris = node.commit.fileStatuses + .filter(s => s.status !== 'D') + .map(s => GitService.toGitContentUri(node.commit.sha, node.commit.shortSha, s.fileName, node.commit.repoPath, s.originalFileName)); + for (const uri of uris) { + await openEditor(uri, options); + } } - refresh() { - this._onDidChangeTreeData.fire(); + private async openFileRevisionInRemote(node: CommitNode | StashNode) { + 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/historyNode.ts b/src/views/historyNode.ts new file mode 100644 index 0000000..96acc31 --- /dev/null +++ b/src/views/historyNode.ts @@ -0,0 +1,30 @@ +'use strict'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { ExplorerNode, ResourceType } from './explorerNode'; +import { FileHistoryNode } from './fileHistoryNode'; +import { GitService, GitUri } from '../gitService'; + +export class HistoryNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:history'; + + constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + super(uri); + } + + async getChildren(): Promise { + return [new FileHistoryNode(this.uri, this.context, this.git)]; + } + + getTreeItem(): TreeItem { + const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded); + item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-history.svg'), + light: this.context.asAbsolutePath('images/light/icon-history.svg') + }; + + return item; + } +} \ No newline at end of file diff --git a/src/views/remoteNode.ts b/src/views/remoteNode.ts new file mode 100644 index 0000000..c19ba24 --- /dev/null +++ b/src/views/remoteNode.ts @@ -0,0 +1,35 @@ +'use strict'; +import { Iterables } from '../system'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { BranchHistoryNode } from './branchHistoryNode'; +import { ExplorerNode, ResourceType } from './explorerNode'; +import { GitRemote, GitService, GitUri } from '../gitService'; + +export class RemoteNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:remote'; + + constructor(public readonly remote: GitRemote, uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + super(uri); + } + + async getChildren(): Promise { + const branches = await this.git.getBranches(this.uri.repoPath!); + if (branches === undefined) return []; + + branches.sort((a, b) => a.name.localeCompare(b.name)); + return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.remote, this.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; + } + + getTreeItem(): TreeItem { + const item = new TreeItem(this.remote.name, TreeItemCollapsibleState.Collapsed); + item.contextValue = this.resourceType; + + // item.iconPath = { + // dark: this.context.asAbsolutePath('images/dark/icon-remote.svg'), + // light: this.context.asAbsolutePath('images/light/icon-remote.svg') + // }; + + return item; + } + } diff --git a/src/views/remotesNode.ts b/src/views/remotesNode.ts new file mode 100644 index 0000000..be7852b --- /dev/null +++ b/src/views/remotesNode.ts @@ -0,0 +1,35 @@ +'use strict'; +import { Arrays, Iterables } from '../system'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { ExplorerNode, ResourceType } from './explorerNode'; +import { GitService, GitUri } from '../gitService'; +import { RemoteNode } from './remoteNode'; + +export class RemotesNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:remotes'; + + constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + super(uri); + } + + async getChildren(): Promise { + const remotes = Arrays.uniqueBy(await this.git.getRemotes(this.uri.repoPath!), r => r.url, r => !!r.provider); + if (remotes === undefined) return []; + + remotes.sort((a, b) => a.name.localeCompare(b.name)); + return [...Iterables.map(remotes, r => new RemoteNode(r, this.uri, this.context, this.git))]; + } + + getTreeItem(): TreeItem { + const item = new TreeItem(`Remotes`, TreeItemCollapsibleState.Collapsed); + item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-remote.svg'), + light: this.context.asAbsolutePath('images/light/icon-remote.svg') + }; + + return item; + } + } diff --git a/src/views/repositoryNode.ts b/src/views/repositoryNode.ts index 5d6cff5..d69519e 100644 --- a/src/views/repositoryNode.ts +++ b/src/views/repositoryNode.ts @@ -4,12 +4,13 @@ import { BranchesNode } from './branchesNode'; import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType } from './explorerNode'; import { GitService, GitUri } from '../gitService'; -// import { StatusNode } from './statusNode'; +import { RemotesNode } from './remotesNode'; +import { StatusNode } from './statusNode'; +import { StashesNode } from './stashesNode'; export class RepositoryNode extends ExplorerNode { - static readonly rootType: ResourceType = 'repository'; - readonly resourceType: ResourceType = 'repository'; + readonly resourceType: ResourceType = 'gitlens:repository'; constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(uri); @@ -17,8 +18,10 @@ export class RepositoryNode extends ExplorerNode { async getChildren(): Promise { return [ - // new StatusNode(this.uri, this.context, this.git), - new BranchesNode(this.uri, this.context, this.git) + new StatusNode(this.uri, this.context, this.git), + new BranchesNode(this.uri, this.context, this.git), + new RemotesNode(this.uri, this.context, this.git), + new StashesNode(this.uri, this.context, this.git) ]; } diff --git a/src/views/stashCommitNode.ts b/src/views/stashCommitNode.ts deleted file mode 100644 index 5b5655c..0000000 --- a/src/views/stashCommitNode.ts +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; -import { Event, EventEmitter, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { CommitFileNode } from './commitFileNode'; -import { ExplorerNode, ResourceType } from './explorerNode'; -import { CommitFormatter, GitService, GitStashCommit, GitUri } from '../gitService'; - -export class StashCommitNode extends ExplorerNode { - - readonly resourceType: ResourceType = 'stash-commit'; - - private _onDidChangeTreeData = new EventEmitter(); - public get onDidChangeTreeData(): Event { - return this._onDidChangeTreeData.event; - } - - constructor(public readonly commit: GitStashCommit, protected readonly context: ExtensionContext, protected readonly git: GitService) { - super(new GitUri(commit.uri, commit)); - } - - async getChildren(): Promise { - return Promise.resolve((this.commit as GitStashCommit).fileStatuses.map(_ => new CommitFileNode(_, this.commit, this.git.config.stashExplorer.stashFileFormat, this.context, this.git))); - } - - getTreeItem(): TreeItem { - const label = CommitFormatter.fromTemplate(this.git.config.stashExplorer.stashFormat, this.commit, this.git.config.defaultDateFormat); - - const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); - item.contextValue = this.resourceType; - // item.command = { - // title: 'Show Stash Details', - // command: Commands.ShowQuickCommitDetails, - // arguments: [ - // new GitUri(commit.uri, commit), - // { - // commit: this.commit, - // sha: this.commit.sha - // } as ShowQuickCommitDetailsCommandArgs - // ] - // }; - return item; - } - - refresh() { - this._onDidChangeTreeData.fire(); - } -} \ No newline at end of file diff --git a/src/views/stashExplorer.ts b/src/views/stashExplorer.ts deleted file mode 100644 index ab067ed..0000000 --- a/src/views/stashExplorer.ts +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; -// import { Functions } from '../system'; -import { commands, Event, EventEmitter, ExtensionContext, TreeDataProvider, TreeItem, Uri } from 'vscode'; -import { Commands, DiffWithPreviousCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands'; -import { ExplorerNode, StashCommitNode, StashNode } from './explorerNodes'; -import { GitService, GitUri } from '../gitService'; - -export * from './explorerNodes'; - -export class StashExplorer 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.stashExplorer.refresh', this.refresh, this); - commands.registerCommand('gitlens.stashExplorer.openChanges', this.openChanges, this); - commands.registerCommand('gitlens.stashExplorer.openFile', this.openFile, this); - commands.registerCommand('gitlens.stashExplorer.openStashedFile', this.openStashedFile, this); - commands.registerCommand('gitlens.stashExplorer.openFileInRemote', this.openFileInRemote, this); - - context.subscriptions.push(this.git.onDidChangeRepo(this.onRepoChanged, this)); - - this._node = this.getRootNode(); - } - - async getTreeItem(node: ExplorerNode): Promise { - return node.getTreeItem(); - } - - async getChildren(node?: ExplorerNode): Promise { - if (node === undefined) return this._node.getChildren(); - return node.getChildren(); - } - - private getRootNode(): ExplorerNode { - const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath }); - return new StashNode(uri, this.context, this.git); - } - - private onRepoChanged(reasons: ('stash' | 'unknown')[]) { - if (!reasons.includes('stash')) return; - - this.refresh(); - } - - refresh() { - this._onDidChangeTreeData.fire(); - } - - private openChanges(node: StashCommitNode) { - 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: StashCommitNode) { - return openEditor(node.uri, { preserveFocus: true, preview: false }); - } - - private openStashedFile(node: StashCommitNode) { - return openEditor(GitService.toGitContentUri(node.uri), { preserveFocus: true, preview: false }); - } - - private openFileInRemote(node: StashCommitNode) { - return commands.executeCommand(Commands.OpenFileInRemote, node.commit.uri, { range: false } as OpenFileInRemoteCommandArgs); - } -} \ No newline at end of file diff --git a/src/views/stashFileNode.ts b/src/views/stashFileNode.ts new file mode 100644 index 0000000..16dbbd5 --- /dev/null +++ b/src/views/stashFileNode.ts @@ -0,0 +1,14 @@ +'use strict'; +import { ExtensionContext } from 'vscode'; +import { ResourceType } from './explorerNode'; +import { GitService, GitStashCommit, IGitStatusFile } from '../gitService'; +import { CommitFileNode } from './commitFileNode'; + +export class StashFileNode extends CommitFileNode { + + readonly resourceType: ResourceType = 'gitlens:stash-file'; + + constructor(readonly status: IGitStatusFile, readonly commit: GitStashCommit, readonly context: ExtensionContext, readonly git: GitService) { + super(status, commit, context, git); + } +} \ No newline at end of file diff --git a/src/views/stashNode.ts b/src/views/stashNode.ts index 4cc83b3..9fc5e64 100644 --- a/src/views/stashNode.ts +++ b/src/views/stashNode.ts @@ -1,29 +1,35 @@ 'use strict'; -import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { ExplorerNode, ResourceType, TextExplorerNode } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; -import { StashCommitNode } from './stashCommitNode'; +import { Event, EventEmitter, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { ExplorerNode, ResourceType } from './explorerNode'; +import { CommitFormatter, GitService, GitStashCommit, GitUri } from '../gitService'; +import { StashFileNode } from './stashFileNode'; export class StashNode extends ExplorerNode { - static readonly rootType: ResourceType = 'stash-history'; - readonly resourceType: ResourceType = 'stash-history'; + readonly resourceType: ResourceType = 'gitlens:stash'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { - super(uri); - } + private _onDidChangeTreeData = new EventEmitter(); + public get onDidChangeTreeData(): Event { + return this._onDidChangeTreeData.event; + } - async getChildren(): Promise { - const stash = await this.git.getStashList(this.uri.repoPath!); - if (stash === undefined) return [new TextExplorerNode('No stashed changes')]; + constructor(public readonly commit: GitStashCommit, protected readonly context: ExtensionContext, protected readonly git: GitService) { + super(new GitUri(commit.uri, commit)); + } - return [...Iterables.map(stash.commits.values(), c => new StashCommitNode(c, this.context, this.git))]; + async getChildren(): Promise { + return Promise.resolve((this.commit as GitStashCommit).fileStatuses.map(s => new StashFileNode(s, this.commit, this.context, this.git))); } getTreeItem(): TreeItem { - const item = new TreeItem(`Stashed Changes`, TreeItemCollapsibleState.Collapsed); + const label = CommitFormatter.fromTemplate(this.git.config.gitExplorer.stashFormat, this.commit, this.git.config.defaultDateFormat); + + const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; return item; } + + refresh() { + this._onDidChangeTreeData.fire(); + } } \ No newline at end of file diff --git a/src/views/stashesNode.ts b/src/views/stashesNode.ts new file mode 100644 index 0000000..42f1035 --- /dev/null +++ b/src/views/stashesNode.ts @@ -0,0 +1,34 @@ +'use strict'; +import { Iterables } from '../system'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; +import { GitService, GitUri } from '../gitService'; +import { StashNode } from './stashNode'; + +export class StashesNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:stashes'; + + constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + super(uri); + } + + async getChildren(): Promise { + const stash = await this.git.getStashList(this.uri.repoPath!); + if (stash === undefined) return [new MessageNode('No stashed changes')]; + + return [...Iterables.map(stash.commits.values(), c => new StashNode(c, this.context, this.git))]; + } + + getTreeItem(): TreeItem { + const item = new TreeItem(`Stashes`, TreeItemCollapsibleState.Collapsed); + item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-stash.svg'), + light: this.context.asAbsolutePath('images/light/icon-stash.svg') + }; + + return item; + } +} \ No newline at end of file diff --git a/src/views/statusNode.ts b/src/views/statusNode.ts index e614b87..b00fd23 100644 --- a/src/views/statusNode.ts +++ b/src/views/statusNode.ts @@ -1,34 +1,60 @@ -import { Strings } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType } from './explorerNode'; import { GitService, GitUri } from '../gitService'; +import { StatusUpstreamNode } from './statusUpstreamNode'; export class StatusNode extends ExplorerNode { - readonly resourceType: ResourceType = 'status'; + readonly resourceType: ResourceType = 'gitlens:status'; constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { super(uri); } async getChildren(): Promise { - return []; - // const status = await this.git.getStatusForRepo(this.uri.repoPath!); - // if (status === undefined) return []; + const status = await this.git.getStatusForRepo(this.uri.repoPath!); + if (status === undefined) return []; + + const children = []; + + if (status.state.behind) { + children.push(new StatusUpstreamNode(status, 'behind', this.git.config.gitExplorer.commitFormat, this.context, this.git)); + } + + if (status.state.ahead) { + children.push(new StatusUpstreamNode(status, 'ahead', this.git.config.gitExplorer.commitFormat, this.context, this.git)); + } - // return [...Iterables.map(status.files, b => new CommitFile(b, this.uri, this.context, this.git))]; + return children; } async getTreeItem(): Promise { const status = await this.git.getStatusForRepo(this.uri.repoPath!); - let suffix = ''; - if (status !== undefined) { - suffix = ` ${GlyphChars.Dash} ${GlyphChars.ArrowUp} ${status.state.ahead} ${GlyphChars.ArrowDown} ${status.state.behind} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${status.branch} ${GlyphChars.ArrowLeftRight} ${status.upstream}`; + if (status === undefined) return new TreeItem('No repo status'); + + let hasChildren = false; + let label = ''; + if (status.upstream) { + if (!status.state.ahead && !status.state.behind) { + label = `${status.branch} is up-to-date with ${status.upstream}`; + } + else { + label = `${status.branch} is not up-to-date with ${status.upstream}`; + hasChildren = true; + } + } + else { + label = `${status.branch} is up-to-date`; } - const item = new TreeItem(`Status${suffix}`, TreeItemCollapsibleState.Collapsed); + const item = new TreeItem(label, hasChildren ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-repo.svg'), + light: this.context.asAbsolutePath('images/light/icon-repo.svg') + }; + return item; } } \ No newline at end of file diff --git a/src/views/statusUpstreamNode.ts b/src/views/statusUpstreamNode.ts new file mode 100644 index 0000000..1f2a230 --- /dev/null +++ b/src/views/statusUpstreamNode.ts @@ -0,0 +1,53 @@ +'use strict'; +import { Iterables } from '../system'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { ExplorerNode, ResourceType } from './explorerNode'; +import { GitService, GitStatus, GitUri } from '../gitService'; +import { CommitNode } from './commitNode'; + +export class StatusUpstreamNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:status-upstream'; + + constructor(public readonly status: GitStatus, public readonly direction: 'ahead' | 'behind', private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { + super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); + } + + async getChildren(): Promise { + const range = this.direction === 'ahead' + ? `${this.status.upstream}..${this.status.branch}` + : `${this.status.branch}..${this.status.upstream}`; + let log = await this.git.getLogForRepo(this.uri.repoPath!, range); + if (log === undefined) return []; + + if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git))]; + + // Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up + const commits = Array.from(log.commits.values()); + const commit = commits[commits.length - 1]; + if (commit.previousSha === undefined) { + log = await this.git.getLogForRepo(this.uri.repoPath!, commit.sha, 2); + if (log !== undefined) { + commits[commits.length - 1] = Iterables.first(log.commits.values()); + } + } + + return [...Iterables.map(commits, c => new CommitNode(c, this.template, this.context, this.git))]; +} + + async getTreeItem(): Promise { + const label = this.direction === 'ahead' + ? `${this.status.state.ahead} commit${this.status.state.ahead > 1 ? 's' : ''} ahead` // of ${this.status.upstream}` + : `${this.status.state.behind} commit${this.status.state.behind > 1 ? 's' : ''} behind`; // ${this.status.upstream}`; + + const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); + item.contextValue = this.resourceType; + + item.iconPath = { + dark: this.context.asAbsolutePath(`images/dark/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`), + light: this.context.asAbsolutePath(`images/light/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`) + }; + + return item; + } +} \ No newline at end of file