21 Commits

Author SHA1 Message Date
Eric Amodio
a10376385a Preps v5.1.1-beta 2017-09-17 22:41:47 -04:00
Eric Amodio
41d25803d8 Updates slack links 2017-09-17 14:38:59 -04:00
Eric Amodio
3802b43027 Adds merged PR to changelog
Adds new contributor to readme
2017-09-17 11:28:42 -04:00
Amanda Cameron
04ea3b7971 Apply Review Comments. 2017-09-17 11:20:21 -04:00
Amanda Cameron
6d7f44e091 Fix GitLab integration's multi-line selection. 2017-09-17 11:20:21 -04:00
Eric Amodio
3a1caa2e0d Disables a set of context menu items by default 2017-09-17 02:44:10 -04:00
Eric Amodio
3f7058bd48 Fixes wrong setting used to control menu commands 2017-09-17 02:43:46 -04:00
Eric Amodio
71d17bcc2f Closes #139 - adds changed files node to repository status
Reworks commit-file nodes
2017-09-17 02:34:09 -04:00
Eric Amodio
a69afdb6ef Closes #144 - support disabling the custom view 2017-09-17 01:43:41 -04:00
Eric Amodio
26c6346b84 Changes default commit format in custom view 2017-09-16 12:06:18 -04:00
Eric Amodio
3a17605017 Preps v5.1.0 2017-09-15 21:03:52 -04:00
Eric Amodio
2c9a26e47b Fixes untracked files not showing in stash list 2017-09-15 18:12:22 -04:00
Eric Amodio
1c7785fd52 Adds note for closed issue in changelog 2017-09-15 17:39:39 -04:00
Eric Amodio
079f7b7f36 Switches to use a unicode arrow for the external link icon 2017-09-15 17:39:39 -04:00
Eric Amodio
bedc1a05f5 Preps v5.1.0-beta 2017-09-15 17:39:39 -04:00
Eric Amodio
858d9ec578 Fixes issue with stashes w/ only untracked files 2017-09-15 17:39:38 -04:00
Eric Amodio
2809991096 Closes #116 - adds full commit msg to annotations
Switches to use HoverProvider for hovers in file blames
2017-09-15 17:38:37 -04:00
Eric Amodio
f6019454b6 Adds open in remote to hover annotations
Optimizes annotation computation (cache by commit)
2017-09-14 23:43:41 -04:00
Eric Amodio
f0bdf3e2c3 Always caches remotes 2017-09-14 22:46:40 -04:00
Eric Amodio
0fdf856c27 Adds performance logging 2017-09-14 22:45:23 -04:00
Eric Amodio
aacf7cc2b5 Reworks date parsing, formatting etc for perf
Isolates moment.js
2017-09-14 21:52:51 -04:00
56 changed files with 804 additions and 358 deletions

View File

@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [5.1.1-beta]
### Added
- Adds new `Changed Files` node to the `Repository Status` node of the `GitLens` custom view's `Repository View` (enabled via `"gitlens.insiders": true`) -- closes [#139](https://github.com/eamodio/vscode-gitlens/issues/139)
- Provides a file-based view of all the changed files in the working tree and/or files in commits that haven't yet been pushed upstream
- Adds `gitlens.gitExplorer.enabled` setting to specify whether or not to show the `GitLens` custom view - closes [#144](https://github.com/eamodio/vscode-gitlens/issues/144)
### Changed
- Chnages the default of the `gitlens.gitExplorer.commitFormat` setting to add parentheses around the commit id
- Removes many menu items from `editor/title` & `editor/title/context` by default -- can be re-enabled via the `gitlens.advanced.menus` setting
### Fixed
- Fixes an issue when running `Open File in Remote` with a multi-line selection wasn't properly opening the selection in GitLab -- thanks to [PR #145](https://github.com/eamodio/vscode-gitlens/pull/145) by Amanda Cameron ([@AmandaCameron](https://github.com/AmandaCameron))!
- Fixes an issue where the `gitlens.advanced.menus` setting wasn't controlling all the menu items properly
## [5.1.0] - 2017-09-15
### Added
- Adds full (multi-line) commit message to the `details` hover annotations -- closes [#116](https://github.com/eamodio/vscode-gitlens/issues/116)
- Adds an external link icon to the `details` hover annotations to run the `Open Commit in Remote` command (`gitlens.openCommitInRemote`)
### Changed
- Optimizes performance of the providing blame annotations, especially for large files (saw a ~78% improvement on some files)
- Optimizes date handling (parsing and formatting) for better performance and reduced memory consumption
### Removed
- Removes `gitlens.annotations.file.recentChanges.hover.wholeLine` setting as it didn't really make sense
### Fixed
- Fixes an issue where stashes with only untracked files would not show in the `Stashes` node of the GitLens custom view
- Fixes an issue where stashes with untracked files would not show its untracked files in the GitLens custom view
## [5.0.0] - 2017-09-12 ## [5.0.0] - 2017-09-12
### Added ### Added
- Adds an all-new `GitLens` custom view to the Explorer activity - Adds an all-new `GitLens` custom view to the Explorer activity

View File

@@ -1,7 +1,7 @@
[![](https://vsmarketplacebadge.apphb.com/version/eamodio.gitlens.svg)](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) [![](https://vsmarketplacebadge.apphb.com/version/eamodio.gitlens.svg)](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
[![](https://vsmarketplacebadge.apphb.com/installs/eamodio.gitlens.svg)](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) [![](https://vsmarketplacebadge.apphb.com/installs/eamodio.gitlens.svg)](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
[![](https://vsmarketplacebadge.apphb.com/rating/eamodio.gitlens.svg)](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) [![](https://vsmarketplacebadge.apphb.com/rating/eamodio.gitlens.svg)](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
[![Chat at https://vscode-gitlens.slack.com/](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/chat-badge.png)](https://join.slack.com/t/vscode-gitlens/shared_invite/MjIxOTgxNDE3NzM0LTE1MDE2Nzk1MTgtMjkwMmZjMzcxNQ) [![Chat at https://vscode-dev-community.slack.com/](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/chat-badge.png)](https://join.slack.com/t/vscode-dev-community/shared_invite/enQtMjIxOTgxNDE3NzM0LWU5M2ZiZDU1YjBlMzdlZjA2YjBjYzRhYTM5NTgzMTAxMjdiNWU0ZmQzYWI3MWU5N2Q1YjBiYmQ4MzY0NDE1MzY)
# GitLens # GitLens
@@ -25,6 +25,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `changes` (diff) hover annotation to the current line annotation, which provides **instant** access to the line's previous version ([optional](#line-blame-annotation-settings), on by default) - Adds a `changes` (diff) hover annotation to the current line annotation, which provides **instant** access to the line's previous version ([optional](#line-blame-annotation-settings), on by default)
- Clicking on `Changes` will run the `Compare File Revisions` command (`gitlens.diffWith`) - Clicking on `Changes` will run the `Compare File Revisions` command (`gitlens.diffWith`)
- Clicking the current and previous commit ids will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`) - Clicking the current and previous commit ids will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`)
- Clicking on external link icon will run the the `Open Commit in Remote` command (`gitlens.openCommitInRemote`)
![Line Blame Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png) ![Line Blame Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png)
@@ -331,7 +332,6 @@ GitLens is highly customizable and provides many configuration settings to allow
|`gitlens.recentChanges.file.lineHighlight.locations`|Specifies where the highlights of the recently changed lines will be shown<br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overviewRuler` - adds a decoration to the overviewRuler (scroll bar) |`gitlens.recentChanges.file.lineHighlight.locations`|Specifies where the highlights of the recently changed lines will be shown<br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overviewRuler` - adds a decoration to the overviewRuler (scroll bar)
|`gitlens.annotations.file.recentChanges.hover.details`|Specifies whether or not to provide a commit details hover annotation |`gitlens.annotations.file.recentChanges.hover.details`|Specifies whether or not to provide a commit details hover annotation
|`gitlens.annotations.file.recentChanges.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation |`gitlens.annotations.file.recentChanges.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation
|`gitlens.annotations.file.recentChanges.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
### Code Lens Settings ### Code Lens Settings
@@ -350,6 +350,7 @@ GitLens is highly customizable and provides many configuration settings to allow
|Name | Description |Name | Description
|-----|------------ |-----|------------
|`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view"
|`gitlens.gitExplorer.view`|Specifies the starting view (mode) of the `GitLens` custom view<br />`history` - shows the commit history of the active file<br />`repository` - shows a repository explorer" |`gitlens.gitExplorer.view`|Specifies the starting view (mode) of the `GitLens` custom view<br />`history` - shows the commit history of the active file<br />`repository` - shows a repository explorer"
|`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view" |`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view"
|`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` custom view<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${authorAgo} - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` custom view<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${authorAgo} - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting
@@ -422,6 +423,7 @@ GitLens is highly customizable and provides many configuration settings to allow
A big thanks to the people that have contributed to this project: A big thanks to the people that have contributed to this project:
- Amanda Cameron ([@AmandaCameron](https://github.com/AmandaCameron)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=AmandaCameron))
- Peng Lyu ([@rebornix](https://github.com/rebornix)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=rebornix)) - Peng Lyu ([@rebornix](https://github.com/rebornix)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=rebornix))
- Aurelio Ogliari ([@nobitagit](https://github.com/nobitagit)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=nobitagit) - Aurelio Ogliari ([@nobitagit](https://github.com/nobitagit)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=nobitagit)
- Johannes Rieken ([@jrieken](https://github.com/jrieken)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=jrieken)) - Johannes Rieken ([@jrieken](https://github.com/jrieken)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=jrieken))

View File

@@ -1,6 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#7F4E7E" x="0" y="0" width="100" height="100" rx="35" ry="35"/> <rect fill="#7F4E7E" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white"> <text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C !
</text> </text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#6C6C6C" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
?
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -1,6 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#9B4F96" x="0" y="0" width="100" height="100" rx="35" ry="35"/> <rect fill="#9B4F96" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white"> <text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C !
</text> </text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#6C6C6C" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
?
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.0.0", "version": "5.1.1-beta",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.0.0", "version": "5.1.1-beta",
"author": { "author": {
"name": "Eric Amodio", "name": "Eric Amodio",
"email": "eamodio@gmail.com" "email": "eamodio@gmail.com"
@@ -15,8 +15,8 @@
"badges": [ "badges": [
{ {
"url": "https://img.shields.io/badge/chat-on%20slack-brightgreen.svg", "url": "https://img.shields.io/badge/chat-on%20slack-brightgreen.svg",
"href": "https://join.slack.com/t/vscode-gitlens/shared_invite/MjIxOTgxNDE3NzM0LTE1MDE2Nzk1MTgtMjkwMmZjMzcxNQ", "href": "https://join.slack.com/t/vscode-dev-community/shared_invite/enQtMjIxOTgxNDE3NzM0LWU5M2ZiZDU1YjBlMzdlZjA2YjBjYzRhYTM5NTgzMTAxMjdiNWU0ZmQzYWI3MWU5N2Q1YjBiYmQ4MzY0NDE1MzY",
"description": "Chat at https://vscode-gitlens.slack.com/" "description": "Chat at https://vscode-dev-community.slack.com/"
} }
], ],
"categories": [ "categories": [
@@ -132,11 +132,6 @@
"default": true, "default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation" "description": "Specifies whether or not to provide a changes (diff) hover annotation"
}, },
"gitlens.annotations.file.recentChanges.hover.wholeLine": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to trigger hover annotations over the whole line"
},
"gitlens.annotations.line.hover.details": { "gitlens.annotations.line.hover.details": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
@@ -420,7 +415,7 @@
}, },
"gitlens.gitExplorer.commitFormat": { "gitlens.gitExplorer.commitFormat": {
"type": "string", "type": "string",
"default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0\u2022\u00a0 ${id}", "default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0 (${id})",
"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" "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": { "gitlens.gitExplorer.commitFileFormat": {
@@ -428,6 +423,11 @@
"default": "${filePath}", "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" "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.gitExplorer.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to show the `GitLens` custom view"
},
"gitlens.gitExplorer.showTrackingBranch": { "gitlens.gitExplorer.showTrackingBranch": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
@@ -651,16 +651,16 @@
}, },
"editorTitle": { "editorTitle": {
"blame": true, "blame": true,
"fileDiff": true, "fileDiff": false,
"history": true, "history": false,
"remote": true, "remote": false,
"status": true "status": false
}, },
"editorTitleContext": { "editorTitleContext": {
"blame": true, "blame": false,
"fileDiff": true, "fileDiff": false,
"history": true, "history": false,
"remote": true "remote": false
}, },
"explorerContext": { "explorerContext": {
"fileDiff": true, "fileDiff": true,
@@ -1402,12 +1402,12 @@
}, },
{ {
"command": "gitlens.openFileInRemote", "command": "gitlens.openFileInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitleContext.remote", "when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitle.remote",
"group": "1_gitlens" "group": "1_gitlens"
}, },
{ {
"command": "gitlens.openRepoInRemote", "command": "gitlens.openRepoInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitleContext.remote", "when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitle.remote",
"group": "1_gitlens" "group": "1_gitlens"
}, },
{ {
@@ -1742,9 +1742,54 @@
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{
"command": "gitlens.gitExplorer.openChanges",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "1_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openChangesWithWorking",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "1_gitlens@2"
},
{
"command": "gitlens.gitExplorer.openFile",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "2_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openFileRevision",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "2_gitlens@2"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "3_gitlens@1"
},
{
"command": "gitlens.showQuickFileHistory",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file && gitlens:gitExplorer:view == repository",
"group": "5_gitlens@1"
},
{
"command": "gitlens.showQuickCommitFileDetails",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "5_gitlens@2"
},
{
"command": "gitlens.gitExplorer.openFile",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file-commits",
"group": "1_gitlens@1"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status-file-commits",
"group": "1_gitlens@2"
},
{ {
"command": "gitlens.gitExplorer.refresh", "command": "gitlens.gitExplorer.refresh",
"when": "view == gitlens.gitExplorer && viewItem != gitlens:commit-file && viewItem != gitlens:stash-file", "when": "view == gitlens.gitExplorer && viewItem != gitlens:commit-file && viewItem != gitlens:stash-file && viewItem != gitlens:status-file",
"group": "9_gitlens@1" "group": "9_gitlens@1"
} }
] ]
@@ -1846,7 +1891,7 @@
{ {
"id": "gitlens.gitExplorer", "id": "gitlens.gitExplorer",
"name": "GitLens", "name": "GitLens",
"when": "gitlens:enabled" "when": "gitlens:enabled && config.gitlens.gitExplorer.enabled"
} }
] ]
} }

View File

@@ -222,7 +222,7 @@ export class AnnotationController extends Disposable {
} }
getProvider(editor: TextEditor | undefined): AnnotationProviderBase | undefined { getProvider(editor: TextEditor | undefined): AnnotationProviderBase | undefined {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return undefined; if (editor === undefined || editor.document === undefined || !this.git.isEditorBlameable(editor)) return undefined;
return this._annotationProviders.get(editor.viewColumn || -1); return this._annotationProviders.get(editor.viewColumn || -1);
} }
@@ -233,7 +233,7 @@ export class AnnotationController extends Disposable {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
const currentProvider = this._annotationProviders.get(editor.viewColumn || -1); const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) { if (currentProvider !== undefined && TextEditorComparer.equals(currentProvider.editor, editor)) {
await currentProvider.selection(shaOrLine); await currentProvider.selection(shaOrLine);
return true; return true;
} }

View File

@@ -1,10 +1,9 @@
import { Strings } from '../system'; import { Dates, Strings } from '../system';
import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode'; import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode';
import { DiffWithCommand, ShowQuickCommitDetailsCommand } from '../commands'; import { DiffWithCommand, OpenCommitInRemoteCommand, ShowQuickCommitDetailsCommand } from '../commands';
import { IThemeConfig, themeDefaults } from '../configuration'; import { IThemeConfig, themeDefaults } from '../configuration';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { CommitFormatter, GitCommit, GitDiffChunkLine, GitService, GitUri, ICommitFormatOptions } from '../gitService'; import { CommitFormatter, GitCommit, GitDiffChunkLine, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
interface IHeatmapConfig { interface IHeatmapConfig {
enabled: boolean; enabled: boolean;
@@ -28,13 +27,13 @@ const escapeMarkdownRegEx = /[`\>\#\*\_\-\+\.]/g;
export class Annotations { export class Annotations {
static applyHeatmap(decoration: DecorationOptions, date: Date, now: moment.Moment) { static applyHeatmap(decoration: DecorationOptions, date: Date, now: number) {
const color = this._getHeatmapColor(now, date); const color = this._getHeatmapColor(now, date);
(decoration.renderOptions!.before! as any).borderColor = color; (decoration.renderOptions!.before! as any).borderColor = color;
} }
private static _getHeatmapColor(now: moment.Moment, date: Date) { private static _getHeatmapColor(now: number, date: Date) {
const days = now.diff(moment(date), 'days'); const days = Dates.dateDaysFromNow(date, now);
if (days <= 2) return '#ffeca7'; if (days <= 2) return '#ffeca7';
if (days <= 7) return '#ffdd8c'; if (days <= 7) return '#ffdd8c';
@@ -48,7 +47,7 @@ export class Annotations {
return '#793738'; return '#793738';
} }
static getHoverMessage(commit: GitCommit, dateFormat: string | null): MarkdownString { static getHoverMessage(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): MarkdownString {
if (dateFormat === null) { if (dateFormat === null) {
dateFormat = 'MMMM Do, YYYY h:MMa'; dateFormat = 'MMMM Do, YYYY h:MMa';
} }
@@ -65,7 +64,11 @@ export class Annotations {
message = `\n\n> ${message}`; message = `\n\n> ${message}`;
} }
const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)}) &nbsp; __${commit.author}__, ${moment(commit.date).fromNow()} &nbsp; _(${moment(commit.date).format(dateFormat)})_${message}`); const openInRemoteCommand = hasRemotes
? `${'&nbsp;'.repeat(3)} [\`${GlyphChars.ArrowUpRight}\`](${OpenCommitInRemoteCommand.getMarkdownCommandArgs(commit.sha)} "Open in Remote")`
: '';
const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details") &nbsp; __${commit.author}__, ${commit.fromNow()} &nbsp; _(${commit.formatDate(dateFormat)})_ ${openInRemoteCommand} &nbsp; ${message}`);
markdown.isTrusted = true; markdown.isTrusted = true;
return markdown; return markdown;
} }
@@ -75,8 +78,8 @@ export class Annotations {
const codeDiff = this._getCodeDiff(chunkLine); const codeDiff = this._getCodeDiff(chunkLine);
const markdown = new MarkdownString(commit.isUncommitted const markdown = new MarkdownString(commit.isUncommitted
? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)}) &nbsp; ${GlyphChars.Dash} &nbsp; _uncommitted_\n${codeDiff}` ? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") &nbsp; ${GlyphChars.Dash} &nbsp; _uncommitted_\n${codeDiff}`
: `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)}) &nbsp; ${GlyphChars.Dash} &nbsp; [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)}) ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)})\n${codeDiff}`); : `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") &nbsp; ${GlyphChars.Dash} &nbsp; [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)} "Show Commit Details") ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details")\n${codeDiff}`);
markdown.isTrusted = true; markdown.isTrusted = true;
return markdown; return markdown;
} }
@@ -98,8 +101,8 @@ export class Annotations {
} as DecorationOptions; } as DecorationOptions;
} }
static detailsHover(commit: GitCommit, dateFormat: string | null): DecorationOptions { static detailsHover(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): DecorationOptions {
const message = this.getHoverMessage(commit, dateFormat); const message = this.getHoverMessage(commit, dateFormat, hasRemotes);
return { return {
hoverMessage: message hoverMessage: message
} as DecorationOptions; } as DecorationOptions;
@@ -164,11 +167,12 @@ export class Annotations {
} as IRenderOptions; } as IRenderOptions;
} }
static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean, dateFormat: string | null): DecorationOptions { static hover(commit: GitCommit, renderOptions: IRenderOptions, now: number): DecorationOptions {
return { const decoration = {
hoverMessage: this.getHoverMessage(commit, dateFormat), renderOptions: { before: { ...renderOptions.before } }
renderOptions: heatmap ? { before: { ...renderOptions.before } } : undefined
} as DecorationOptions; } as DecorationOptions;
this.applyHeatmap(decoration, commit.date, now);
return decoration;
} }
static hoverRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions { static hoverRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {

View File

@@ -1,13 +1,15 @@
'use strict'; 'use strict';
import { Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode'; import { CancellationToken, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider'; import { AnnotationProviderBase } from './annotationProvider';
import { GitBlame, GitService, GitUri } from '../gitService'; import { Annotations, endOfLineIndex } from './annotations';
import { GitBlame, GitCommit, GitService, GitUri } from '../gitService';
import { WhitespaceController } from './whitespaceController'; import { WhitespaceController } from './whitespaceController';
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase { export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase implements HoverProvider {
protected _blame: Promise<GitBlame | undefined>; protected _blame: Promise<GitBlame | undefined>;
protected _hoverProviderDisposable: Disposable;
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) { constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) {
super(context, editor, decoration, highlightDecoration, whitespaceController); super(context, editor, decoration, highlightDecoration, whitespaceController);
@@ -15,6 +17,11 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
this._blame = this.git.getBlameForFile(this.uri); this._blame = this.git.getBlameForFile(this.uri);
} }
async clear() {
this._hoverProviderDisposable && this._hoverProviderDisposable.dispose();
super.clear();
}
async selection(shaOrLine?: string | number, blame?: GitBlame) { async selection(shaOrLine?: string | number, blame?: GitBlame) {
if (!this.highlightDecoration) return; if (!this.highlightDecoration) return;
@@ -56,6 +63,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
const blame = await this._blame; const blame = await this._blame;
return blame !== undefined && blame.lines.length !== 0; return blame !== undefined && blame.lines.length !== 0;
} }
protected async getBlame(requiresWhitespaceHack: boolean): Promise<GitBlame | undefined> { protected async getBlame(requiresWhitespaceHack: boolean): Promise<GitBlame | undefined> {
let whitespacePromise: Promise<void> | undefined; let whitespacePromise: Promise<void> | undefined;
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off) // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off)
@@ -64,18 +72,47 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
} }
let blame: GitBlame | undefined; let blame: GitBlame | undefined;
if (whitespacePromise) { if (whitespacePromise !== undefined) {
[blame] = await Promise.all([this._blame, whitespacePromise]); [blame] = await Promise.all([this._blame, whitespacePromise]);
} }
else { else {
blame = await this._blame; blame = await this._blame;
} }
if (blame === undefined || !blame.lines.length) { if (blame === undefined || blame.lines.length === 0) {
this.whitespaceController && await this.whitespaceController.restore(); this.whitespaceController && await this.whitespaceController.restore();
return undefined; return undefined;
} }
return blame; return blame;
} }
registerHoverProvider() {
this._hoverProviderDisposable = languages.registerHoverProvider({ pattern: this.uri.fsPath }, this);
}
async provideHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> {
// Avoid double annotations if we are showing the whole-file hover blame annotations
if (this._config.blame.line.enabled && this.editor.selection.start.line === position.line) return undefined;
const cfg = this._config.annotations.file.gutter;
if (!cfg.hover.wholeLine && position.character !== 0) return undefined;
const blame = await this.getBlame(true);
if (blame === undefined) return undefined;
const line = blame.lines[position.line - this.uri.offset];
const commit = blame.commits.get(line.sha);
if (commit === undefined) return undefined;
// Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined;
if (!commit.isUncommitted) {
logCommit = await this.git.getLogCommit(commit.repoPath, commit.uri.fsPath, commit.sha);
}
const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes(commit.repoPath));
return new Hover(message, document.validateRange(new Range(position.line, 0, position.line, endOfLineIndex)));
}
} }

View File

@@ -2,11 +2,11 @@
import { Strings } from '../system'; import { Strings } from '../system';
import { DecorationOptions, Range } from 'vscode'; import { DecorationOptions, Range } from 'vscode';
import { FileAnnotationType } from './annotationController'; import { FileAnnotationType } from './annotationController';
import { Annotations, endOfLineIndex } from './annotations'; import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { GitBlameCommit, ICommitFormatOptions } from '../gitService'; import { GitBlameCommit, ICommitFormatOptions } from '../gitService';
import * as moment from 'moment'; import { Logger } from '../logger';
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
@@ -16,7 +16,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
const blame = await this.getBlame(true); const blame = await this.getBlame(true);
if (blame === undefined) return false; if (blame === undefined) return false;
// console.time('Computing blame annotations...'); const start = process.hrtime();
const cfg = this._config.annotations.file.gutter; const cfg = this._config.annotations.file.gutter;
@@ -32,59 +32,53 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
tokenOptions: tokenOptions tokenOptions: tokenOptions
}; };
const now = moment(); const now = Date.now();
const offset = this.uri.offset; const offset = this.uri.offset;
const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap); const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap);
const dateFormat = this._config.defaultDateFormat;
const separateLines = this._config.theme.annotations.file.gutter.separateLines; const separateLines = this._config.theme.annotations.file.gutter.separateLines;
const decorations: DecorationOptions[] = []; const decorations: DecorationOptions[] = [];
const document = this.document; const decorationsMap: { [sha: string]: DecorationOptions | undefined } = Object.create(null);
let commit: GitBlameCommit | undefined; let commit: GitBlameCommit | undefined;
let compacted = false; let compacted = false;
let details: DecorationOptions | undefined;
let gutter: DecorationOptions | undefined; let gutter: DecorationOptions | undefined;
let previousSha: string | undefined; let previousSha: string | undefined;
for (const l of blame.lines) { for (const l of blame.lines) {
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
const line = l.line + offset; const line = l.line + offset;
if (previousSha === l.sha) { if (previousSha === l.sha) {
// Use a shallow copy of the previous decoration options // Use a shallow copy of the previous decoration options
gutter = { ...gutter } as DecorationOptions; gutter = { ...gutter } as DecorationOptions;
if (cfg.compact && !compacted) { if (cfg.compact && !compacted) {
// Since we are wiping out the contextText make sure to copy the objects // Since we are wiping out the contextText make sure to copy the objects
gutter.renderOptions = { ...gutter.renderOptions }; gutter.renderOptions = {
gutter.renderOptions.before = { ...gutter.renderOptions,
...gutter.renderOptions.before, before: {
...{ contentText: GlyphChars.Space.repeat(Strings.getWidth(gutter.renderOptions!.before!.contentText!)) } ...gutter.renderOptions!.before,
contentText: GlyphChars.Space.repeat(Strings.getWidth(gutter.renderOptions!.before!.contentText!))
}
}; };
if (separateLines) { if (separateLines) {
gutter.renderOptions.dark = { ...gutter.renderOptions.dark }; gutter.renderOptions.dark = {
gutter.renderOptions.dark.before = { ...gutter.renderOptions.dark.before, ...{ textDecoration: 'none' } }; ...gutter.renderOptions.dark,
gutter.renderOptions.light = { ...gutter.renderOptions.light }; before: { ...gutter.renderOptions.dark!.before, textDecoration: 'none' }
gutter.renderOptions.light.before = { ...gutter.renderOptions.light.before, ...{ textDecoration: 'none' } }; };
gutter.renderOptions.light = {
...gutter.renderOptions.light,
before: { ...gutter.renderOptions.light!.before, textDecoration: 'none' }
};
} }
compacted = true; compacted = true;
} }
const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex; gutter.range = new Range(line, 0, line, 0);
gutter.range = new Range(line, 0, line, endIndex);
decorations.push(gutter);
if (details !== undefined) { decorations.push(gutter);
details = { ...details } as DecorationOptions;
details.range = cfg.hover.wholeLine
? document.validateRange(new Range(line, 0, line, endOfLineIndex))
: gutter.range;
decorations.push(details);
}
continue; continue;
} }
@@ -92,30 +86,43 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
compacted = false; compacted = false;
previousSha = l.sha; previousSha = l.sha;
gutter = decorationsMap[l.sha];
if (gutter !== undefined) {
gutter = {
...gutter,
range: new Range(line, 0, line, 0)
} as DecorationOptions;
decorations.push(gutter);
continue;
}
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
gutter = Annotations.gutter(commit, cfg.format, options, renderOptions); gutter = Annotations.gutter(commit, cfg.format, options, renderOptions);
if (cfg.heatmap.enabled) { if (cfg.heatmap.enabled) {
Annotations.applyHeatmap(gutter, commit.date, now); Annotations.applyHeatmap(gutter, commit.date, now);
} }
const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex; gutter.range = new Range(line, 0, line, 0);
gutter.range = new Range(line, 0, line, endIndex);
decorations.push(gutter);
if (cfg.hover.details) { decorations.push(gutter);
details = Annotations.detailsHover(commit, dateFormat); decorationsMap[l.sha] = gutter;
details.range = cfg.hover.wholeLine
? document.validateRange(new Range(line, 0, line, endOfLineIndex))
: gutter.range;
decorations.push(details);
}
} }
if (decorations.length) { if (decorations.length) {
this.editor.setDecorations(this.decoration!, decorations); this.editor.setDecorations(this.decoration!, decorations);
} }
// console.timeEnd('Computing blame annotations...'); const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute gutter blame annotations`);
if (cfg.hover.details) {
this.registerHoverProvider();
}
this.selection(shaOrLine, blame); this.selection(shaOrLine, blame);
return true; return true;

View File

@@ -1,63 +1,70 @@
'use strict'; 'use strict';
import { DecorationOptions, Range } from 'vscode'; import { DecorationOptions, Range } from 'vscode';
import { FileAnnotationType } from './annotationController'; import { FileAnnotationType } from './annotationController';
import { Annotations, endOfLineIndex } from './annotations'; import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { GitBlameCommit } from '../gitService'; import { GitBlameCommit } from '../gitService';
import * as moment from 'moment'; import { Logger } from '../logger';
export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> { async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
this.annotationType = FileAnnotationType.Hover; this.annotationType = FileAnnotationType.Hover;
const blame = await this.getBlame(this._config.annotations.file.hover.heatmap.enabled);
if (blame === undefined) return false;
// console.time('Computing blame annotations...');
const cfg = this._config.annotations.file.hover; const cfg = this._config.annotations.file.hover;
const now = moment(); const blame = await this.getBlame(cfg.heatmap.enabled);
const offset = this.uri.offset; if (blame === undefined) return false;
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
const dateFormat = this._config.defaultDateFormat;
const decorations: DecorationOptions[] = []; if (cfg.heatmap.enabled) {
const document = this.document; const start = process.hrtime();
let commit: GitBlameCommit | undefined; const now = Date.now();
let hover: DecorationOptions | undefined; const offset = this.uri.offset;
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
for (const l of blame.lines) { const decorations: DecorationOptions[] = [];
commit = blame.commits.get(l.sha); const decorationsMap: { [sha: string]: DecorationOptions } = Object.create(null);
if (commit === undefined) continue;
const line = l.line + offset; let commit: GitBlameCommit | undefined;
let hover: DecorationOptions | undefined;
hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled, dateFormat); for (const l of blame.lines) {
const line = l.line + offset;
hover = decorationsMap[l.sha];
if (hover !== undefined) {
hover = {
...hover,
range: new Range(line, 0, line, 0)
} as DecorationOptions;
decorations.push(hover);
continue;
}
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
hover = Annotations.hover(commit, renderOptions, now);
hover.range = new Range(line, 0, line, 0);
decorations.push(hover);
decorationsMap[l.sha] = hover;
if (cfg.wholeLine) {
hover.range = document.validateRange(new Range(line, 0, line, endOfLineIndex));
}
else {
const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex;
hover.range = new Range(line, 0, line, endIndex);
} }
if (cfg.heatmap.enabled) { if (decorations.length) {
Annotations.applyHeatmap(hover, commit.date, now); this.editor.setDecorations(this.decoration!, decorations);
} }
decorations.push(hover); const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute hover blame annotations`);
} }
if (decorations.length) { this.registerHoverProvider();
this.editor.setDecorations(this.decoration!, decorations);
}
// console.timeEnd('Computing blame annotations...');
this.selection(shaOrLine, blame); this.selection(shaOrLine, blame);
return true; return true;
} }

View File

@@ -4,6 +4,7 @@ import { Annotations, endOfLineIndex } from './annotations';
import { FileAnnotationType } from './annotationController'; import { FileAnnotationType } from './annotationController';
import { AnnotationProviderBase } from './annotationProvider'; import { AnnotationProviderBase } from './annotationProvider';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger';
export class RecentChangesAnnotationProvider extends AnnotationProviderBase { export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
@@ -20,6 +21,8 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
const diff = await this.git.getDiffForFile(this.uri, commit.previousSha); const diff = await this.git.getDiffForFile(this.uri, commit.previousSha);
if (diff === undefined) return false; if (diff === undefined) return false;
const start = process.hrtime();
const cfg = this._config.annotations.file.recentChanges; const cfg = this._config.annotations.file.recentChanges;
const dateFormat = this._config.defaultDateFormat; const dateFormat = this._config.defaultDateFormat;
@@ -34,16 +37,11 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
if (line.state === 'unchanged') continue; if (line.state === 'unchanged') continue;
let endingIndex = 0; const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endOfLineIndex)));
if (cfg.hover.details || cfg.hover.changes) {
endingIndex = cfg.hover.wholeLine ? endOfLineIndex : this.editor.document.lineAt(count).firstNonWhitespaceCharacterIndex;
}
const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endingIndex)));
if (cfg.hover.details) { if (cfg.hover.details) {
decorators.push({ decorators.push({
hoverMessage: Annotations.getHoverMessage(commit, dateFormat), hoverMessage: Annotations.getHoverMessage(commit, dateFormat, this.git.hasRemotes(commit.repoPath)),
range: range range: range
} as DecorationOptions); } as DecorationOptions);
} }
@@ -62,6 +60,9 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
this.editor.setDecorations(this.highlightDecoration!, decorators); this.editor.setDecorations(this.highlightDecoration!, decorators);
const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute recent changes annotations`);
return true; return true;
} }

View File

@@ -12,6 +12,15 @@ export interface OpenCommitInRemoteCommandArgs {
export class OpenCommitInRemoteCommand extends ActiveEditorCommand { export class OpenCommitInRemoteCommand extends ActiveEditorCommand {
static getMarkdownCommandArgs(sha: string): string;
static getMarkdownCommandArgs(args: OpenCommitInRemoteCommandArgs): string;
static getMarkdownCommandArgs(argsOrSha: OpenCommitInRemoteCommandArgs | string): string {
const args = typeof argsOrSha === 'string'
? { sha: argsOrSha }
: argsOrSha;
return super.getMarkdownCommandArgsCore<OpenCommitInRemoteCommandArgs>(Commands.OpenCommitInRemote, args);
}
constructor(private git: GitService) { constructor(private git: GitService) {
super(Commands.OpenCommitInRemote); super(Commands.OpenCommitInRemote);
} }

View File

@@ -251,7 +251,6 @@ export interface IConfig {
hover: { hover: {
details: boolean; details: boolean;
changes: boolean; changes: boolean;
wholeLine: boolean;
}; };
}; };
}; };
@@ -318,6 +317,7 @@ export interface IConfig {
defaultDateFormat: string | null; defaultDateFormat: string | null;
gitExplorer: { gitExplorer: {
enabled: boolean;
view: GitExplorerView; view: GitExplorerView;
showTrackingBranch: boolean; showTrackingBranch: boolean;
commitFormat: string; commitFormat: string;

View File

@@ -80,6 +80,7 @@ export type GlyphChars = '\u21a9' |
'\u2192' | '\u2192' |
'\u21e8' | '\u21e8' |
'\u2191' | '\u2191' |
'\u2197' |
'\u2713' | '\u2713' |
'\u2014' | '\u2014' |
'\u2022' | '\u2022' |
@@ -95,6 +96,7 @@ export const GlyphChars = {
ArrowRight: '\u2192' as GlyphChars, ArrowRight: '\u2192' as GlyphChars,
ArrowRightHollow: '\u21e8' as GlyphChars, ArrowRightHollow: '\u21e8' as GlyphChars,
ArrowUp: '\u2191' as GlyphChars, ArrowUp: '\u2191' as GlyphChars,
ArrowUpRight: '\u2197' as GlyphChars,
Check: '\u2713' as GlyphChars, Check: '\u2713' as GlyphChars,
Dash: '\u2014' as GlyphChars, Dash: '\u2014' as GlyphChars,
Dot: '\u2022' as GlyphChars, Dot: '\u2022' as GlyphChars,

View File

@@ -295,12 +295,10 @@ export class CurrentLineController extends Disposable {
const decorationOptions: DecorationOptions[] = []; const decorationOptions: DecorationOptions[] = [];
let showChanges = false; let showChanges = false;
let showChangesStartIndex = 0;
let showChangesInStartingWhitespace = false;
let showDetails = false; let showDetails = false;
let showDetailsStartIndex = 0;
let showDetailsInStartingWhitespace = false; let showAtStart = false;
let showStartIndex = 0;
switch (state.annotationType) { switch (state.annotationType) {
case LineAnnotationType.Trailing: { case LineAnnotationType.Trailing: {
@@ -308,21 +306,7 @@ export class CurrentLineController extends Disposable {
showChanges = cfgAnnotations.hover.changes; showChanges = cfgAnnotations.hover.changes;
showDetails = cfgAnnotations.hover.details; showDetails = cfgAnnotations.hover.details;
showStartIndex = cfgAnnotations.hover.wholeLine ? 0 : endOfLineIndex;
if (cfgAnnotations.hover.wholeLine) {
showChangesStartIndex = 0;
showChangesInStartingWhitespace = false;
showDetailsStartIndex = 0;
showDetailsInStartingWhitespace = false;
}
else {
showChangesStartIndex = endOfLineIndex;
showChangesInStartingWhitespace = true;
showDetailsStartIndex = endOfLineIndex;
showDetailsInStartingWhitespace = true;
}
const decoration = Annotations.trailing(commit, cfgAnnotations.format, cfgAnnotations.dateFormat === null ? this._config.defaultDateFormat : cfgAnnotations.dateFormat, this._config.theme); const decoration = Annotations.trailing(commit, cfgAnnotations.format, cfgAnnotations.dateFormat === null ? this._config.defaultDateFormat : cfgAnnotations.dateFormat, this._config.theme);
decoration.range = editor.document.validateRange(new Range(line, endOfLineIndex, line, endOfLineIndex)); decoration.range = editor.document.validateRange(new Range(line, endOfLineIndex, line, endOfLineIndex));
@@ -334,12 +318,8 @@ export class CurrentLineController extends Disposable {
const cfgAnnotations = this._config.annotations.line.hover; const cfgAnnotations = this._config.annotations.line.hover;
showChanges = cfgAnnotations.changes; showChanges = cfgAnnotations.changes;
showChangesStartIndex = 0;
showChangesInStartingWhitespace = false;
showDetails = cfgAnnotations.details; showDetails = cfgAnnotations.details;
showDetailsStartIndex = 0; showStartIndex = 0;
showDetailsInStartingWhitespace = false;
break; break;
} }
@@ -348,25 +328,15 @@ export class CurrentLineController extends Disposable {
if (showDetails || showChanges) { if (showDetails || showChanges) {
const annotationType = this.annotationController.getAnnotationType(editor); const annotationType = this.annotationController.getAnnotationType(editor);
const firstNonWhitespace = editor.document.lineAt(line).firstNonWhitespaceCharacterIndex;
switch (annotationType) { switch (annotationType) {
case FileAnnotationType.Gutter: { case FileAnnotationType.Gutter: {
const cfgHover = this._config.annotations.file.gutter.hover; const cfgHover = this._config.annotations.file.gutter.hover;
if (cfgHover.details) { if (cfgHover.details) {
showDetailsInStartingWhitespace = false;
if (cfgHover.wholeLine) { if (cfgHover.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations showStartIndex = 0;
showDetails = false;
} }
else { else if (showStartIndex !== 0) {
if (showDetailsStartIndex === 0) { showAtStart = true;
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
} }
} }
@@ -374,20 +344,11 @@ export class CurrentLineController extends Disposable {
} }
case FileAnnotationType.Hover: { case FileAnnotationType.Hover: {
const cfgHover = this._config.annotations.file.hover; const cfgHover = this._config.annotations.file.hover;
showDetailsInStartingWhitespace = false;
if (cfgHover.wholeLine) { if (cfgHover.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations showStartIndex = 0;
showDetails = false;
showChangesStartIndex = 0;
} }
else { else if (showStartIndex !== 0) {
if (showDetailsStartIndex === 0) { showAtStart = true;
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
} }
break; break;
@@ -395,29 +356,21 @@ export class CurrentLineController extends Disposable {
case FileAnnotationType.RecentChanges: { case FileAnnotationType.RecentChanges: {
const cfgChanges = this._config.annotations.file.recentChanges.hover; const cfgChanges = this._config.annotations.file.recentChanges.hover;
if (cfgChanges.details) { if (cfgChanges.details) {
if (cfgChanges.wholeLine) { // Avoid double annotations if we are showing the whole-file hover blame annotations
// Avoid double annotations if we are showing the whole-file hover blame annotations showDetails = false;
showDetails = false;
}
else {
showDetailsInStartingWhitespace = false;
}
} }
if (cfgChanges.changes) { if (cfgChanges.changes) {
if (cfgChanges.wholeLine) { // Avoid double annotations if we are showing the whole-file hover blame annotations
// Avoid double annotations if we are showing the whole-file hover blame annotations showChanges = false;
showChanges = false;
}
else {
showChangesInStartingWhitespace = false;
}
} }
break; break;
} }
} }
const range = editor.document.validateRange(new Range(line, showStartIndex, line, endOfLineIndex));
if (showDetails) { if (showDetails) {
// Get the full commit message -- since blame only returns the summary // Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined; let logCommit: GitCommit | undefined = undefined;
@@ -425,29 +378,22 @@ export class CurrentLineController extends Disposable {
logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha); logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha);
} }
// I have no idea why I need this protection -- but it happens const decoration = Annotations.detailsHover(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes((logCommit || commit).repoPath));
if (editor.document === undefined) return; decoration.range = range;
const decoration = Annotations.detailsHover(logCommit || commit, this._config.defaultDateFormat);
decoration.range = editor.document.validateRange(new Range(line, showDetailsStartIndex, line, endOfLineIndex));
decorationOptions.push(decoration); decorationOptions.push(decoration);
if (showDetailsInStartingWhitespace && showDetailsStartIndex !== 0 && decoration.range.end.character !== 0) { if (showAtStart) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace)); decorationOptions.push(Annotations.withRange(decoration, 0, 0));
} }
} }
if (showChanges) { if (showChanges) {
const decoration = await Annotations.changesHover(commit, line, this._uri, this.git); const decoration = await Annotations.changesHover(commit, line, this._uri, this.git);
decoration.range = range;
// I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return;
decoration.range = editor.document.validateRange(new Range(line, showChangesStartIndex, line, endOfLineIndex));
decorationOptions.push(decoration); decorationOptions.push(decoration);
if (showChangesInStartingWhitespace && showChangesStartIndex !== 0 && decoration.range.end.character !== 0) { if (showAtStart) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace)); decorationOptions.push(Annotations.withRange(decoration, 0, 0));
} }
} }
} }

View File

@@ -2,7 +2,6 @@
import { Strings } from '../../system'; import { Strings } from '../../system';
import { GitCommit } from '../models/commit'; import { GitCommit } from '../models/commit';
import { Formatter, IFormatOptions } from './formatter'; import { Formatter, IFormatOptions } from './formatter';
import * as moment from 'moment';
import { GlyphChars } from '../../constants'; import { GlyphChars } from '../../constants';
export interface ICommitFormatOptions extends IFormatOptions { export interface ICommitFormatOptions extends IFormatOptions {
@@ -20,7 +19,7 @@ export interface ICommitFormatOptions extends IFormatOptions {
export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions> { export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions> {
get ago() { get ago() {
const ago = moment(this._item.date).fromNow(); const ago = this._item.fromNow();
return this._padOrTruncate(ago, this._options.tokenOptions!.ago); return this._padOrTruncate(ago, this._options.tokenOptions!.ago);
} }
@@ -30,17 +29,17 @@ export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions>
} }
get authorAgo() { get authorAgo() {
const authorAgo = `${this._item.author}, ${moment(this._item.date).fromNow()}`; const authorAgo = `${this._item.author}, ${this._item.fromNow()}`;
return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo); return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo);
} }
get date() { get date() {
const date = moment(this._item.date).format(this._options.dateFormat!); const date = this._item.formatDate(this._options.dateFormat!);
return this._padOrTruncate(date, this._options.tokenOptions!.date); return this._padOrTruncate(date, this._options.tokenOptions!.date);
} }
get id() { get id() {
return this._item.shortSha; return this._item.isUncommitted ? 'index' : this._item.shortSha;
} }
get message() { get message() {

View File

@@ -21,9 +21,9 @@ export * from './remotes/provider';
let git: IGit; let git: IGit;
// `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nparents %P%nsummary %B%nfilename ?` const defaultBlameParams = [`blame`, `--root`, `--incremental`];
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--date=iso8601`, `--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?`]; const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor %an%nauthor-date %at%nparents %P%nsummary %B%nfilename ?`];
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %ai%nreflog-selector %gd%nsummary %B%nfilename ?`]; const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?`];
let defaultEncoding = 'utf8'; let defaultEncoding = 'utf8';
export function setDefaultEncoding(encoding: string) { export function setDefaultEncoding(encoding: string) {
@@ -180,7 +180,7 @@ export class Git {
static blame(repoPath: string | undefined, fileName: string, sha?: string, options: { ignoreWhitespace?: boolean, startLine?: number, endLine?: number } = {}) { static blame(repoPath: string | undefined, fileName: string, sha?: string, options: { ignoreWhitespace?: boolean, startLine?: number, endLine?: number } = {}) {
const [file, root] = Git.splitPath(fileName, repoPath); const [file, root] = Git.splitPath(fileName, repoPath);
const params = [`blame`, `--root`, `--incremental`]; const params = [...defaultBlameParams];
if (options.ignoreWhitespace) { if (options.ignoreWhitespace) {
params.push('-w'); params.push('-w');

View File

@@ -42,7 +42,7 @@ export class GitUri extends Uri {
} }
else { else {
const commit = commitOrRepoPath; const commit = commitOrRepoPath;
base._fsPath = path.resolve(commit.repoPath, commit.originalFileName || commit.fileName); base._fsPath = path.resolve(commit.repoPath, commit.originalFileName || commit.fileName || '');
if (commit.repoPath !== undefined) { if (commit.repoPath !== undefined) {
this.repoPath = commit.repoPath; this.repoPath = commit.repoPath;

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Strings } from '../../system'; import { Dates, Strings } from '../../system';
import { Uri } from 'vscode'; import { Uri } from 'vscode';
import { GlyphChars } from '../../constants'; import { GlyphChars } from '../../constants';
import { Git } from '../git'; import { Git } from '../git';
@@ -70,7 +70,23 @@ export class GitCommit {
} }
get uri(): Uri { get uri(): Uri {
return Uri.file(path.resolve(this.repoPath, this.originalFileName || this.fileName)); return Uri.file(path.resolve(this.repoPath, this.originalFileName || this.fileName || ''));
}
private _dateFormatter?: Dates.IDateFormatter;
formatDate(format: string) {
if (this._dateFormatter === undefined) {
this._dateFormatter = Dates.toFormatter(this.date);
}
return this._dateFormatter.format(format);
}
fromNow() {
if (this._dateFormatter === undefined) {
this._dateFormatter = Dates.toFormatter(this.date);
}
return this._dateFormatter.fromNow();
} }
getFormattedPath(separator: string = Strings.pad(GlyphChars.Dot, 2, 2)): string { getFormattedPath(separator: string = Strings.pad(GlyphChars.Dot, 2, 2)): string {

View File

@@ -40,7 +40,12 @@ export class GitLogCommit extends GitCommit {
this.status = fileStatus.status; this.status = fileStatus.status;
} }
else { else {
this.fileStatuses = [{ status: status, fileName: fileName, originalFileName: originalFileName } as IGitStatusFile]; if (fileName === undefined) {
this.fileStatuses = [];
}
else {
this.fileStatuses = [{ status: status, fileName: fileName, originalFileName: originalFileName } as IGitStatusFile];
}
this.status = status; this.status = status;
} }
} }

View File

@@ -19,7 +19,7 @@ export interface GitStatus {
files: GitStatusFile[]; files: GitStatusFile[];
} }
export declare type GitStatusFileStatus = '!' | '?' | 'A' | 'C' | 'D' | 'M' | 'R' | 'U'; export declare type GitStatusFileStatus = '!' | '?' | 'A' | 'C' | 'D' | 'M' | 'R' | 'T' | 'U' | 'X' | 'B';
export interface IGitStatusFile { export interface IGitStatusFile {
status: GitStatusFileStatus; status: GitStatusFileStatus;
@@ -71,7 +71,10 @@ const statusOcticonsMap = {
D: '$(diff-removed)', D: '$(diff-removed)',
M: '$(diff-modified)', M: '$(diff-modified)',
R: '$(diff-renamed)', R: '$(diff-renamed)',
U: '$(question)' T: '$(diff-modified)',
U: '$(alert)',
X: '$(question)',
B: '$(question)'
}; };
export function getGitStatusOcticon(status: GitStatusFileStatus, missing: string = GlyphChars.Space.repeat(4)): string { export function getGitStatusOcticon(status: GitStatusFileStatus, missing: string = GlyphChars.Space.repeat(4)): string {
@@ -86,7 +89,10 @@ const statusIconsMap = {
D: 'icon-status-deleted.svg', D: 'icon-status-deleted.svg',
M: 'icon-status-modified.svg', M: 'icon-status-modified.svg',
R: 'icon-status-renamed.svg', R: 'icon-status-renamed.svg',
U: 'icon-status-conflict.svg' T: 'icon-status-modified.svg',
U: 'icon-status-conflict.svg',
X: 'icon-status-unknown.svg',
B: 'icon-status-unknown.svg'
}; };
export function getGitStatusIcon(status: GitStatusFileStatus): string { export function getGitStatusIcon(status: GitStatusFileStatus): string {

View File

@@ -1,7 +1,6 @@
'use strict'; 'use strict';
import { Strings } from '../../system'; import { Strings } from '../../system';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitCommitLine } from './../git'; import { Git, GitAuthor, GitBlame, GitBlameCommit, GitCommitLine } from './../git';
import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
interface BlameEntry { interface BlameEntry {
@@ -134,7 +133,7 @@ export class GitBlameParser {
} }
} }
commit = new GitBlameCommit(repoPath!, entry.sha, fileName!, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary!, []); commit = new GitBlameCommit(repoPath!, entry.sha, fileName!, entry.author, new Date(entry.authorDate as any * 1000), entry.summary!, []);
if (fileName !== entry.fileName) { if (fileName !== entry.fileName) {
commit.originalFileName = entry.fileName; commit.originalFileName = entry.fileName;

View File

@@ -3,7 +3,6 @@ import { Strings } from '../../system';
import { Range } from 'vscode'; import { Range } from 'vscode';
import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatus, IGitStatusFile } from './../git'; import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger'; // import { Logger } from '../../logger';
import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
interface LogEntry { interface LogEntry {
@@ -87,7 +86,7 @@ export class GitLogParser {
break; break;
case 'author-date': case 'author-date':
entry.authorDate = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`; entry.authorDate = lineParts[1];
break; break;
case 'parents': case 'parents':
@@ -231,7 +230,7 @@ export class GitLogParser {
} }
} }
commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName); commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, new Date(entry.authorDate! as any * 1000), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName);
commit.parentShas = entry.parentShas!; commit.parentShas = entry.parentShas!;
if (relativeFileName !== entry.fileName) { if (relativeFileName !== entry.fileName) {

View File

@@ -1,7 +1,6 @@
'use strict'; 'use strict';
import { Git, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git'; import { Git, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger'; // import { Logger } from '../../logger';
import * as moment from 'moment';
interface StashEntry { interface StashEntry {
sha: string; sha: string;
@@ -14,11 +13,33 @@ interface StashEntry {
export class GitStashParser { export class GitStashParser {
static parse(data: string, repoPath: string): GitStash | undefined {
const entries = this._parseEntries(data);
if (entries === undefined) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
let commit = commits.get(entry.sha);
if (commit === undefined) {
commit = new GitStashCommit(entry.stashName, repoPath, entry.sha, entry.fileNames, new Date(entry.date! as any * 1000), entry.summary, undefined, entry.fileStatuses) as GitStashCommit;
commits.set(entry.sha, commit);
}
}
return {
repoPath: repoPath,
commits: commits
} as GitStash;
}
private static _parseEntries(data: string): StashEntry[] | undefined { private static _parseEntries(data: string): StashEntry[] | undefined {
if (!data) return undefined; if (!data) return undefined;
const lines = data.split('\n'); const lines = data.split('\n');
if (!lines.length) return undefined; if (lines.length === 0) return undefined;
const entries: StashEntry[] = []; const entries: StashEntry[] = [];
@@ -42,7 +63,7 @@ export class GitStashParser {
switch (lineParts[0]) { switch (lineParts[0]) {
case 'author-date': case 'author-date':
entry.date = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`; entry.date = lineParts[1];
break; break;
case 'summary': case 'summary':
@@ -66,7 +87,12 @@ export class GitStashParser {
case 'filename': case 'filename':
const nextLine = lines[position + 1]; const nextLine = lines[position + 1];
// If the next line isn't blank, make sure it isn't starting a new commit // If the next line isn't blank, make sure it isn't starting a new commit
if (nextLine && Git.shaRegex.test(nextLine)) continue; if (nextLine && Git.shaRegex.test(nextLine)) {
entries.push(entry);
entry = undefined;
continue;
}
position++; position++;
@@ -109,28 +135,6 @@ export class GitStashParser {
return entries; return entries;
} }
static parse(data: string, repoPath: string): GitStash | undefined {
const entries = this._parseEntries(data);
if (entries === undefined) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
let commit = commits.get(entry.sha);
if (commit === undefined) {
commit = new GitStashCommit(entry.stashName, repoPath, entry.sha, entry.fileNames, moment(entry.date).toDate(), entry.summary, undefined, entry.fileStatuses) as GitStashCommit;
commits.set(entry.sha, commit);
}
}
return {
repoPath: repoPath,
commits: commits
} as GitStash;
}
private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) { private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) {
if (entry.fileName === undefined) return; if (entry.fileName === undefined) return;

View File

@@ -17,7 +17,7 @@ export class GitStatusParser {
if (!data) return undefined; if (!data) return undefined;
const lines = data.split('\n').filter(_ => !!_); const lines = data.split('\n').filter(_ => !!_);
if (!lines.length) return undefined; if (lines.length === 0) return undefined;
const status = { const status = {
branch: '', branch: '',

View File

@@ -1,7 +1,8 @@
'use strict'; 'use strict';
import { GitHubService } from './github'; import { Range } from 'vscode';
import { RemoteProvider } from './provider';
export class GitLabService extends GitHubService { export class GitLabService extends RemoteProvider {
constructor(public domain: string, public path: string, public custom: boolean = false) { constructor(public domain: string, public path: string, public custom: boolean = false) {
super(domain, path); super(domain, path);
@@ -10,4 +11,32 @@ export class GitLabService extends GitHubService {
get name() { get name() {
return this.formatName('GitLab'); return this.formatName('GitLab');
} }
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}
protected getUrlForBranch(branch: string): string {
return `${this.baseUrl}/commits/${branch}`;
}
protected getUrlForCommit(sha: string): string {
return `${this.baseUrl}/commit/${sha}`;
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = `#L${range.start.line}`;
}
else {
line = `#L${range.start.line}-${range.end.line}`;
}
}
if (sha) return `${this.baseUrl}/blob/${sha}/${fileName}${line}`;
if (branch) return `${this.baseUrl}/blob/${branch}/${fileName}${line}`;
return `${this.baseUrl}?path=${fileName}${line}`;
}
} }

View File

@@ -6,7 +6,6 @@ import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants';
import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration'; import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration';
import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService'; import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService';
import { Logger } from './logger'; import { Logger } from './logger';
import * as moment from 'moment';
export class GitRecentChangeCodeLens extends CodeLens { export class GitRecentChangeCodeLens extends CodeLens {
@@ -254,7 +253,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (blame === undefined) return lens; if (blame === undefined) return lens;
const recentCommit = Iterables.first(blame.commits.values()); const recentCommit = Iterables.first(blame.commits.values());
title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; title = `${recentCommit.author}, ${recentCommit.fromNow()}`;
if (this._config.codeLens.debug) { if (this._config.codeLens.debug) {
title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`; title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`;
} }

View File

@@ -9,7 +9,6 @@ import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri';
import { Logger } from './logger'; import { Logger } from './logger';
import * as fs from 'fs'; import * as fs from 'fs';
import * as ignore from 'ignore'; import * as ignore from 'ignore';
import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
export { GitUri, IGitCommitInfo }; export { GitUri, IGitCommitInfo };
@@ -75,6 +74,7 @@ export const RepoChangedReasons = {
export class GitService extends Disposable { export class GitService extends Disposable {
static fakeSha = 'ffffffffffffffffffffffffffffffffffffffff'; static fakeSha = 'ffffffffffffffffffffffffffffffffffffffff';
static uncommittedSha = '0000000000000000000000000000000000000000';
private _onDidBlameFail = new EventEmitter<string>(); private _onDidBlameFail = new EventEmitter<string>();
get onDidBlameFail(): Event<string> { get onDidBlameFail(): Event<string> {
@@ -167,7 +167,6 @@ export class GitService extends Disposable {
this._repoWatcher = undefined; this._repoWatcher = undefined;
this._gitCache.clear(); this._gitCache.clear();
this._remotesCache.clear();
} }
this._gitignore = new Promise<ignore.Ignore | undefined>((resolve, reject) => { this._gitignore = new Promise<ignore.Ignore | undefined>((resolve, reject) => {
@@ -568,7 +567,7 @@ export class GitService extends Disposable {
Iterables.forEach(blame.commits.values(), (c, i) => { Iterables.forEach(blame.commits.values(), (c, i) => {
if (c.isUncommitted) return; if (c.isUncommitted) return;
const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${moment(c.date).format(dateFormat)}`; const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${c.formatDate(dateFormat)}`;
const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration, dateFormat); const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration, dateFormat);
locations.push(new Location(uri, new Position(0, 0))); locations.push(new Location(uri, new Position(0, 0)));
if (c.sha === selectedSha) { if (c.sha === selectedSha) {
@@ -888,7 +887,7 @@ export class GitService extends Disposable {
Iterables.forEach(log.commits.values(), (c, i) => { Iterables.forEach(log.commits.values(), (c, i) => {
if (c.isUncommitted) return; if (c.isUncommitted) return;
const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${moment(c.date).format(dateFormat)}`; const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${c.formatDate(dateFormat)}`;
const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration, dateFormat); const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration, dateFormat);
locations.push(new Location(uri, new Position(0, 0))); locations.push(new Location(uri, new Position(0, 0)));
if (c.sha === selectedSha) { if (c.sha === selectedSha) {
@@ -899,21 +898,26 @@ export class GitService extends Disposable {
return locations; return locations;
} }
hasRemotes(repoPath: string): boolean {
const remotes = this._remotesCache.get(repoPath);
return remotes !== undefined && remotes.length > 0;
}
async getRemotes(repoPath: string): Promise<GitRemote[]> { async getRemotes(repoPath: string): Promise<GitRemote[]> {
if (!repoPath) return []; if (!repoPath) return [];
Logger.log(`getRemotes('${repoPath}')`); Logger.log(`getRemotes('${repoPath}')`);
if (this.UseCaching) { let remotes = this._remotesCache.get(repoPath);
const remotes = this._remotesCache.get(repoPath); if (remotes !== undefined) return remotes;
if (remotes !== undefined) return remotes;
}
const data = await Git.remote(repoPath); const data = await Git.remote(repoPath);
const remotes = GitRemoteParser.parse(data, repoPath); remotes = GitRemoteParser.parse(data, repoPath);
if (this.UseCaching) {
if (remotes !== undefined) {
this._remotesCache.set(repoPath, remotes); this._remotesCache.set(repoPath, remotes);
} }
return remotes; return remotes;
} }
@@ -1138,7 +1142,7 @@ export class GitService extends Disposable {
} }
// NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location
return Uri.parse(`${scheme}:${pad(data.index || 0)} ${GlyphChars.Dot} ${encodeURIComponent(message)} ${GlyphChars.Dot} ${moment(commit.date).format(dateFormat)} ${GlyphChars.Dot} ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`); return Uri.parse(`${scheme}:${pad(data.index || 0)} ${GlyphChars.Dot} ${encodeURIComponent(message)} ${GlyphChars.Dot} ${commit.formatDate(dateFormat)} ${GlyphChars.Dot} ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`);
} }
private static _toGitUriData<T extends IGitUriData>(commit: IGitUriData, index?: number, originalFileName?: string, decoration?: string): T { private static _toGitUriData<T extends IGitUriData>(commit: IGitUriData, index?: number, originalFileName?: string, decoration?: string): T {

View File

@@ -3,7 +3,6 @@ import { commands, ExtensionContext, Uri, window } from 'vscode';
import { BuiltInCommands } from './constants'; import { BuiltInCommands } from './constants';
import { GitCommit } from './gitService'; import { GitCommit } from './gitService';
import { Logger } from './logger'; import { Logger } from './logger';
import * as moment from 'moment';
export type SuppressedKeys = 'suppressCommitHasNoPreviousCommitWarning' | export type SuppressedKeys = 'suppressCommitHasNoPreviousCommitWarning' |
'suppressCommitNotFoundWarning' | 'suppressCommitNotFoundWarning' |
@@ -34,7 +33,7 @@ export class Messages {
static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise<string | undefined> { static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise<string | undefined> {
if (commit === undefined) return Messages._showMessage('info', `Commit has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning); if (commit === undefined) return Messages._showMessage('info', `Commit has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
return Messages._showMessage('info', `Commit ${commit.shortSha} (${commit.author}, ${moment(commit.date).fromNow()}) has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning); return Messages._showMessage('info', `Commit ${commit.shortSha} (${commit.author}, ${commit.fromNow()}) has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
} }
static showCommitNotFoundWarningMessage(message: string): Promise<string | undefined> { static showCommitNotFoundWarningMessage(message: string): Promise<string | undefined> {

View File

@@ -7,7 +7,6 @@ import { GlyphChars } from '../constants';
import { getGitStatusOcticon, GitCommit, GitLog, GitLogCommit, GitService, GitStashCommit, GitStatusFile, GitStatusFileStatus, GitUri, IGitCommitInfo, IGitStatusFile, RemoteResource } from '../gitService'; import { getGitStatusOcticon, GitCommit, GitLog, GitLogCommit, GitService, GitStashCommit, GitStatusFile, GitStatusFileStatus, GitUri, IGitCommitInfo, IGitStatusFile, RemoteResource } from '../gitService';
import { Keyboard, KeyCommand, KeyNoopCommand, Keys } from '../keyboard'; import { Keyboard, KeyCommand, KeyNoopCommand, Keys } from '../keyboard';
import { OpenRemotesCommandQuickPickItem } from './remotes'; import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickItem { export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickItem {
@@ -299,7 +298,7 @@ export class CommitDetailsQuickPick {
const pick = await window.showQuickPick(items, { const pick = await window.showQuickPick(items, {
matchOnDescription: true, matchOnDescription: true,
matchOnDetail: true, matchOnDetail: true,
placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author ? `${commit.author}, ` : ''}${moment(commit.date).fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`, placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author ? `${commit.author}, ` : ''}${commit.fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`,
ignoreFocusOut: getQuickPickIgnoreFocusOut(), ignoreFocusOut: getQuickPickIgnoreFocusOut(),
onDidSelectItem: (item: QuickPickItem) => { onDidSelectItem: (item: QuickPickItem) => {
scope.setKeyCommand('right', item); scope.setKeyCommand('right', item);

View File

@@ -7,7 +7,6 @@ import { GlyphChars } from '../constants';
import { GitLog, GitLogCommit, GitService, GitUri, RemoteResource } from '../gitService'; import { GitLog, GitLogCommit, GitService, GitUri, RemoteResource } from '../gitService';
import { Keyboard, KeyCommand, KeyNoopCommand } from '../keyboard'; import { Keyboard, KeyCommand, KeyNoopCommand } from '../keyboard';
import { OpenRemotesCommandQuickPickItem } from './remotes'; import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
export class OpenCommitFileCommandQuickPickItem extends OpenFileCommandQuickPickItem { export class OpenCommitFileCommandQuickPickItem extends OpenFileCommandQuickPickItem {
@@ -275,7 +274,7 @@ export class CommitFileDetailsQuickPick {
const pick = await window.showQuickPick(items, { const pick = await window.showQuickPick(items, {
matchOnDescription: true, matchOnDescription: true,
placeHolder: `${commit.getFormattedPath()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${isUncommitted ? `Uncommitted ${GlyphChars.ArrowRightHollow} ` : '' }${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author}, ${moment(commit.date).fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`, placeHolder: `${commit.getFormattedPath()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${isUncommitted ? `Uncommitted ${GlyphChars.ArrowRightHollow} ` : '' }${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author}, ${commit.fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`,
ignoreFocusOut: getQuickPickIgnoreFocusOut(), ignoreFocusOut: getQuickPickIgnoreFocusOut(),
onDidSelectItem: (item: QuickPickItem) => { onDidSelectItem: (item: QuickPickItem) => {
scope.setKeyCommand('right', item as KeyCommand); scope.setKeyCommand('right', item as KeyCommand);

View File

@@ -7,7 +7,6 @@ import { GlyphChars } from '../constants';
import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService'; import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService';
import { Keyboard, KeyboardScope, KeyMapping, Keys } from '../keyboard'; import { Keyboard, KeyboardScope, KeyMapping, Keys } from '../keyboard';
// import { Logger } from '../logger'; // import { Logger } from '../logger';
import * as moment from 'moment';
export function getQuickPickIgnoreFocusOut() { export function getQuickPickIgnoreFocusOut() {
const cfg = workspace.getConfiguration(ExtensionKey).get<IAdvancedConfig>('advanced')!; const cfg = workspace.getConfiguration(ExtensionKey).get<IAdvancedConfig>('advanced')!;
@@ -174,12 +173,12 @@ export class CommitQuickPickItem implements QuickPickItem {
if (commit instanceof GitStashCommit) { if (commit instanceof GitStashCommit) {
this.label = message; this.label = message;
this.description = ''; this.description = '';
this.detail = `${GlyphChars.Space} ${commit.stashName} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${moment(commit.date).fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getDiffStatus()}`; this.detail = `${GlyphChars.Space} ${commit.stashName} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getDiffStatus()}`;
} }
else { else {
this.label = message; this.label = message;
this.description = `${Strings.pad('$(git-commit)', 1, 1)} ${commit.shortSha}`; this.description = `${Strings.pad('$(git-commit)', 1, 1)} ${commit.shortSha}`;
this.detail = `${GlyphChars.Space} ${commit.author}, ${moment(commit.date).fromNow()}${(commit.type === 'branch') ? ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${(commit as GitLogCommit).getDiffStatus()}` : ''}`; this.detail = `${GlyphChars.Space} ${commit.author}, ${commit.fromNow()}${(commit.type === 'branch') ? ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${(commit as GitLogCommit).getDiffStatus()}` : ''}`;
} }
} }
} }

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
export * from './system/array'; export * from './system/array';
export * from './system/date';
// export * from './system/disposable'; // export * from './system/disposable';
// export * from './system/element'; // export * from './system/element';
// export * from './system/event'; // export * from './system/event';

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
export namespace Arrays { export namespace Arrays {
export function groupBy<T>(array: T[], accessor: (item: T) => any): T[] { export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } {
return array.reduce((previous, current) => { return array.reduce((previous, current) => {
const value = accessor(current); const value = accessor(current);
previous[value] = previous[value] || []; previous[value] = previous[value] || [];

33
src/system/date.ts Normal file
View File

@@ -0,0 +1,33 @@
'use strict';
import * as moment from 'moment';
const MillisecondsPerMinute = 60000; // 60 * 1000
const MillisecondsPerDay = 86400000; // 24 * 60 * 60 * 1000
export namespace Dates {
export interface IDateFormatter {
fromNow: () => string;
format: (format: string) => string;
}
export function dateDaysFromNow(date: Date, now: number = Date.now()) {
const startOfDayLeft = startOfDay(now);
const startOfDayRight = startOfDay(date);
const timestampLeft = startOfDayLeft.getTime() - startOfDayLeft.getTimezoneOffset() * MillisecondsPerMinute;
const timestampRight = startOfDayRight.getTime() - startOfDayRight.getTimezoneOffset() * MillisecondsPerMinute;
return Math.round((timestampLeft - timestampRight) / MillisecondsPerDay);
}
export function startOfDay(date: Date | number) {
const newDate = new Date(typeof date === 'number' ? date : date.getTime());
newDate.setHours(0, 0, 0, 0);
return newDate;
}
export function toFormatter(date: Date): IDateFormatter {
return moment(date);
}
}

View File

@@ -3,7 +3,7 @@ import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode'; import { CommitNode } from './commitNode';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType, ShowAllCommitsNode } from './explorerNode'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitBranch, GitService, GitUri } from '../gitService'; import { GitBranch, GitService, GitUri } from '../gitService';
export class BranchHistoryNode extends ExplorerNode { export class BranchHistoryNode extends ExplorerNode {
@@ -12,7 +12,12 @@ export class BranchHistoryNode extends ExplorerNode {
maxCount: number | undefined = undefined; maxCount: number | undefined = undefined;
constructor(public readonly branch: GitBranch, uri: GitUri, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
public readonly branch: GitBranch,
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
@@ -20,10 +25,11 @@ export class BranchHistoryNode extends ExplorerNode {
const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount); const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount);
if (log === undefined) return []; if (log === undefined) return [];
const children = Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git, this.branch)); const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git, this.branch))];
if (!log.truncated) return [...children]; if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.context));
return [...children, new ShowAllCommitsNode(this, this.context)]; }
return children;
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {

View File

@@ -9,7 +9,11 @@ export class BranchesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:branches'; readonly resourceType: ResourceType = 'gitlens:branches';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
@@ -18,7 +22,7 @@ export class BranchesNode extends ExplorerNode {
if (branches === undefined) return []; if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name)); 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, this.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))];
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {

View File

@@ -2,19 +2,36 @@
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService'; import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, StatusFileFormatter } from '../gitService';
import * as path from 'path'; import * as path from 'path';
export enum CommitFileNodeDisplayAs {
CommitLabel = 1 << 0,
CommitIcon = 1 << 1,
FileLabel = 1 << 2,
StatusIcon = 1 << 3,
Commit = CommitLabel | CommitIcon,
File = FileLabel | StatusIcon
}
export class CommitFileNode extends ExplorerNode { export class CommitFileNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:commit-file'; readonly resourceType: ResourceType = 'gitlens:commit-file';
constructor(public readonly status: IGitStatusFile, public commit: GitCommit, protected readonly context: ExtensionContext, protected readonly git: GitService, public readonly branch?: GitBranch) { constructor(
public readonly status: IGitStatusFile,
public commit: GitCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
private displayAs: CommitFileNodeDisplayAs = CommitFileNodeDisplayAs.Commit,
public readonly branch?: GitBranch
) {
super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha })); super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha }));
} }
getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
return Promise.resolve([]); return [];
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
@@ -25,10 +42,20 @@ export class CommitFileNode extends ExplorerNode {
} }
} }
const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.None); const label = (this.displayAs & CommitFileNodeDisplayAs.CommitLabel)
? CommitFormatter.fromTemplate(this.getCommitTemplate(), this.commit, {
truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions)
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status);
const item = new TreeItem(label, TreeItemCollapsibleState.None);
item.contextValue = this.resourceType; item.contextValue = this.resourceType;
const icon = getGitStatusIcon(this.status.status); const icon = (this.displayAs & CommitFileNodeDisplayAs.CommitIcon)
? 'icon-commit.svg'
: getGitStatusIcon(this.status.status);
item.iconPath = { item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon)) light: this.context.asAbsolutePath(path.join('images', 'light', icon))
@@ -39,6 +66,14 @@ export class CommitFileNode extends ExplorerNode {
return item; return item;
} }
protected getCommitTemplate() {
return this.git.config.gitExplorer.commitFormat;
}
protected getCommitFileTemplate() {
return this.git.config.gitExplorer.commitFileFormat;
}
getCommand(): Command | undefined { getCommand(): Command | undefined {
return { return {
title: 'Compare File with Previous Revision', title: 'Compare File with Previous Revision',

View File

@@ -2,58 +2,44 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as path from 'path';
export class CommitNode extends ExplorerNode { export class CommitNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:commit'; readonly resourceType: ResourceType = 'gitlens:commit';
constructor(public readonly commit: GitLogCommit, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService, public readonly branch?: GitBranch) { constructor(
public readonly commit: GitLogCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
public readonly branch?: GitBranch
) {
super(new GitUri(commit.uri, commit)); super(new GitUri(commit.uri, commit));
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
if (this.commit.type === 'file') Promise.resolve([]);
const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1); const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1);
if (log === undefined) return []; if (log === undefined) return [];
const commit = Iterables.first(log.commits.values()); const commit = Iterables.first(log.commits.values());
if (commit === undefined) return []; if (commit === undefined) return [];
return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, this.branch))]; return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))];
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {
const item = new TreeItem(CommitFormatter.fromTemplate(this.template, this.commit, { const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.commitFormat, this.commit, {
truncateMessageAtNewLine: true, truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions)); } as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed);
if (this.commit.type === 'file') { item.contextValue = this.resourceType;
item.collapsibleState = TreeItemCollapsibleState.None; item.iconPath = {
item.command = this.getCommand(); dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'),
const resourceType: ResourceType = 'gitlens:commit-file'; light: this.context.asAbsolutePath('images/light/icon-commit.svg')
item.contextValue = resourceType; };
const icon = getGitStatusIcon(this.commit.status!);
item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon))
};
}
else {
item.collapsibleState = TreeItemCollapsibleState.Collapsed;
item.contextValue = this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'),
light: this.context.asAbsolutePath('images/light/icon-commit.svg')
};
}
return item; return item;
} }

View File

@@ -20,6 +20,9 @@ export declare type ResourceType =
'gitlens:stash-file' | 'gitlens:stash-file' |
'gitlens:stashes' | 'gitlens:stashes' |
'gitlens:status' | 'gitlens:status' |
'gitlens:status-file' |
'gitlens:status-files' |
'gitlens:status-file-commits' |
'gitlens:status-upstream'; 'gitlens:status-upstream';
export abstract class ExplorerNode { export abstract class ExplorerNode {
@@ -88,11 +91,11 @@ export class PagerNode extends ExplorerNode {
} }
} }
export class ShowAllCommitsNode extends PagerNode { export class ShowAllNode extends PagerNode {
args: RefreshNodeCommandArgs = { maxCount: 0 }; args: RefreshNodeCommandArgs = { maxCount: 0 };
constructor(node: ExplorerNode, context: ExtensionContext) { constructor(message: string, node: ExplorerNode, context: ExtensionContext) {
super(`Show All Commits ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context); super(`${message} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context);
} }
} }

View File

@@ -10,8 +10,10 @@ export * from './historyNode';
export * from './remoteNode'; export * from './remoteNode';
export * from './remotesNode'; export * from './remotesNode';
export * from './repositoryNode'; export * from './repositoryNode';
export * from './stashesNode';
export * from './stashFileNode'; export * from './stashFileNode';
export * from './stashNode'; export * from './stashNode';
export * from './stashesNode'; export * from './statusFileCommitsNode';
export * from './statusFilesNode';
export * from './statusNode'; export * from './statusNode';
export * from './statusUpstreamNode'; export * from './statusUpstreamNode';

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
import { Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
@@ -9,15 +9,19 @@ export class FileHistoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:file-history'; readonly resourceType: ResourceType = 'gitlens:file-history';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha);
if (log === undefined) return [new MessageNode('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.gitExplorer.commitFormat, this.context, this.git))]; return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.context, this.git, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))];
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {

View File

@@ -8,9 +8,13 @@ export class HistoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:history'; readonly resourceType: ResourceType = 'gitlens:history';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
return [new FileHistoryNode(this.uri, this.context, this.git)]; return [new FileHistoryNode(this.uri, this.context, this.git)];

View File

@@ -10,7 +10,12 @@ export class RemoteNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:remote'; readonly resourceType: ResourceType = 'gitlens:remote';
constructor(public readonly remote: GitRemote, uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
public readonly remote: GitRemote,
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
@@ -19,7 +24,7 @@ export class RemoteNode extends ExplorerNode {
if (branches === undefined) return []; if (branches === undefined) return [];
branches.sort((a, b) => a.name.localeCompare(b.name)); 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.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))];
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {

View File

@@ -9,7 +9,11 @@ export class RemotesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:remotes'; readonly resourceType: ResourceType = 'gitlens:remotes';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }

View File

@@ -12,9 +12,13 @@ export class RepositoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:repository'; readonly resourceType: ResourceType = 'gitlens:repository';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
return [ return [

View File

@@ -2,13 +2,26 @@
import { ExtensionContext } from 'vscode'; import { ExtensionContext } from 'vscode';
import { ResourceType } from './explorerNode'; import { ResourceType } from './explorerNode';
import { GitService, GitStashCommit, IGitStatusFile } from '../gitService'; import { GitService, GitStashCommit, IGitStatusFile } from '../gitService';
import { CommitFileNode } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
export class StashFileNode extends CommitFileNode { export class StashFileNode extends CommitFileNode {
readonly resourceType: ResourceType = 'gitlens:stash-file'; readonly resourceType: ResourceType = 'gitlens:stash-file';
constructor(readonly status: IGitStatusFile, readonly commit: GitStashCommit, readonly context: ExtensionContext, readonly git: GitService) { constructor(
super(status, commit, context, git); readonly status: IGitStatusFile,
readonly commit: GitStashCommit,
readonly context: ExtensionContext,
readonly git: GitService
) {
super(status, commit, context, git, CommitFileNodeDisplayAs.File);
}
protected getCommitTemplate() {
return this.git.config.gitExplorer.stashFormat;
}
protected getCommitFileTemplate() {
return this.git.config.gitExplorer.stashFileFormat;
} }
} }

View File

@@ -1,4 +1,5 @@
'use strict'; 'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, GitService, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService'; import { CommitFormatter, GitService, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService';
@@ -8,12 +9,29 @@ export class StashNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:stash'; readonly resourceType: ResourceType = 'gitlens:stash';
constructor(public readonly commit: GitStashCommit, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
public readonly commit: GitStashCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(new GitUri(commit.uri, commit)); super(new GitUri(commit.uri, commit));
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
return Promise.resolve((this.commit as GitStashCommit).fileStatuses.map(s => new StashFileNode(s, this.commit, this.context, this.git))); const statuses = (this.commit as GitStashCommit).fileStatuses;
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
const log = await this.git.getLogForRepo(this.commit.repoPath, `${(this.commit as GitStashCommit).stashName}^3`, 1);
if (log !== undefined) {
const commit = Iterables.first(log.commits.values());
if (commit !== undefined && commit.fileStatuses.length !== 0) {
// Since these files are untracked -- make them look that way
commit.fileStatuses.forEach(s => s.status = '?');
statuses.splice(statuses.length, 0, ...commit.fileStatuses);
}
}
return statuses.map(s => new StashFileNode(s, this.commit, this.context, this.git));
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {

View File

@@ -9,9 +9,13 @@ export class StashesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:stashes'; readonly resourceType: ResourceType = 'gitlens:stashes';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
const stash = await this.git.getStashList(this.uri.repoPath!); const stash = await this.git.getStashList(this.uri.repoPath!);

View File

@@ -0,0 +1,68 @@
'use strict';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService';
import * as path from 'path';
export class StatusFileCommitsNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status-file-commits';
constructor(
repoPath: string,
public readonly status: IGitStatusFile,
public commits: GitLogCommit[],
protected readonly context: ExtensionContext,
protected readonly git: GitService,
public readonly branch?: GitBranch
) {
super(new GitUri(Uri.file(path.resolve(repoPath, status.fileName)), { repoPath: repoPath, fileName: status.fileName, sha: 'HEAD' }));
}
async getChildren(): Promise<ExplorerNode[]> {
return this.commits.map(c => new CommitFileNode(this.status, c, this.context, this.git, CommitFileNodeDisplayAs.Commit, this.branch));
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
const icon = getGitStatusIcon(this.status.status);
item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon))
};
if (this.commits.length === 1 && this.commits[0].isUncommitted) {
item.collapsibleState = TreeItemCollapsibleState.None;
item.contextValue = 'gitlens:status-file' as ResourceType;
item.command = this.getCommand();
}
return item;
}
get commit() {
return this.commits[0];
}
getCommand(): Command | undefined {
return {
title: 'Compare File with Previous Revision',
command: Commands.DiffWithPrevious,
arguments: [
GitUri.fromFileStatus(this.status, this.uri.repoPath!),
{
commit: this.commit,
line: 0,
showOptions: {
preserveFocus: true,
preview: true
}
} as DiffWithPreviousCommandArgs
]
};
}
}

View File

@@ -0,0 +1,70 @@
'use strict';
import { Arrays, Iterables, Objects } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFile } from '../gitService';
import { StatusFileCommitsNode } from './statusFileCommitsNode';
interface IGitStatusFileWithCommit extends IGitStatusFile {
commit: GitLogCommit;
}
export class StatusFilesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status-files';
maxCount: number | undefined = undefined;
constructor(
public readonly status: GitStatus,
public readonly range: string | undefined,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
public readonly branch?: GitBranch
) {
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
}
async getChildren(): Promise<ExplorerNode[]> {
let statuses: IGitStatusFileWithCommit[];
let log: GitLog | undefined;
if (this.range !== undefined) {
log = await this.git.getLogForRepo(this.status.repoPath, this.range, this.maxCount);
if (log === undefined) return [];
statuses = Array.from(Iterables.flatMap(log.commits.values(), c => {
return c.fileStatuses.map(s => {
return { ...s, commit: c } as IGitStatusFileWithCommit;
});
}));
}
else {
statuses = [];
}
if (this.status.files.length !== 0) {
statuses.splice(0, 0, ...this.status.files.map(s => {
return { ...s, commit: new GitLogCommit('file', this.status.repoPath, GitService.uncommittedSha, s.fileName, 'You', new Date(), '', s.status, [s], s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit;
}));
}
statuses.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
const groups = Arrays.groupBy(statuses, s => s.fileName);
const children: (StatusFileCommitsNode | ShowAllNode)[] = [
...Iterables.map(Objects.values<IGitStatusFileWithCommit[]>(groups),
statuses => new StatusFileCommitsNode(this.uri.repoPath!, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch))
];
if (log !== undefined && log.truncated) {
children.push(new ShowAllNode('Show All Changes', this, this.context));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Changed Files`, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
return item;
}
}

View File

@@ -1,28 +1,40 @@
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { StatusFilesNode } from './statusFilesNode';
import { StatusUpstreamNode } from './statusUpstreamNode'; import { StatusUpstreamNode } from './statusUpstreamNode';
export class StatusNode extends ExplorerNode { export class StatusNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status'; readonly resourceType: ResourceType = 'gitlens:status';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
const status = await this.git.getStatusForRepo(this.uri.repoPath!); const status = await this.git.getStatusForRepo(this.uri.repoPath!);
if (status === undefined) return []; if (status === undefined) return [];
const children = []; const children: ExplorerNode[] = [];
if (status.state.behind) { if (status.state.behind) {
children.push(new StatusUpstreamNode(status, 'behind', this.git.config.gitExplorer.commitFormat, this.context, this.git)); children.push(new StatusUpstreamNode(status, 'behind', this.context, this.git));
} }
if (status.state.ahead) { if (status.state.ahead) {
children.push(new StatusUpstreamNode(status, 'ahead', this.git.config.gitExplorer.commitFormat, this.context, this.git)); children.push(new StatusUpstreamNode(status, 'ahead', this.context, this.git));
}
if (status.files.length !== 0 || status.state.ahead && this.git.config.insiders) {
const range = status.state.ahead
? `${status.upstream}..${status.branch}`
: undefined;
children.splice(0, 0, new StatusFilesNode(status, range, this.context, this.git));
} }
return children; return children;
@@ -57,6 +69,10 @@ export class StatusNode extends ExplorerNode {
label = `${status.branch} is up-to-date`; label = `${status.branch} is up-to-date`;
} }
if (this.git.config.insiders) {
hasChildren = hasChildren || status.files.length !== 0;
}
const item = new TreeItem(label, hasChildren ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); const item = new TreeItem(label, hasChildren ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None);
item.contextValue = this.resourceType; item.contextValue = this.resourceType;

View File

@@ -1,15 +1,20 @@
'use strict'; 'use strict';
import { Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { CommitNode } from './commitNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { GitService, GitStatus, GitUri } from '../gitService'; import { GitService, GitStatus, GitUri } from '../gitService';
import { CommitNode } from './commitNode';
export class StatusUpstreamNode extends ExplorerNode { export class StatusUpstreamNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status-upstream'; 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) { constructor(
public readonly status: GitStatus,
public readonly direction: 'ahead' | 'behind',
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
} }
@@ -17,10 +22,11 @@ export class StatusUpstreamNode extends ExplorerNode {
const range = this.direction === 'ahead' const range = this.direction === 'ahead'
? `${this.status.upstream}..${this.status.branch}` ? `${this.status.upstream}..${this.status.branch}`
: `${this.status.branch}..${this.status.upstream}`; : `${this.status.branch}..${this.status.upstream}`;
let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0); let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0);
if (log === undefined) return []; if (log === undefined) return [];
if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git))]; if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, 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 // 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 commits = Array.from(log.commits.values());
@@ -32,8 +38,8 @@ export class StatusUpstreamNode extends ExplorerNode {
} }
} }
return [...Iterables.map(commits, c => new CommitNode(c, this.template, this.context, this.git))]; return [...Iterables.map(commits, c => new CommitNode(c, this.context, this.git))];
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
const label = this.direction === 'ahead' const label = this.direction === 'ahead'