45 Commits

Author SHA1 Message Date
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
Eric Amodio
11eacb27a1 Preps v5.0.0 2017-09-12 19:06:15 -04:00
Eric Amodio
543d39246f Closes #138 - adds ignore whitespace setting 2017-09-12 17:46:22 -04:00
Eric Amodio
6837414f22 Adds more details to remotes in custom view 2017-09-12 15:48:36 -04:00
Eric Amodio
ea6fdbaaf2 Adds groupBy function 2017-09-12 15:47:57 -04:00
Eric Amodio
ccc29e3dfc Reworks remote parsing
Combines same url into same remote
Adds a change event for custom remote providers
Adds a repo change event for custom remote providers
2017-09-12 15:46:44 -04:00
Eric Amodio
48814d4213 Changes show all commits icon 2017-09-12 13:02:04 -04:00
Eric Amodio
c3dd83cf3c Updates documention with remotes exanple 2017-09-12 11:56:17 -04:00
Eric Amodio
77482f4930 Adds openChangedFileChanges to custom view
Adds openChangedFileChangesWithWorking to custom view
Removes unneeded context checks from custom view commands
2017-09-12 11:11:03 -04:00
Eric Amodio
503b2a3785 Fixes issue where the revision wasn't properly opened
Adds ability to provide a branch to open file in remote
2017-09-12 10:38:51 -04:00
Eric Amodio
9464f7e79f Preps v5.0.0-beta.2 2017-09-11 23:51:27 -04:00
Eric Amodio
77ae37c54c Adds rudimentary "paging" to custom view branch history 2017-09-11 23:47:51 -04:00
Eric Amodio
e20ec552b7 Removes branches remote commands if no remotes
Removes branch remote commands if not tracked
2017-09-11 23:41:47 -04:00
Eric Amodio
f911447c5e Formats svg like other icons 2017-09-11 23:38:47 -04:00
Eric Amodio
38c44c808d Fixes double getRemotes call 2017-09-11 21:32:52 -04:00
Eric Amodio
655afb358e Fixes double hovers on blank lines 2017-09-11 21:32:52 -04:00
Eric Amodio
21e0963600 Adds bitbucket server support 2017-09-11 21:32:40 -04:00
Eric Amodio
62580da702 Preps v5.0.0-beta 2017-09-11 03:02:56 -04:00
Eric Amodio
6b97c107eb Updates dependencies 2017-09-11 02:27:49 -04:00
Eric Amodio
92b57580b8 Fixes #120 - Adds custom remotes support 2017-09-11 02:09:32 -04:00
Eric Amodio
e400f27c84 Adds refs changes into repo watcher
Catches branch & remote changes
2017-09-11 00:50:40 -04:00
Eric Amodio
4221e06ae3 Removes history limit for custom view 2017-09-11 00:41:32 -04:00
Eric Amodio
a2dc65c044 Adds message truncation at newline 2017-09-11 00:41:11 -04:00
Eric Amodio
4102bdd471 Reworks git command error handling
Switches to use the new diffWith command
2017-09-11 00:39:52 -04:00
Eric Amodio
d420d82ab2 Removes unneeded shortSha parameter 2017-09-11 00:36:38 -04:00
Eric Amodio
260874fa1d Adds better filename sanitization 2017-09-10 17:44:26 -04:00
Eric Amodio
9d83fbcacb Switches to use the new diffWith command 2017-09-09 15:48:44 -04:00
Eric Amodio
a50f04c569 Adds commands to hover links 2017-09-09 01:57:12 -04:00
Eric Amodio
df0599a832 Adds shortenSha method 2017-09-09 00:43:20 -04:00
Eric Amodio
f05d236e79 Updates dependencies
Updates to latest vscode engine
2017-09-08 18:47:25 -04:00
Eric Amodio
1b7610857a Fixes issue where repo change wasn't fired in some cases
Consolidates repo watching into a single watcher
Adds debounce to repo changes in the custom view
2017-09-08 18:02:43 -04:00
Eric Amodio
04d2c00ebf Fixes issue with branch name truncations (rebase) 2017-09-08 17:03:21 -04:00
Eric Amodio
ece34dba32 Cleans up some command overrides 2017-09-05 21:30:07 -04:00
Eric Amodio
68fcbf713d Fixes regression with not opening line in remote 2017-09-05 21:29:22 -04:00
Eric Amodio
e192c547b1 Fixes #130 - Stops repeated welcome for some users
No idea why the version check fails, but hopefully this will help
2017-09-04 03:30:19 -04:00
91 changed files with 1564 additions and 846 deletions

View File

@@ -4,13 +4,29 @@ 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/).
## [5.0.0-alpha.2] - 2017-09-03 ## [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
### Added ### Added
- Adds an all-new `GitLens` custom view to the Explorer activity - Adds an all-new `GitLens` custom view to the Explorer activity
- `Repository View` - provides a full repository explorer - `Repository View` - provides a full repository explorer
![GitLens Repository view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-git-custom-view-repository.png) ![GitLens Repository view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-git-custom-view-repository.png)
- `Repository Status` node — provides the status of the repository - `Repository Status` node — provides the status of the repository
- Provides the name of the current branch, its upstream tracking branch (if available), and its upstream status (if available) - Provides the name of the current branch, its upstream tracking branch (if available), and its upstream status (if available)
@@ -26,29 +42,30 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Indicates which branch is the current branch and optionally shows the remote tracking branch - Indicates which branch is the current branch and optionally shows the remote tracking branch
- Expand each branch to easily see its revision (commit) history - Expand each branch to easily see its revision (commit) history
- Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes - Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes
- Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, and `Show Commit File Details` commands - Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, `Show File History`, and `Show Commit File Details` commands
- Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`, `Open Files`, `Open Revisions`, `Show Commit Details`, and `Refresh` commands - Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Open All Changes`, `Open All Changes with Working Tree`, `Open Files`, `Open Revisions`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`, `Show Commit Details`, and `Refresh` commands
- Provides a context menu on each branch with `Open Branch in Remote`, and `Refresh` commands - Provides a context menu on each branch with `Open Branch in Remote`, and `Refresh` commands
- Provides a context menu with `Open Branches in Remote`, and `Refresh` commands - Provides a context menu with `Open Branches in Remote`, and `Refresh` commands
- `Remotes` node — provides a list of remotes - `Remotes` node — provides a list of remotes
- Indicates the direction of the remote (fetch, push, both), remote service (if applicable), and repository path
- Expand each remote to see its list of branches - Expand each remote to see its list of branches
- Expand each branch to easily see its revision (commit) history - Expand each branch to easily see its revision (commit) history
- Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes - Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes
- Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, and `Show Commit File Details` commands - Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, and `Show Commit File Details` commands
- Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`, `Open Files`, `Open Revisions`, `Show Commit Details`, and `Refresh` commands - Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Open All Changes`, `Open All Changes with Working Tree`, `Open Files`, `Open Revisions`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`,`Show Commit Details`, and `Refresh` commands
- Provides a context menu on each remote with `Open Branches in Remote`, `Open Repository in Remote`, and `Refresh` commands - Provides a context menu on each remote with `Open Branches in Remote`, `Open Repository in Remote`, and `Refresh` commands
- Provides a context menu with a `Refresh` command - Provides a context menu with a `Refresh` command
- `Stashes` node — provides a list of stashed changes - `Stashes` node — provides a list of stashed changes
- Expand each stash to quickly see the set of files stashed, complete with status indicators for adds, changes, renames, and deletes - Expand each stash to quickly see the set of files stashed, complete with status indicators for adds, changes, renames, and deletes
- Provides a context menu with `Stash Changes`, and `Refresh` commands - Provides a context menu with `Stash Changes`, and `Refresh` commands
- Provides a context menu on each stash with `Apply Stashed Changes` (confirmation required), `Delete Stashed Changes` (confirmation required), `Copy Commit Message to Clipboard`, `Open Files`, `Open Revisions`, and `Refresh` commands - Provides a context menu on each stash with `Apply Stashed Changes` (confirmation required), `Delete Stashed Changes` (confirmation required), `Open All Changes`, `Open All Changes with Working Tree`, `Open Files`, `Open Revisions`, `Copy Commit Message to Clipboard`, and `Refresh` commands
- Provides a context menu on each stashed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Apply Changes`, and `Show File History` commands - Provides a context menu on each stashed file with `Apply Changes`, `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, and `Show File History` commands
- `History View` - provides the revision history of the active file - `History View` - provides the revision history of the active file
![GitLens History view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-git-custom-view-history.png) ![GitLens History view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-git-custom-view-history.png)
- Automatically updates to track the active editor - Automatically updates to track the active editor
- Provides a context menu with `Open File`, `Open File in Remote`, and `Refresh` commands - Provides a context menu with `Open File`, `Open File in Remote`, and `Refresh` commands
@@ -57,6 +74,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Quickly switch between views using the `Switch to Repository View` or `Switch to History View` commands - Quickly switch between views using the `Switch to Repository View` or `Switch to History View` commands
- Provides toolbar commands to `Search Commits`, `Switch to Repository View` or `Switch to History View`, and `Refresh` - Provides toolbar commands to `Search Commits`, `Switch to Repository View` or `Switch to History View`, and `Refresh`
- Adds all-new interactivity to the hover annotations
![Hover Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png)
- Adds the following command-links to the `details` hover annotation
- Clicking the commit id will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`)
- Adds the following command-links to the `changes` hover annotation
- 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`)
- Adds support for remote services with custom domains -- closes [#120](https://github.com/eamodio/vscode-gitlens/issues/120)
- Adds support for the Bitbucket Server (previously called Stash) remote service -- closes [#120](https://github.com/eamodio/vscode-gitlens/issues/120)
- Adds `gitlens.blame.ignoreWhitespace` setting to specify whether or not to ignore whitespace when comparing revisions during blame operations -- closes [#138](https://github.com/eamodio/vscode-gitlens/issues/138)
- Adds `Compare File Revisions` command (`gitlens.diffWith`) - compares the specified file revisions
- Adds `Open Branches in Remote` command (`gitlens.openBranchesInRemote`) - opens the branches in the supported remote service - Adds `Open Branches in Remote` command (`gitlens.openBranchesInRemote`) - opens the branches in the supported remote service
- Adds `Stash Changes` command (`gitlens.stashSave`) to the source control group context menu -- can now stash a group of files - Adds `Stash Changes` command (`gitlens.stashSave`) to the source control group context menu -- can now stash a group of files
- Adds `Stash Changes` command (`gitlens.stashSave`) to the source control resource context menu -- can now stash individual files (works with multi-select too!) - Adds `Stash Changes` command (`gitlens.stashSave`) to the source control resource context menu -- can now stash individual files (works with multi-select too!)
@@ -85,7 +116,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Removes the seeding of the commit search command from the clipboard - Removes the seeding of the commit search command from the clipboard
### Fixed ### Fixed
- Fixes an issue where double hover annotations could be shown on blank lines
- Fixes an issue where remote branches couldn't be opened properly in their remote service - Fixes an issue where remote branches couldn't be opened properly in their remote service
- Fixes [#130](https://github.com/eamodio/vscode-gitlens/issues/130) - First-run "Thank you for choosing GitLens! [...]" info message shown on every start up
- Fixes an issue where sometimes diffs (via branch name) wouldn't open properly
- Fixes an issue where remotes are queried more than once on startup
## [4.4.3] - 2017-08-30 ## [4.4.3] - 2017-08-30
## Fixed ## Fixed
@@ -146,9 +181,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [4.3.0] - 2017-07-03 ## [4.3.0] - 2017-07-03
## Added ## Added
- Adds `Git Stashes` custom view to the Explorer activity - Adds `Git Stashes` custom view to the Explorer activity
![Git Stashes view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-git-stashes.png)
- Shows all of the stashed changes in the repository - Shows all of the stashed changes in the repository
- Provides toolbar buttons to `Stash Changes` and `Refresh` - Provides toolbar buttons to `Stash Changes` and `Refresh`
- Provides a context menu with `Apply Stashed Changes` and `Delete Stashed Changes` commands - both require a confirmation - Provides a context menu with `Apply Stashed Changes` and `Delete Stashed Changes` commands - both require a confirmation
@@ -157,7 +189,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [4.2.0] - 2017-06-27 ## [4.2.0] - 2017-06-27
## Added ## Added
- Adds `Compare File with Revision...` command (`gitlens.diffWithRevision`) - compare the active file with the selected revision of the same file - Adds `Compare File with Revision...` command (`gitlens.diffWithRevision`) - compares the active file with the selected revision of the same file
- Adds `Open Changed Files` command (`gitlens.openChangedFiles`) to the source control group context menu - Adds `Open Changed Files` command (`gitlens.openChangedFiles`) to the source control group context menu
- Adds `Close Unchanged Files` command (`gitlens.closeUnchangedFiles`) to the source control group context menu - Adds `Close Unchanged Files` command (`gitlens.closeUnchangedFiles`) to the source control group context menu
- Adds `Open File in Remote` command (`gitlens.openFileInRemote`) to the source control resource context menu - Adds `Open File in Remote` command (`gitlens.openFileInRemote`) to the source control resource context menu
@@ -211,7 +243,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Fixed ### Fixed
- Fixes excessive memory usage when parsing diffs - Fixes excessive memory usage when parsing diffs
- Fixes extra newline in multiline commit messages - Fixes extra newline in multi-line commit messages
- Fixes (again) [#33](https://github.com/eamodio/vscode-gitlens/issues/33) - Commit messages can causes markdown formatting in hovers - Fixes (again) [#33](https://github.com/eamodio/vscode-gitlens/issues/33) - Commit messages can causes markdown formatting in hovers
## [4.0.1] - 2017-06-09 ## [4.0.1] - 2017-06-09
@@ -295,7 +327,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added ### Added
- Improves performance - Improves performance
- Reduces the number of git calls on known "untrackables" - Reduces the number of git calls on known "untrackables"
- Caches many more git commands to reduce git command roundtrips and parsing - Caches many more git commands to reduce git command round-trips and parsing
- Increases the debounce (delay) on cursor movement to reduce lag when navigating around a file - Increases the debounce (delay) on cursor movement to reduce lag when navigating around a file
- Adds diff information (the line's previous version) into the active line hover when the current line is uncommitted - Adds diff information (the line's previous version) into the active line hover when the current line is uncommitted
- Adds `gitlens.statusBar.alignment` settings to control the alignment of the status bar -- thanks to [PR #72](https://github.com/eamodio/vscode-gitlens/pull/72) by Zack Schuster ([@zackschuster](https://github.com/zackschuster))! - Adds `gitlens.statusBar.alignment` settings to control the alignment of the status bar -- thanks to [PR #72](https://github.com/eamodio/vscode-gitlens/pull/72) by Zack Schuster ([@zackschuster](https://github.com/zackschuster))!
@@ -486,7 +518,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Fixed ### Fixed
- Fixes issue with `gitlens.diffWithPrevious` command execution via code lens when the code lens was not at the document/file level - Fixes issue with `gitlens.diffWithPrevious` command execution via code lens when the code lens was not at the document/file level
- Fixes issue where full shas were displayed on the file/blame history explorers - Fixes issue where full shas were displayed on the file/blame history explorers
- Fixes [#30](https://github.com/eamodio/vscode-gitlens/issues/30) - Diff with Working Tree fails from repo/commit quickpick list if file was renamed (and the commit was before the rename) - Fixes [#30](https://github.com/eamodio/vscode-gitlens/issues/30) - Diff with Working Tree fails from repo/commit quick pick list if file was renamed (and the commit was before the rename)
- Fixes various other quick pick menu command issues when a file was renamed - Fixes various other quick pick menu command issues when a file was renamed
- Fixes various issues when caching is disabled - Fixes various issues when caching is disabled
- Fixes issues with parsing commits history - Fixes issues with parsing commits history
@@ -589,7 +621,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [2.9.0] ## [2.9.0]
### Changed ### Changed
- To accomodate the realization that blame information is invalid when a file has unsaved changes, the following behavior changes have been made - To accommodate the realization that blame information is invalid when a file has unsaved changes, the following behavior changes have been made
- Status bar blame information will hide - Status bar blame information will hide
- Code lens change to a `Cannot determine...` message and become unclickable - Code lens change to a `Cannot determine...` message and become unclickable
- Many menu choices and commands will hide - Many menu choices and commands will hide
@@ -627,7 +659,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Fixed ### Fixed
- Fixes [#34](https://github.com/eamodio/vscode-gitlens/issues/34) - Open file should open the selected version of the file - Fixes [#34](https://github.com/eamodio/vscode-gitlens/issues/34) - Open file should open the selected version of the file
- Fixes some issue where some editors opened by the quickpick would not be opened in preview tabs - Fixes some issue where some editors opened by the quick pick would not be opened in preview tabs
- Fixes issue where copy to clipboard commands would fail if there was no active editor - Fixes issue where copy to clipboard commands would fail if there was no active editor
- Fixes issue where active line annotations would show for opened versioned files - Fixes issue where active line annotations would show for opened versioned files
- Fixes issue where code lens compare commands on opened versioned files would fail - Fixes issue where code lens compare commands on opened versioned files would fail

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/develop/images/chat-badge.png)](https://join.slack.com/t/vscode-gitlens/shared_invite/MjIxOTgxNDE3NzM0LTE1MDE2Nzk1MTgtMjkwMmZjMzcxNQ) [![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)
# GitLens # GitLens
@@ -10,7 +10,7 @@ GitLens **supercharges** the built-in Visual Studio Code Git capabilities. It he
GitLens provides an unobtrusive blame annotation at the end of the current line, a status bar item showing the commit information (author and date, by default) of the current line, code lens showing the most recent commit and # of authors of the file and/or code block, and many commands for exploring commits and histories, comparing and navigating revisions, stash access, repository status, and more. GitLens is also [highly customizable](#extension-settings) to meet your specific needs — find code lens intrusive or the current line blame annotation distracting — no problem, it is easy to [turn them off or change how they behave](#extension-settings). GitLens provides an unobtrusive blame annotation at the end of the current line, a status bar item showing the commit information (author and date, by default) of the current line, code lens showing the most recent commit and # of authors of the file and/or code block, and many commands for exploring commits and histories, comparing and navigating revisions, stash access, repository status, and more. GitLens is also [highly customizable](#extension-settings) to meet your specific needs — find code lens intrusive or the current line blame annotation distracting — no problem, it is easy to [turn them off or change how they behave](#extension-settings).
### Preview — featuring blame annotations, code lens, status bar details, quick pick menus for navigation and exploration, compare with previous, and more ### Preview — featuring blame annotations, code lens, status bar details, quick pick menus for navigation and exploration, compare with previous, and more
![GitLens preview](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/gitlens-preview.gif) ![GitLens preview](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/gitlens-preview.gif)
## Features ## Features
@@ -18,26 +18,31 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds an unobtrusive, highly [customizable](#line-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotation** to the end of the current line ([optional](#line-blame-annotation-settings), on by default) - Adds an unobtrusive, highly [customizable](#line-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotation** to the end of the current line ([optional](#line-blame-annotation-settings), on by default)
![Line Blame Annotation](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-line-blame-annotation.png) ![Line Blame Annotation](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotation.png)
- Contains the author, date, and message of the line's most recent commit, by [default](#line-blame-annotation-settings) - Contains the author, date, and message of the line's most recent commit, by [default](#line-blame-annotation-settings)
- Adds a `details` hover annotation to the current line annotation, which provides more commit details ([optional](#line-blame-annotation-settings), on by default) - Adds a `details` hover annotation to the current line annotation, which provides more commit details ([optional](#line-blame-annotation-settings), on by default)
- Clicking the commit id will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`)
- 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 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/develop/images/screenshot-line-blame-annotations.png) ![Line Blame Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png)
- Adds on-demand, beautiful, highly [customizable](#file-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotations** of the whole file - Adds on-demand, beautiful, highly [customizable](#file-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotations** of the whole file
![File Blame Annotation](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-file-blame-annotations.png) ![File Blame Annotation](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-file-blame-annotations.png)
- Choose between `gutter` (default) and `hover` [annotation styles](#file-blame-annotation-settings) - Choose between `gutter` (default) and `hover` [annotation styles](#file-blame-annotation-settings)
- Contains the commit message and date, by [default](#file-blame-annotation-settings) - Contains the commit message and date, by [default](#file-blame-annotation-settings)
- Adds a `details` hover annotation to the line's annotation, which provides more commit details ([optional](#file-blame-annotation-settings), on by default) - Adds a `details` hover annotation to the line's annotation, which provides more commit details ([optional](#file-blame-annotation-settings), on by default)
- Clicking the commit id will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`)
- Adds a `heatmap` (age) indicator to the gutter annotations (on right edge by [default](#file-blame-annotation-settings)), which provides an easy, at-a-glance way to tell the age of a line ([optional](#file-blame-annotation-settings), on by default) - Adds a `heatmap` (age) indicator to the gutter annotations (on right edge by [default](#file-blame-annotation-settings)), which provides an easy, at-a-glance way to tell the age of a line ([optional](#file-blame-annotation-settings), on by default)
- Indicator ranges from bright yellow (newer) to dark brown (older) - Indicator ranges from bright yellow (newer) to dark brown (older)
- Press `Escape` to quickly toggle the annotations off - Press `Escape` to quickly toggle the annotations off
- Adds [customizable](#status-bar-settings) **blame information** about the current line to the **status bar** ([optional](#status-bar-settings), on by default) - Adds [customizable](#status-bar-settings) **blame information** about the current line to the **status bar** ([optional](#status-bar-settings), on by default)
![Status Bar Blame](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-status-bar.png) ![Status Bar Blame](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-status-bar.png)
- Contains the commit author and date, by [default](#status-bar-settings) - Contains the commit author and date, by [default](#status-bar-settings)
- Clicking the status bar item will, by [default](#status-bar-settings), show a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more - Clicking the status bar item will, by [default](#status-bar-settings), show a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Provides [customizable](#status-bar-settings) click behavior — choose between one of the following - Provides [customizable](#status-bar-settings) click behavior — choose between one of the following
@@ -61,7 +66,10 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds on-demand, [customizable](#file-recent-changes-annotation-settings) and [themeable](#theme-settings), **recent changes annotations** of the whole file - Adds on-demand, [customizable](#file-recent-changes-annotation-settings) and [themeable](#theme-settings), **recent changes annotations** of the whole file
- Highlights all of lines changed in the most recent commit - Highlights all of lines changed in the most recent commit
- Adds a `details` hover annotation to each line, which provides more commit details ([optional](#file-blame-annotation-settings), on by default) - Adds a `details` hover annotation to each line, which provides more commit details ([optional](#file-blame-annotation-settings), on by default)
- Clicking the commit id will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`)
- Adds a `changes` (diff) hover annotation to each line, which provides **instant** access to the line's previous version ([optional](#file-recent-changes-annotation-settings), on by default) - Adds a `changes` (diff) hover annotation to each line, which provides **instant** access to the line's previous version ([optional](#file-recent-changes-annotation-settings), on by default)
- 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`)
- Press `Escape` to quickly toggle the annotations off - Press `Escape` to quickly toggle the annotations off
- Adds `Toggle Recent File Changes Annotations` command (`gitlens.toggleFileRecentChanges`) to toggle the recent changes annotations on and off - Adds `Toggle Recent File Changes Annotations` command (`gitlens.toggleFileRecentChanges`) to toggle the recent changes annotations on and off
@@ -70,7 +78,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default) - Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default)
![Git Code Lens](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-code-lens.png) ![Git Code Lens](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-code-lens.png)
- **Recent Change** — author and date of the most recent commit for the file or code block - **Recent Change** — author and date of the most recent commit for the file or code block
- Clicking the code lens will, by [default](#code-lens-settings), show a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more - Clicking the code lens will, by [default](#code-lens-settings), show a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- **Authors** — number of authors of the file or code block and the most prominent author (if there is more than one) - **Authors** — number of authors of the file or code block and the most prominent author (if there is more than one)
@@ -115,7 +123,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- `Repository View` - provides a full repository explorer - `Repository View` - provides a full repository explorer
![GitLens Repository view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-git-custom-view-repository.png) ![GitLens Repository view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-git-custom-view-repository.png)
- `Repository Status` node — provides the status of the repository - `Repository Status` node — provides the status of the repository
- Provides the name of the current branch, its upstream tracking branch (if available), and its upstream status (if available) - Provides the name of the current branch, its upstream tracking branch (if available), and its upstream status (if available)
@@ -132,28 +140,30 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Expand each branch to easily see its revision (commit) history - Expand each branch to easily see its revision (commit) history
- Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes - Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes
- Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, and `Show Commit File Details` commands - Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, and `Show Commit File Details` commands
- Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`, `Open Files`, `Open Revisions`, `Show Commit Details`, and `Refresh` commands
- Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Open All Changes`, `Open All Changes with Working Tree`, `Open Files`, `Open Revisions`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`, `Show Commit Details`, and `Refresh` commands
- Provides a context menu on each branch with `Open Branch in Remote`, and `Refresh` commands - Provides a context menu on each branch with `Open Branch in Remote`, and `Refresh` commands
- Provides a context menu with `Open Branches in Remote`, and `Refresh` commands - Provides a context menu with `Open Branches in Remote`, and `Refresh` commands
- `Remotes` node — provides a list of remotes - `Remotes` node — provides a list of remotes
- Indicates the direction of the remote (fetch, push, both), remote service (if applicable), and repository path
- Expand each remote to see its list of branches - Expand each remote to see its list of branches
- Expand each branch to easily see its revision (commit) history - Expand each branch to easily see its revision (commit) history
- Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes - Expand each revision (commit) to quickly see the set of files changed, complete with status indicators for adds, changes, renames, and deletes
- Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, and `Show Commit File Details` commands - Provides a context menu on each changed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Open Revision in Remote`, `Apply Changes`, `Show File History`, and `Show Commit File Details` commands
- Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`, `Open Files`, `Open Revisions`, `Show Commit Details`, and `Refresh` commands - Provides a context menu on each revision (commit) with `Open Commit in Remote`, `Open All Changes`, `Open All Changes with Working Tree`, `Open Files`, `Open Revisions`, `Copy Commit ID to Clipboard`, `Copy Commit Message to Clipboard`,`Show Commit Details`, and `Refresh` commands
- Provides a context menu on each remote with `Open Branches in Remote`, `Open Repository in Remote`, and `Refresh` commands - Provides a context menu on each remote with `Open Branches in Remote`, `Open Repository in Remote`, and `Refresh` commands
- Provides a context menu with a `Refresh` command - Provides a context menu with a `Refresh` command
- `Stashes` node — provides a list of stashed changes - `Stashes` node — provides a list of stashed changes
- Expand each stash to quickly see the set of files stashed, complete with status indicators for adds, changes, renames, and deletes - Expand each stash to quickly see the set of files stashed, complete with status indicators for adds, changes, renames, and deletes
- Provides a context menu with `Stash Changes`, and `Refresh` commands - Provides a context menu with `Stash Changes`, and `Refresh` commands
- Provides a context menu on each stash with `Apply Stashed Changes` (confirmation required), `Delete Stashed Changes` (confirmation required), `Copy Commit Message to Clipboard`, `Open Files`, `Open Revisions`, and `Refresh` commands - Provides a context menu on each stash with `Apply Stashed Changes` (confirmation required), `Delete Stashed Changes` (confirmation required), `Open All Changes`, `Open All Changes with Working Tree`, `Open Files`, `Open Revisions`, `Copy Commit Message to Clipboard`, and `Refresh` commands
- Provides a context menu on each stashed file with `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, `Apply Changes`, and `Show File History` commands - Provides a context menu on each stashed file with `Apply Changes`, `Open Changes`, `Open Changes with Working Tree`, `Open File`, `Open Revision`, `Open File in Remote`, and `Show File History` commands
- `History View` - provides the revision history of the active file - `History View` - provides the revision history of the active file
![GitLens History view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-git-custom-view-history.png) ![GitLens History view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-git-custom-view-history.png)
- Automatically updates to track the active editor - Automatically updates to track the active editor
- Provides a context menu with `Open File`, `Open File in Remote`, and `Refresh` commands - Provides a context menu with `Open File`, `Open File in Remote`, and `Refresh` commands
@@ -165,6 +175,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Search Commits` command (`gitlens.showCommitSearch`) with a shortcut of `alt+/` to search for commits by message, author, file(s), or commit id - Adds a `Search Commits` command (`gitlens.showCommitSearch`) with a shortcut of `alt+/` to search for commits by message, author, file(s), or commit id
- Adds commands to open files, commits, branches, and the repository in the supported remote services, currently **BitBucket, GitHub, GitLab, and Visual Studio Team Services** — only available if a Git upstream service is configured in the repository - Adds commands to open files, commits, branches, and the repository in the supported remote services, currently **BitBucket, GitHub, GitLab, and Visual Studio Team Services** — only available if a Git upstream service is configured in the repository
- Also supports [remote services with custom domains](#custom-remotes-settings), such as **BitBucket, Bitbucket Server (previously called Stash), GitHub, GitHub Enterprise, GitLab**
- `Open Branches in Remote` command (`gitlens.openBranchesInRemote`) — opens the branches in the supported remote service - `Open Branches in Remote` command (`gitlens.openBranchesInRemote`) — opens the branches in the supported remote service
- `Open Branch in Remote` command (`gitlens.openBranchInRemote`) — opens the current branch commits in the supported remote service - `Open Branch in Remote` command (`gitlens.openBranchInRemote`) — opens the current branch commits in the supported remote service
- `Open Commit in Remote` command (`gitlens.openCommitInRemote`) — opens the commit revision of the active line in the supported remote service - `Open Commit in Remote` command (`gitlens.openCommitInRemote`) — opens the commit revision of the active line in the supported remote service
@@ -173,7 +184,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Show Current Branch History` command (`gitlens.showQuickRepoHistory`) with a shortcut of `shift+alt+h` to show a paged **branch history quick pick menu** of the current branch for exploring its commit history - Adds a `Show Current Branch History` command (`gitlens.showQuickRepoHistory`) with a shortcut of `shift+alt+h` to show a paged **branch history quick pick menu** of the current branch for exploring its commit history
![Branch History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-branch-history.png) ![Branch History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-branch-history.png)
- Provides entries to `Show Commit Search` and `Open Branch in <remote-service>` when available - Provides entries to `Show Commit Search` and `Open Branch in <remote-service>` when available
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
@@ -184,7 +195,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Show File History` command (`gitlens.showQuickFileHistory`) to show a paged **file history quick pick menu** of the active file for exploring its commit history - Adds a `Show File History` command (`gitlens.showQuickFileHistory`) to show a paged **file history quick pick menu** of the active file for exploring its commit history
![File History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-file-history.png) ![File History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-file-history.png)
- Provides entries to `Show Branch History` and `Open File in <remote-service>` when available - Provides entries to `Show Branch History` and `Open File in <remote-service>` when available
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
@@ -192,7 +203,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Show Commit Details` command (`gitlens.showQuickCommitDetails`) to show a **commit details quick pick menu** of the most recent commit of the active file - Adds a `Show Commit Details` command (`gitlens.showQuickCommitDetails`) to show a **commit details quick pick menu** of the most recent commit of the active file
![Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-commit-details.png) ![Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-commit-details.png)
- Quickly see the set of files changed in the commit, complete with status indicators for adds, changes, renames, and deletes - Quickly see the set of files changed in the commit, complete with status indicators for adds, changes, renames, and deletes
- Provides entries to `Copy to Clipboard`, `Directory Compare`, `Open Changed Files`, `Open File in <remote-service>` when available, and more - Provides entries to `Copy to Clipboard`, `Directory Compare`, `Open Changed Files`, `Open File in <remote-service>` when available, and more
@@ -202,7 +213,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Show Commit File Details` command (`gitlens.showQuickCommitFileDetails`) with a shortcut of `alt+c` to show a **file commit details quick pick menu** of the most recent commit of the active file - Adds a `Show Commit File Details` command (`gitlens.showQuickCommitFileDetails`) with a shortcut of `alt+c` to show a **file commit details quick pick menu** of the most recent commit of the active file
![Commit File Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-commit-file-details.png) ![Commit File Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-commit-file-details.png)
- Provides entries to `Show Commit Details`, `Show File History`, `Compare File with...`, `Copy to Clipboard`, `Open File`, `Open File in <remote-service>` when available, and more - Provides entries to `Show Commit Details`, `Show File History`, `Compare File with...`, `Copy to Clipboard`, `Open File`, `Open File in <remote-service>` when available, and more
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
@@ -210,7 +221,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Show Repository Status` command (`gitlens.showQuickRepoStatus`) with a shortcut of `alt+s` to show a **repository status quick pick menu** for visualizing the current repository status - Adds a `Show Repository Status` command (`gitlens.showQuickRepoStatus`) with a shortcut of `alt+s` to show a **repository status quick pick menu** for visualizing the current repository status
![Repository Status Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-repo-status.png) ![Repository Status Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-repo-status.png)
- Quickly see upstream status (if an Git upstream is configured) — complete with ahead and behind information - Quickly see upstream status (if an Git upstream is configured) — complete with ahead and behind information
- If you are ahead of the upstream, an entry will be shown with the number of commits ahead. Choosing it will show a limited **branch history quick pick menu** containing just the commits ahead of the upstream - If you are ahead of the upstream, an entry will be shown with the number of commits ahead. Choosing it will show a limited **branch history quick pick menu** containing just the commits ahead of the upstream
@@ -222,14 +233,14 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `Show Stashed Changes` command (`gitlens.showQuickStashList`) to show a **stashed changes quick pick menu** for exploring your repository stash history - Adds a `Show Stashed Changes` command (`gitlens.showQuickStashList`) to show a **stashed changes quick pick menu** for exploring your repository stash history
![Stashed Changes Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-stash-list.png) ![Stashed Changes Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-stash-list.png)
- Provides entries to `Stash Changes` - Provides entries to `Stash Changes`
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
- Choosing a stash entry shows a **stash details quick pick menu** which is very similar to the **commit details quick pick menu** above - Choosing a stash entry shows a **stash details quick pick menu** which is very similar to the **commit details quick pick menu** above
![Stash Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/develop/images/screenshot-stash-details.png) ![Stash Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-stash-details.png)
- Quickly see the set of files changed in the stash, complete with status indicators for adds, changes, renames, and deletes - Quickly see the set of files changed in the stash, complete with status indicators for adds, changes, renames, and deletes
- Provides entries to `Copy Message to Clipboard`, `Directory Compare`, and `Open Changed Files` - Provides entries to `Copy Message to Clipboard`, `Directory Compare`, and `Open Changed Files`
@@ -277,7 +288,11 @@ GitLens is highly customizable and provides many configuration settings to allow
|`gitlens.insiders`|Opts into the insiders channel -- provides access to upcoming features |`gitlens.insiders`|Opts into the insiders channel -- provides access to upcoming features
|`gitlens.outputLevel`|Specifies how much (if any) output will be sent to the GitLens output channel |`gitlens.outputLevel`|Specifies how much (if any) output will be sent to the GitLens output channel
### Blame Annotation Settings ### Blame Settings
|Name | Description
|-----|------------
|`gitlens.blame.ignoreWhitespace`|Specifies whether or not to ignore whitespace when comparing revisions during blame operations
#### File Blame Annotation Settings #### File Blame Annotation Settings
@@ -317,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
@@ -343,6 +357,12 @@ GitLens is highly customizable and provides many configuration settings to allow
|`gitlens.gitExplorer.stashFormat`|Specifies the format of stashed 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.stashFormat`|Specifies the format of stashed 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.stashFileFormat`|Specifies the format of a stashed file in the `GitLens` custom view<br />Available tokens<br /> ${file} - file name<br /> ${filePath} - file name and path<br /> ${path} - file path |`gitlens.gitExplorer.stashFileFormat`|Specifies the format of a stashed file in the `GitLens` custom view<br />Available tokens<br /> ${file} - file name<br /> ${filePath} - file name and path<br /> ${path} - file path
### Custom Remotes Settings
|Name | Description
|-----|------------
|`gitlens.remotes`|Specifies any custom domains for remote (code-hosting) services<br />Example: ```"gitlens.remotes": [{ "domain": "git.corporate-url.com", "type": "GitHub" }]```
### Status Bar Settings ### Status Bar Settings
|Name | Description |Name | Description

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="22px"> <svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="m14.687501,11.955358l-1.079554,-6.821251c-0.076429,-0.458572 -0.477679,-0.821607 -0.955357,-0.821607l-9.30518,0c-0.477679,0 -0.878929,0.363036 -0.955357,0.821607l-1.079554,6.821251l0,4.776787c0,0.525447 0.429911,0.955357 0.955357,0.955357l11.464288,0c0.525447,0 0.955357,-0.429911 0.955357,-0.955357l0,-4.776787l0,0zm-3.133572,0.525447l-0.420357,0.850268c-0.162411,0.324821 -0.496786,0.535 -0.869375,0.535l-4.547501,0c-0.363036,0 -0.687857,-0.210179 -0.850268,-0.525447l-0.420357,-0.869375c-0.162411,-0.315268 -0.496786,-0.525447 -0.850268,-0.525447l-1.327947,0l0.955357,-6.687501l9.553573,0l0.955357,6.687501l-1.318393,0c-0.372589,0 -0.697411,0.210179 -0.869375,0.525447l0.009554,0.009554z" /> <path fill="#C5C5C5" d="m14.687501,11.955358l-1.079554,-6.821251c-0.076429,-0.458572 -0.477679,-0.821607 -0.955357,-0.821607l-9.30518,0c-0.477679,0 -0.878929,0.363036 -0.955357,0.821607l-1.079554,6.821251l0,4.776787c0,0.525447 0.429911,0.955357 0.955357,0.955357l11.464288,0c0.525447,0 0.955357,-0.429911 0.955357,-0.955357l0,-4.776787l0,0zm-3.133572,0.525447l-0.420357,0.850268c-0.162411,0.324821 -0.496786,0.535 -0.869375,0.535l-4.547501,0c-0.363036,0 -0.687857,-0.210179 -0.850268,-0.525447l-0.420357,-0.869375c-0.162411,-0.315268 -0.496786,-0.525447 -0.850268,-0.525447l-1.327947,0l0.955357,-6.687501l9.553573,0l0.955357,6.687501l-1.318393,0c-0.372589,0 -0.697411,0.210179 -0.869375,0.525447l0.009554,0.009554z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 846 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="#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

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="m12.24,10.4c0.19,1.28 -0.2,2.62 -1.2,3.6c-1.47,1.45 -3.74,1.63 -5.41,0.54l1.17,-1.14l-4.3,-0.6l0.6,4.2l1.31,-1.26c2.36,1.74 5.7,1.57 7.84,-0.54c1.24,-1.23 1.81,-2.85 1.74,-4.46l-1.75,-0.34l0,0zm-7.28,-2.4c1.47,-1.45 3.74,-1.63 5.41,-0.54l-1.17,1.14l4.3,0.6l-0.6,-4.2l-1.31,1.26c-2.36,-1.74 -5.7,-1.57 -7.85,0.54c-1.24,1.23 -1.8,2.85 -1.73,4.46l1.75,0.35c-0.19,-1.28 0.2,-2.63 1.2,-3.61l0,0z" />
</svg>

After

Width:  |  Height:  |  Size: 546 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="m12.5,11l2.5,2.5c0,0.55 -0.45,1 -1,1l-4,0l0,-1l3.5,0l-2,-2l-7,0l-2,2l3.5,0l0,1l-4,0c-0.55,0 -1,-0.45 -1,-1l2.5,-2.5l-2.5,-2.5c0,-0.55 0.45,-1 1,-1l4,0l0,1l-3.5,0l2,2l7,0l2,-2l-3.5,0l0,-1l4,0c0.55,0 1,0.45 1,1l-2.5,2.5l0,0zm-5.5,-1.5l2,0l0,-3l2,0l-3,-3l-3,3l2,0l0,3l0,0zm2,3l-2,0l0,3l-2,0l3,3l3,-3l-2,0l0,-3l0,0z" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="22px"> <svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="m14.687501,11.955358l-1.079554,-6.821251c-0.076429,-0.458572 -0.477679,-0.821607 -0.955357,-0.821607l-9.30518,0c-0.477679,0 -0.878929,0.363036 -0.955357,0.821607l-1.079554,6.821251l0,4.776787c0,0.525447 0.429911,0.955357 0.955357,0.955357l11.464288,0c0.525447,0 0.955357,-0.429911 0.955357,-0.955357l0,-4.776787l0,0zm-3.133572,0.525447l-0.420357,0.850268c-0.162411,0.324821 -0.496786,0.535 -0.869375,0.535l-4.547501,0c-0.363036,0 -0.687857,-0.210179 -0.850268,-0.525447l-0.420357,-0.869375c-0.162411,-0.315268 -0.496786,-0.525447 -0.850268,-0.525447l-1.327947,0l0.955357,-6.687501l9.553573,0l0.955357,6.687501l-1.318393,0c-0.372589,0 -0.697411,0.210179 -0.869375,0.525447l0.009554,0.009554z" /> <path fill="#424242" d="m14.687501,11.955358l-1.079554,-6.821251c-0.076429,-0.458572 -0.477679,-0.821607 -0.955357,-0.821607l-9.30518,0c-0.477679,0 -0.878929,0.363036 -0.955357,0.821607l-1.079554,6.821251l0,4.776787c0,0.525447 0.429911,0.955357 0.955357,0.955357l11.464288,0c0.525447,0 0.955357,-0.429911 0.955357,-0.955357l0,-4.776787l0,0zm-3.133572,0.525447l-0.420357,0.850268c-0.162411,0.324821 -0.496786,0.535 -0.869375,0.535l-4.547501,0c-0.363036,0 -0.687857,-0.210179 -0.850268,-0.525447l-0.420357,-0.869375c-0.162411,-0.315268 -0.496786,-0.525447 -0.850268,-0.525447l-1.327947,0l0.955357,-6.687501l9.553573,0l0.955357,6.687501l-1.318393,0c-0.372589,0 -0.697411,0.210179 -0.869375,0.525447l0.009554,0.009554z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 846 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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="m12.24,10.4c0.19,1.28 -0.2,2.62 -1.2,3.6c-1.47,1.45 -3.74,1.63 -5.41,0.54l1.17,-1.14l-4.3,-0.6l0.6,4.2l1.31,-1.26c2.36,1.74 5.7,1.57 7.84,-0.54c1.24,-1.23 1.81,-2.85 1.74,-4.46l-1.75,-0.34l0,0zm-7.28,-2.4c1.47,-1.45 3.74,-1.63 5.41,-0.54l-1.17,1.14l4.3,0.6l-0.6,-4.2l-1.31,1.26c-2.36,-1.74 -5.7,-1.57 -7.85,0.54c-1.24,1.23 -1.8,2.85 -1.73,4.46l1.75,0.35c-0.19,-1.28 0.2,-2.63 1.2,-3.61l0,0z" />
</svg>

After

Width:  |  Height:  |  Size: 546 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="m12.5,11l2.5,2.5c0,0.55 -0.45,1 -1,1l-4,0l0,-1l3.5,0l-2,-2l-7,0l-2,2l3.5,0l0,1l-4,0c-0.55,0 -1,-0.45 -1,-1l2.5,-2.5l-2.5,-2.5c0,-0.55 0.45,-1 1,-1l4,0l0,1l-3.5,0l2,2l7,0l2,-2l-3.5,0l0,-1l4,0c0.55,0 1,0.45 1,1l-2.5,2.5l0,0zm-5.5,-1.5l2,0l0,-3l2,0l-3,-3l-3,3l2,0l0,3l0,0zm2,3l-2,0l0,3l-2,0l3,3l3,-3l-2,0l0,-3l0,0z" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

65
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.0.0-alpha.2", "version": "5.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -16,19 +16,19 @@
"integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=", "integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "8.0.26" "@types/node": "8.0.28"
} }
}, },
"@types/mocha": { "@types/mocha": {
"version": "2.2.42", "version": "2.2.43",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.42.tgz", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.43.tgz",
"integrity": "sha512-b6gVDoxEbAQGwbV7gSzeFw/hy3/eEAokztktdzl4bHvGgb9K5zW4mVQDlVYch2w31m8t/J7L2iqhQvz3r5edCQ==", "integrity": "sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw==",
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "8.0.26", "version": "8.0.28",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.26.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.28.tgz",
"integrity": "sha512-wbKN0MB4XsjdnSE04HiCzLoBDirGCM6zXrqavSj44nZnPFYpnrTF64E9O6Xmf0ca/IuKK/BHUcXwMiwk92gW6Q==", "integrity": "sha512-HupkFXEv3O3KSzcr3Ylfajg0kaerBg1DyaZzRBBQfrU3NN1mTBRE7sCveqHwXLS5Yrjvww8qFzkzYQQakG9FuQ==",
"dev": true "dev": true
}, },
"@types/tmp": { "@types/tmp": {
@@ -348,7 +348,7 @@
"resolved": "https://registry.npmjs.org/copy-paste/-/copy-paste-1.3.0.tgz", "resolved": "https://registry.npmjs.org/copy-paste/-/copy-paste-1.3.0.tgz",
"integrity": "sha1-p+bEocKP3t8rCB5yuX3y75X0ce0=", "integrity": "sha1-p+bEocKP3t8rCB5yuX3y75X0ce0=",
"requires": { "requires": {
"iconv-lite": "0.4.18", "iconv-lite": "0.4.19",
"sync-exec": "0.6.2" "sync-exec": "0.6.2"
} }
}, },
@@ -1248,6 +1248,12 @@
"sntp": "1.0.9" "sntp": "1.0.9"
} }
}, },
"he": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
"dev": true
},
"hoek": { "hoek": {
"version": "2.16.3", "version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
@@ -1266,9 +1272,9 @@
} }
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.4.18", "version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
}, },
"ignore": { "ignore": {
"version": "3.3.5", "version": "3.3.5",
@@ -1788,9 +1794,9 @@
} }
}, },
"mocha": { "mocha": {
"version": "3.5.0", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.2.tgz",
"integrity": "sha512-pIU2PJjrPYvYRqVpjXzj76qltO9uBYI7woYAMoxbSefsa+vqAfptjoeevd6bUgwD0mPIO+hv9f7ltvsNreL2PA==", "integrity": "sha512-iH5zl7afRZl1GvD0pnrRlazgc9Z/o34pXWmTFi8xNIMFKXgNL1SoBTDDb9sJfbV/sJV/j8X+0gvwY1QS1He7Nw==",
"dev": true, "dev": true,
"requires": { "requires": {
"browser-stdout": "1.3.0", "browser-stdout": "1.3.0",
@@ -1800,6 +1806,7 @@
"escape-string-regexp": "1.0.5", "escape-string-regexp": "1.0.5",
"glob": "7.1.1", "glob": "7.1.1",
"growl": "1.9.2", "growl": "1.9.2",
"he": "1.1.1",
"json3": "3.3.2", "json3": "3.3.2",
"lodash.create": "3.1.1", "lodash.create": "3.1.1",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
@@ -2234,9 +2241,9 @@
"dev": true "dev": true
}, },
"source-map-support": { "source-map-support": {
"version": "0.4.16", "version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.16.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
"integrity": "sha512-A6vlydY7H/ljr4L2UOhDSajQdZQ6dMD7cLH0pzwcmwLyc9u8PNI4WGtnfDDzX7uzGL6c/T+ORL97Zlh+S4iOrg==", "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
"dev": true, "dev": true,
"requires": { "requires": {
"source-map": "0.5.7" "source-map": "0.5.7"
@@ -2327,15 +2334,6 @@
"integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=",
"dev": true "dev": true
}, },
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"string-width": { "string-width": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -2345,6 +2343,15 @@
"strip-ansi": "4.0.0" "strip-ansi": "4.0.0"
} }
}, },
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"stringstream": { "stringstream": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
@@ -2707,10 +2714,10 @@
"gulp-symdest": "1.1.0", "gulp-symdest": "1.1.0",
"gulp-untar": "0.0.6", "gulp-untar": "0.0.6",
"gulp-vinyl-zip": "1.4.0", "gulp-vinyl-zip": "1.4.0",
"mocha": "3.5.0", "mocha": "3.5.2",
"request": "2.81.0", "request": "2.81.0",
"semver": "5.4.1", "semver": "5.4.1",
"source-map-support": "0.4.16", "source-map-support": "0.4.18",
"url-parse": "1.1.9", "url-parse": "1.1.9",
"vinyl-source-stream": "1.1.0" "vinyl-source-stream": "1.1.0"
} }

View File

@@ -1,13 +1,13 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.0.0-alpha.2", "version": "5.1.0",
"author": { "author": {
"name": "Eric Amodio", "name": "Eric Amodio",
"email": "eamodio@gmail.com" "email": "eamodio@gmail.com"
}, },
"publisher": "eamodio", "publisher": "eamodio",
"engines": { "engines": {
"vscode": "^1.15.0" "vscode": "^1.16.0"
}, },
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"displayName": "Git Lens \u2014 git blame annotations, code lens, and more", "displayName": "Git Lens \u2014 git blame annotations, code lens, and more",
@@ -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,
@@ -172,6 +167,11 @@
"default": false, "default": false,
"description": "Specifies whether or not to trigger hover annotations over the whole line" "description": "Specifies whether or not to trigger hover annotations over the whole line"
}, },
"gitlens.blame.ignoreWhitespace": {
"type": "boolean",
"default": false,
"description": "Specifies whether or not to ignore whitespace when comparing revisions during blame operations"
},
"gitlens.blame.file.annotationType": { "gitlens.blame.file.annotationType": {
"type": "string", "type": "string",
"default": "gutter", "default": "gutter",
@@ -447,6 +447,35 @@
], ],
"description": "Specifies the starting view (mode) of the `GitLens` custom view\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer" "description": "Specifies the starting view (mode) of the `GitLens` custom view\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer"
}, },
"gitlens.remotes": {
"type": "array",
"default": null,
"items": {
"type": "object",
"required": [
"type",
"domain"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Bitbucket",
"BitbucketServer",
"GitHub",
"GitLab"
],
"description": "Specifies the type of the custom remote service\n `Bitbucket`, `BitbucketServer`, `GitHub`, or `GitLab`"
},
"domain": {
"type": "string",
"description": "Specifies the domain name of the custom remote service"
}
}
},
"uniqueItems": true,
"description": "Specifies any custom domains for remote (code-hosting) services"
},
"gitlens.statusBar.enabled": { "gitlens.statusBar.enabled": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
@@ -784,6 +813,11 @@
"title": "Directory Compare", "title": "Directory Compare",
"category": "GitLens" "category": "GitLens"
}, },
{
"command": "gitlens.diffWith",
"title": "Compare File Revisions",
"category": "GitLens"
},
{ {
"command": "gitlens.diffWithBranch", "command": "gitlens.diffWithBranch",
"title": "Compare File with Branch...", "title": "Compare File with Branch...",
@@ -1060,6 +1094,16 @@
"title": "Open Files", "title": "Open Files",
"category": "GitLens" "category": "GitLens"
}, },
{
"command": "gitlens.gitExplorer.openChangedFileChanges",
"title": "Open All Changes",
"category": "GitLens"
},
{
"command": "gitlens.gitExplorer.openChangedFileChangesWithWorking",
"title": "Open All Changes with Working Tree",
"category": "GitLens"
},
{ {
"command": "gitlens.gitExplorer.openChangedFileRevisions", "command": "gitlens.gitExplorer.openChangedFileRevisions",
"title": "Open Revisions", "title": "Open Revisions",
@@ -1077,6 +1121,10 @@
"command": "gitlens.diffDirectory", "command": "gitlens.diffDirectory",
"when": "gitlens:enabled" "when": "gitlens:enabled"
}, },
{
"command": "gitlens.diffWith",
"when": "false"
},
{ {
"command": "gitlens.diffWithBranch", "command": "gitlens.diffWithBranch",
"when": "gitlens:isTracked" "when": "gitlens:isTracked"
@@ -1261,6 +1309,14 @@
"command": "gitlens.gitExplorer.openChangedFiles", "command": "gitlens.gitExplorer.openChangedFiles",
"when": "false" "when": "false"
}, },
{
"command": "gitlens.gitExplorer.openChangedFileChanges",
"when": "false"
},
{
"command": "gitlens.gitExplorer.openChangedFileChangesWithWorking",
"when": "false"
},
{ {
"command": "gitlens.gitExplorer.openChangedFileRevisions", "command": "gitlens.gitExplorer.openChangedFileRevisions",
"when": "false" "when": "false"
@@ -1466,204 +1522,224 @@
"view/title": [ "view/title": [
{ {
"command": "gitlens.showCommitSearch", "command": "gitlens.showCommitSearch",
"when": "gitlens:enabled && view == gitlens.gitExplorer", "when": "view == gitlens.gitExplorer",
"group": "navigation@1" "group": "navigation@1"
}, },
{ {
"command": "gitlens.gitExplorer.switchToHistoryView", "command": "gitlens.gitExplorer.switchToHistoryView",
"when": "gitlens:enabled && view == gitlens.gitExplorer && gitlens:gitExplorer:view == repository", "when": "view == gitlens.gitExplorer && gitlens:gitExplorer:view == repository",
"group": "navigation@2" "group": "navigation@2"
}, },
{ {
"command": "gitlens.gitExplorer.switchToRepositoryView", "command": "gitlens.gitExplorer.switchToRepositoryView",
"when": "gitlens:enabled && view == gitlens.gitExplorer && gitlens:gitExplorer:view == history", "when": "view == gitlens.gitExplorer && gitlens:gitExplorer:view == history",
"group": "navigation@3" "group": "navigation@3"
}, },
{ {
"command": "gitlens.gitExplorer.refresh", "command": "gitlens.gitExplorer.refresh",
"when": "gitlens:enabled && view == gitlens.gitExplorer", "when": "view == gitlens.gitExplorer",
"group": "navigation@4" "group": "navigation@4"
} }
], ],
"view/item/context": [ "view/item/context": [
{ {
"command": "gitlens.openBranchesInRemote", "command": "gitlens.openBranchesInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:branches", "when": "view == gitlens.gitExplorer && viewItem == gitlens:branches:remote",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.openBranchInRemote", "command": "gitlens.openBranchInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:branch-history", "when": "view == gitlens.gitExplorer && viewItem == gitlens:branch-history:remote",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.openCommitInRemote", "command": "gitlens.openCommitInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:commit", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.copyShaToClipboard", "command": "gitlens.gitExplorer.openChangedFileChanges",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "2_gitlens@1" "group": "2_gitlens@1"
}, },
{ {
"command": "gitlens.copyMessageToClipboard", "command": "gitlens.gitExplorer.openChangedFileChangesWithWorking",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "2_gitlens@2" "group": "2_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openChangedFiles", "command": "gitlens.gitExplorer.openChangedFiles",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "3_gitlens@1" "group": "3_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openChangedFileRevisions", "command": "gitlens.gitExplorer.openChangedFileRevisions",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "3_gitlens@2" "group": "3_gitlens@2"
}, },
{ {
"command": "gitlens.showQuickCommitDetails", "command": "gitlens.copyShaToClipboard",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "4_gitlens@1" "group": "4_gitlens@1"
}, },
{
"command": "gitlens.copyMessageToClipboard",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "4_gitlens@2"
},
{
"command": "gitlens.showQuickCommitDetails",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:commit",
"group": "5_gitlens@1"
},
{ {
"command": "gitlens.gitExplorer.openChanges", "command": "gitlens.gitExplorer.openChanges",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openChangesWithWorking", "command": "gitlens.gitExplorer.openChangesWithWorking",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "1_gitlens@2" "group": "1_gitlens@2"
}, },
{ {
"command": "gitlens.gitExplorer.openFile", "command": "gitlens.gitExplorer.openFile",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "2_gitlens@1" "group": "2_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openFileRevision", "command": "gitlens.gitExplorer.openFileRevision",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "2_gitlens@2" "group": "2_gitlens@2"
}, },
{ {
"command": "gitlens.openFileInRemote", "command": "gitlens.openFileInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "3_gitlens@1" "group": "3_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openFileRevisionInRemote", "command": "gitlens.gitExplorer.openFileRevisionInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "3_gitlens@2" "group": "3_gitlens@2"
}, },
{ {
"command": "gitlens.gitExplorer.applyChanges", "command": "gitlens.gitExplorer.applyChanges",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "4_gitlens@1" "group": "4_gitlens@1"
}, },
{ {
"command": "gitlens.showQuickFileHistory", "command": "gitlens.showQuickFileHistory",
"when": "gitlens:isTracked && view == gitlens.gitExplorer && viewItem == gitlens:commit-file && gitlens:gitExplorer:view == repository", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file && gitlens:gitExplorer:view == repository",
"group": "5_gitlens@1" "group": "5_gitlens@1"
}, },
{ {
"command": "gitlens.showQuickCommitFileDetails", "command": "gitlens.showQuickCommitFileDetails",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:commit-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:commit-file",
"group": "5_gitlens@2" "group": "5_gitlens@2"
}, },
{ {
"command": "gitlens.gitExplorer.openFile", "command": "gitlens.gitExplorer.openFile",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:file-history", "when": "view == gitlens.gitExplorer && viewItem == gitlens:file-history",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.openFileInRemote", "command": "gitlens.openFileInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:file-history", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:file-history",
"group": "1_gitlens@2" "group": "1_gitlens@2"
}, },
{ {
"command": "gitlens.openBranchesInRemote", "command": "gitlens.openBranchesInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:remote", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:remote",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.openRepoInRemote", "command": "gitlens.openRepoInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:remote", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:remote",
"group": "1_gitlens@2" "group": "1_gitlens@2"
}, },
{ {
"command": "gitlens.stashSave", "command": "gitlens.stashSave",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stashes", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stashes",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.stashApply", "command": "gitlens.stashApply",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "1_gitlens@1" "group": "1_gitlens@1"
}, },
{ {
"command": "gitlens.stashDelete", "command": "gitlens.stashDelete",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "1_gitlens@2" "group": "1_gitlens@2"
}, },
{ {
"command": "gitlens.copyMessageToClipboard", "command": "gitlens.gitExplorer.openChangedFileChanges",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "2_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openChangedFileChangesWithWorking",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "2_gitlens@1" "group": "2_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openChangedFiles", "command": "gitlens.gitExplorer.openChangedFiles",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "3_gitlens@1" "group": "3_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.openChangedFileRevisions", "command": "gitlens.gitExplorer.openChangedFileRevisions",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "3_gitlens@2" "group": "3_gitlens@2"
}, },
{ {
"command": "gitlens.gitExplorer.openChanges", "command": "gitlens.copyMessageToClipboard",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash",
"group": "1_gitlens@1" "group": "4_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openChangesWithWorking",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "1_gitlens@2"
},
{
"command": "gitlens.gitExplorer.openFile",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "2_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openFileRevision",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "2_gitlens@2"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "3_gitlens@1"
}, },
{ {
"command": "gitlens.gitExplorer.applyChanges", "command": "gitlens.gitExplorer.applyChanges",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "1_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openChanges",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "2_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openChangesWithWorking",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "2_gitlens@2"
},
{
"command": "gitlens.gitExplorer.openFile",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "3_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openFileRevision",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "3_gitlens@2"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "4_gitlens@1" "group": "4_gitlens@1"
}, },
{ {
"command": "gitlens.showQuickFileHistory", "command": "gitlens.showQuickFileHistory",
"when": "gitlens:isTracked && view == gitlens.gitExplorer && viewItem == gitlens:stash-file", "when": "view == gitlens.gitExplorer && viewItem == gitlens:stash-file",
"group": "5_gitlens@1" "group": "5_gitlens@1"
}, },
{ {
"command": "gitlens.openRepoInRemote", "command": "gitlens.openRepoInRemote",
"when": "gitlens:enabled && 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.refresh", "command": "gitlens.gitExplorer.refresh",
"when": "gitlens:enabled && view == gitlens.gitExplorer && viewItem != gitlens:commit-file && viewItem != gitlens:stash-file", "when": "view == gitlens.gitExplorer && viewItem != gitlens:commit-file && viewItem != gitlens:stash-file",
"group": "9_gitlens@1" "group": "9_gitlens@1"
} }
] ]
@@ -1787,7 +1863,7 @@
"dependencies": { "dependencies": {
"applicationinsights": "0.21.0", "applicationinsights": "0.21.0",
"copy-paste": "1.3.0", "copy-paste": "1.3.0",
"iconv-lite": "0.4.18", "iconv-lite": "0.4.19",
"ignore": "3.3.5", "ignore": "3.3.5",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"lodash.escaperegexp": "4.1.2", "lodash.escaperegexp": "4.1.2",
@@ -1801,10 +1877,10 @@
"devDependencies": { "devDependencies": {
"@types/copy-paste": "1.1.30", "@types/copy-paste": "1.1.30",
"@types/iconv-lite": "0.0.1", "@types/iconv-lite": "0.0.1",
"@types/mocha": "2.2.42", "@types/mocha": "2.2.43",
"@types/node": "8.0.26", "@types/node": "8.0.28",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"mocha": "3.5.0", "mocha": "3.5.2",
"tslint": "5.7.0", "tslint": "5.7.0",
"typescript": "2.5.2", "typescript": "2.5.2",
"vscode": "1.1.5" "vscode": "1.1.5"

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,9 +1,9 @@
import { Strings } from '../system'; import { Dates, Strings } from '../system';
import { DecorationInstanceRenderOptions, DecorationOptions, ThemableDecorationRenderOptions } from 'vscode'; import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode';
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;
@@ -27,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';
@@ -47,7 +47,7 @@ export class Annotations {
return '#793738'; return '#793738';
} }
static getHoverMessage(commit: GitCommit, dateFormat: string | null): string | string[] { 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';
} }
@@ -63,16 +63,25 @@ export class Annotations {
.replace(/\n/g, ' \n'); .replace(/\n/g, ' \n');
message = `\n\n> ${message}`; message = `\n\n> ${message}`;
} }
return `\`${commit.shortSha}\` &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;
return markdown;
} }
static getHoverDiffMessage(commit: GitCommit, chunkLine: GitDiffChunkLine | undefined): string | undefined { static getHoverDiffMessage(commit: GitCommit, chunkLine: GitDiffChunkLine | undefined): MarkdownString | undefined {
if (chunkLine === undefined) return undefined; if (chunkLine === undefined) return undefined;
const codeDiff = this._getCodeDiff(chunkLine); const codeDiff = this._getCodeDiff(chunkLine);
return commit.isUncommitted const markdown = new MarkdownString(commit.isUncommitted
? `\`Changes\` &nbsp; ${GlyphChars.Dash} &nbsp; _uncommitted_\n${codeDiff}` ? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") &nbsp; ${GlyphChars.Dash} &nbsp; _uncommitted_\n${codeDiff}`
: `\`Changes\` &nbsp; ${GlyphChars.Dash} &nbsp; \`${commit.previousShortSha}\` ${GlyphChars.ArrowLeftRight} \`${commit.shortSha}\`\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;
return markdown;
} }
private static _getCodeDiff(chunkLine: GitDiffChunkLine): string { private static _getCodeDiff(chunkLine: GitDiffChunkLine): string {
@@ -92,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;
@@ -158,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 {
@@ -181,7 +191,10 @@ export class Annotations {
} }
static trailing(commit: GitCommit, format: string, dateFormat: string | null, cfgTheme: IThemeConfig): DecorationOptions { static trailing(commit: GitCommit, format: string, dateFormat: string | null, cfgTheme: IThemeConfig): DecorationOptions {
const message = CommitFormatter.fromTemplate(format, commit, dateFormat); const message = CommitFormatter.fromTemplate(format, commit, {
truncateMessageAtNewLine: true,
dateFormat: dateFormat
} as ICommitFormatOptions);
return { return {
renderOptions: { renderOptions: {
after: { after: {

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

@@ -1,9 +1,10 @@
'use strict'; 'use strict';
import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; import { DecorationOptions, ExtensionContext, MarkdownString, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { Annotations, endOfLineIndex } from './annotations'; 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,21 +37,16 @@ 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);
} }
let message: string | undefined = undefined; let message: MarkdownString | undefined = undefined;
if (cfg.hover.changes) { if (cfg.hover.changes) {
message = Annotations.getHoverDiffMessage(commit, line); message = Annotations.getHoverDiffMessage(commit, line);
} }
@@ -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

@@ -8,6 +8,7 @@ export * from './commands/copyShaToClipboard';
export * from './commands/diffDirectory'; export * from './commands/diffDirectory';
export * from './commands/diffLineWithPrevious'; export * from './commands/diffLineWithPrevious';
export * from './commands/diffLineWithWorking'; export * from './commands/diffLineWithWorking';
export * from './commands/diffWith';
export * from './commands/diffWithBranch'; export * from './commands/diffWithBranch';
export * from './commands/diffWithNext'; export * from './commands/diffWithNext';
export * from './commands/diffWithPrevious'; export * from './commands/diffWithPrevious';

View File

@@ -11,6 +11,7 @@ export type Commands =
'gitlens.copyMessageToClipboard' | 'gitlens.copyMessageToClipboard' |
'gitlens.copyShaToClipboard' | 'gitlens.copyShaToClipboard' |
'gitlens.diffDirectory' | 'gitlens.diffDirectory' |
'gitlens.diffWith' |
'gitlens.diffWithBranch' | 'gitlens.diffWithBranch' |
'gitlens.diffWithNext' | 'gitlens.diffWithNext' |
'gitlens.diffWithPrevious' | 'gitlens.diffWithPrevious' |
@@ -52,6 +53,7 @@ export const Commands = {
CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands, CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands,
CopyShaToClipboard: 'gitlens.copyShaToClipboard' as Commands, CopyShaToClipboard: 'gitlens.copyShaToClipboard' as Commands,
DiffDirectory: 'gitlens.diffDirectory' as Commands, DiffDirectory: 'gitlens.diffDirectory' as Commands,
DiffWith: 'gitlens.diffWith' as Commands,
DiffWithBranch: 'gitlens.diffWithBranch' as Commands, DiffWithBranch: 'gitlens.diffWithBranch' as Commands,
DiffWithNext: 'gitlens.diffWithNext' as Commands, DiffWithNext: 'gitlens.diffWithNext' as Commands,
DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands, DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands,
@@ -166,6 +168,10 @@ function isTextEditor(editor: any): editor is TextEditor {
export abstract class Command extends Disposable { export abstract class Command extends Disposable {
static getMarkdownCommandArgsCore<T>(command: Commands, args: T): string {
return `command:${command}?${encodeURIComponent(JSON.stringify(args))}`;
}
protected readonly contextParsingOptions: CommandContextParsingOptions = { editor: false, uri: false }; protected readonly contextParsingOptions: CommandContextParsingOptions = { editor: false, uri: false };
private _disposable: Disposable; private _disposable: Disposable;

View File

@@ -1,16 +1,14 @@
'use strict'; 'use strict';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands, GlyphChars } from '../constants'; import { DiffWithCommandArgs } from './diffWith';
import { DiffWithPreviousCommandArgs } from './diffWithPrevious';
import { DiffWithWorkingCommandArgs } from './diffWithWorking';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages'; import { Messages } from '../messages';
import * as path from 'path';
export interface DiffLineWithPreviousCommandArgs { export interface DiffLineWithPreviousCommandArgs {
commit?: GitCommit; commit?: GitCommit;
line?: number; line?: number;
showOptions?: TextDocumentShowOptions; showOptions?: TextDocumentShowOptions;
} }
@@ -43,56 +41,26 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand {
if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare'); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
args.commit = blame.commit; args.commit = blame.commit;
// If we don't have a sha or the current commit matches the blame, show the previous
if (gitUri.sha === undefined || gitUri.sha === args.commit.sha) {
return commands.executeCommand(Commands.DiffWithPrevious, new GitUri(uri, args.commit), {
line: args.line,
showOptions: args.showOptions
} as DiffWithPreviousCommandArgs);
}
// If the line is uncommitted, find the previous commit and treat it as a DiffWithWorking
if (args.commit.isUncommitted) {
uri = args.commit.uri;
args.commit = new GitCommit(args.commit.type, args.commit.repoPath, args.commit.previousSha!, args.commit.previousFileName!, args.commit.author, args.commit.date, args.commit.message);
args.line = (blame.line.line + 1) + gitUri.offset;
return commands.executeCommand(Commands.DiffWithWorking, uri, {
commit: args.commit,
line: args.line,
showOptions: args.showOptions
} as DiffWithWorkingCommandArgs);
}
} }
catch (ex) { catch (ex) {
Logger.error(ex, 'DiffWithPreviousLineCommand', `getBlameForLine(${blameline})`); Logger.error(ex, 'DiffLineWithPreviousCommand', `getBlameForLine(${blameline})`);
return window.showErrorMessage(`Unable to open compare. See output channel for more details`); return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
} }
} }
try { const diffArgs: DiffWithCommandArgs = {
const [rhs, lhs] = await Promise.all([ repoPath: args.commit.repoPath,
this.git.getVersionedFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha!), lhs: {
this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha) sha: args.commit.previousSha !== undefined ? args.commit.previousSha : GitService.fakeSha,
]); uri: args.commit.previousUri
},
if (args.line !== undefined && args.line !== 0) { rhs: {
if (args.showOptions === undefined) { sha: args.commit.sha,
args.showOptions = {}; uri: args.commit.uri
} },
args.showOptions.selection = new Range(args.line, 0, args.line, 0); line: args.line,
} showOptions: args.showOptions
};
await commands.executeCommand(BuiltInCommands.Diff, return commands.executeCommand(Commands.DiffWith, diffArgs);
Uri.file(lhs),
Uri.file(rhs),
`${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(gitUri.fsPath)} (${gitUri.shortSha})`,
args.showOptions);
}
catch (ex) {
Logger.error(ex, 'DiffWithPreviousLineCommand', 'getVersionedFile');
return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
}
} }
} }

View File

@@ -1,13 +1,14 @@
'use strict'; 'use strict';
import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { DiffWithCommandArgs } from './diffWith';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Messages } from '../messages'; import { Messages } from '../messages';
import { Logger } from '../logger'; import { Logger } from '../logger';
export interface DiffLineWithWorkingCommandArgs { export interface DiffLineWithWorkingCommandArgs {
commit?: GitCommit; commit?: GitCommit;
line?: number; line?: number;
showOptions?: TextDocumentShowOptions; showOptions?: TextDocumentShowOptions;
} }
@@ -52,6 +53,19 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand {
} }
} }
return commands.executeCommand(Commands.DiffWithWorking, uri, args as DiffWithWorkingCommandArgs); const diffArgs: DiffWithCommandArgs = {
repoPath: args.commit.repoPath,
lhs: {
sha: args.commit.sha,
uri: args.commit.uri
},
rhs: {
sha: '',
uri: args.commit.uri
},
line: args.line,
showOptions: args.showOptions
};
return commands.executeCommand(Commands.DiffWith, diffArgs);
} }
} }

142
src/commands/diffWith.ts Normal file
View File

@@ -0,0 +1,142 @@
'use strict';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands } from './common';
import { BuiltInCommands, GlyphChars } from '../constants';
import { GitCommit, GitService } from '../gitService';
import { Logger } from '../logger';
import * as path from 'path';
export interface DiffWithCommandArgsRevision {
sha: string;
uri: Uri;
title?: string;
}
export interface DiffWithCommandArgs {
lhs?: DiffWithCommandArgsRevision;
rhs?: DiffWithCommandArgsRevision;
repoPath?: string;
line?: number;
showOptions?: TextDocumentShowOptions;
}
export class DiffWithCommand extends ActiveEditorCommand {
static getMarkdownCommandArgs(args: DiffWithCommandArgs): string;
static getMarkdownCommandArgs(commit1: GitCommit, commit2: GitCommit): string;
static getMarkdownCommandArgs(argsOrCommit1: DiffWithCommandArgs | GitCommit, commit2?: GitCommit): string {
let args = argsOrCommit1;
if (argsOrCommit1 instanceof GitCommit) {
const commit1 = argsOrCommit1;
if (commit2 === undefined) {
if (commit1.isUncommitted) {
args = {
repoPath: commit1.repoPath,
lhs: {
sha: 'HEAD',
uri: commit1.uri
},
rhs: {
sha: '',
uri: commit1.uri
}
};
}
else {
args = {
repoPath: commit1.repoPath,
lhs: {
sha: commit1.previousSha!,
uri: commit1.previousUri!
},
rhs: {
sha: commit1.sha,
uri: commit1.uri
}
};
}
}
else {
args = {
repoPath: commit1.repoPath,
lhs: {
sha: commit1.sha,
uri: commit1.uri
},
rhs: {
sha: commit2.sha,
uri: commit2.uri
}
};
}
}
return super.getMarkdownCommandArgsCore<DiffWithCommandArgs>(Commands.DiffWith, args);
}
constructor(private git: GitService) {
super(Commands.DiffWith);
}
async execute(editor?: TextEditor, uri?: Uri, args: DiffWithCommandArgs = {}): Promise<any> {
args = { ...args };
if (args.repoPath === undefined || args.lhs === undefined || args.rhs === undefined) return undefined;
try {
const [lhs, rhs] = await Promise.all([
args.lhs.sha !== '' && !GitService.isUncommitted(args.lhs.sha)
? this.git.getVersionedFile(args.repoPath, args.lhs.uri.fsPath, args.lhs.sha)
: args.lhs.uri.fsPath,
args.rhs.sha !== '' && !GitService.isUncommitted(args.rhs.sha)
? this.git.getVersionedFile(args.repoPath, args.rhs.uri.fsPath, args.rhs.sha)
: args.rhs.uri.fsPath
]);
if (args.line !== undefined && args.line !== 0) {
if (args.showOptions === undefined) {
args.showOptions = {};
}
args.showOptions.selection = new Range(args.line, 0, args.line, 0);
}
let rhsPrefix = '';
if (rhs === undefined) {
rhsPrefix = 'deleted in ';
}
else if (lhs === undefined || args.lhs.sha === GitService.fakeSha) {
rhsPrefix = 'added in ';
}
if (args.lhs.title === undefined && lhs !== undefined && args.lhs.sha !== GitService.fakeSha) {
args.lhs.title = (args.lhs.sha === '' || GitService.isUncommitted(args.lhs.sha))
? `${path.basename(args.lhs.uri.fsPath)}`
: `${path.basename(args.lhs.uri.fsPath)} (${GitService.shortenSha(args.lhs.sha)})`;
}
if (args.rhs.title === undefined && args.rhs.sha !== GitService.fakeSha) {
args.rhs.title = (args.rhs.sha === '' || GitService.isUncommitted(args.rhs.sha))
? `${path.basename(args.rhs.uri.fsPath)}`
: `${path.basename(args.rhs.uri.fsPath)} (${rhsPrefix}${GitService.shortenSha(args.rhs.sha)})`;
}
const title = (args.lhs.title !== undefined && args.rhs.title !== undefined)
? `${args.lhs.title} ${GlyphChars.ArrowLeftRight} ${args.rhs.title}`
: args.lhs.title || args.rhs.title;
return await commands.executeCommand(BuiltInCommands.Diff,
lhs === undefined
? GitService.toGitContentUri(GitService.fakeSha, args.lhs.uri.fsPath, args.repoPath)
: Uri.file(lhs),
rhs === undefined
? GitService.toGitContentUri(GitService.fakeSha, args.rhs.uri.fsPath, args.repoPath)
: Uri.file(rhs),
title,
args.showOptions);
}
catch (ex) {
Logger.error(ex, 'DiffWithCommand', 'getVersionedFile');
return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
}
}
}

View File

@@ -1,9 +1,9 @@
'use strict'; 'use strict';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, TextDocumentShowOptions, TextEditor, Uri } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands, GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { DiffWithCommandArgs } from './diffWith';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger';
import { Messages } from '../messages'; import { Messages } from '../messages';
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks'; import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
import * as path from 'path'; import * as path from 'path';
@@ -42,25 +42,20 @@ export class DiffWithBranchCommand extends ActiveEditorCommand {
const branch = pick.branch.name; const branch = pick.branch.name;
if (branch === undefined) return undefined; if (branch === undefined) return undefined;
try { const diffArgs: DiffWithCommandArgs = {
const compare = await this.git.getVersionedFile(gitUri.repoPath, gitUri.fsPath, branch); repoPath: gitUri.repoPath,
lhs: {
if (args.line !== undefined && args.line !== 0) { sha: pick.branch.remote ? `remotes/${branch}` : branch,
if (args.showOptions === undefined) { uri: gitUri as Uri,
args.showOptions = {}; title: `${path.basename(gitUri.fsPath)} (${branch})`
} },
args.showOptions.selection = new Range(args.line, 0, args.line, 0); rhs: {
} sha: '',
uri: gitUri as Uri
await commands.executeCommand(BuiltInCommands.Diff, },
Uri.file(compare), line: args.line,
gitUri.fileUri(), showOptions: args.showOptions
`${path.basename(gitUri.fsPath)} (${branch}) ${GlyphChars.ArrowLeftRight} ${path.basename(gitUri.fsPath)}`, };
args.showOptions); return commands.executeCommand(Commands.DiffWith, diffArgs);
}
catch (ex) {
Logger.error(ex, 'DiffWithBranchCommand', 'getVersionedFile');
return window.showErrorMessage(`Unable to open branch compare. See output channel for more details`);
}
} }
} }

View File

@@ -2,16 +2,16 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands, GlyphChars } from '../constants'; import { DiffWithCommandArgs } from './diffWith';
import { GitLogCommit, GitService, GitUri } from '../gitService'; import { GitLogCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages'; import { Messages } from '../messages';
import * as path from 'path';
export interface DiffWithNextCommandArgs { export interface DiffWithNextCommandArgs {
commit?: GitLogCommit; commit?: GitLogCommit;
line?: number;
range?: Range; range?: Range;
line?: number;
showOptions?: TextDocumentShowOptions; showOptions?: TextDocumentShowOptions;
} }
@@ -54,28 +54,19 @@ export class DiffWithNextCommand extends ActiveEditorCommand {
if (args.commit.nextSha === undefined) return commands.executeCommand(Commands.DiffWithWorking, uri); if (args.commit.nextSha === undefined) return commands.executeCommand(Commands.DiffWithWorking, uri);
try { const diffArgs: DiffWithCommandArgs = {
const [rhs, lhs] = await Promise.all([ repoPath: args.commit.repoPath,
this.git.getVersionedFile(args.commit.repoPath, args.commit.nextUri.fsPath, args.commit.nextSha), lhs: {
this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha) sha: args.commit.sha,
]); uri: args.commit.uri
},
if (args.line !== undefined && args.line !== 0) { rhs: {
if (args.showOptions === undefined) { sha: args.commit.nextSha,
args.showOptions = {}; uri: args.commit.nextUri
} },
args.showOptions.selection = new Range(args.line, 0, args.line, 0); line: args.line,
} showOptions: args.showOptions
};
await commands.executeCommand(BuiltInCommands.Diff, return commands.executeCommand(Commands.DiffWith, diffArgs);
Uri.file(lhs),
Uri.file(rhs),
`${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(args.commit.nextUri.fsPath)} (${args.commit.nextShortSha})`,
args.showOptions);
}
catch (ex) {
Logger.error(ex, 'DiffWithNextCommand', 'getVersionedFile');
return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
}
} }
} }

View File

@@ -2,22 +2,18 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands, FakeSha, GlyphChars } from '../constants'; import { DiffWithCommandArgs } from './diffWith';
import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { DiffWithWorkingCommandArgs } from './diffWithWorking';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages'; import { Messages } from '../messages';
import * as path from 'path';
export interface DiffWithPreviousCommandArgs { export interface DiffWithPreviousCommandArgs {
commit?: GitCommit; commit?: GitCommit;
line?: number;
range?: Range; range?: Range;
showOptions?: TextDocumentShowOptions;
allowMissingPrevious?: boolean; line?: number;
leftTitlePrefix?: string; showOptions?: TextDocumentShowOptions;
rightTitlePrefix?: string;
} }
export class DiffWithPreviousCommand extends ActiveEditorCommand { export class DiffWithPreviousCommand extends ActiveEditorCommand {
@@ -40,6 +36,7 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
try { try {
const sha = args.commit === undefined ? gitUri.sha : args.commit.sha; const sha = args.commit === undefined ? gitUri.sha : args.commit.sha;
if (sha === GitService.fakeSha) return Messages.showCommitHasNoPreviousCommitWarningMessage();
const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, sha, { maxCount: 2, range: args.range!, skipMerges: true }); const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, sha, { maxCount: 2, range: args.range!, skipMerges: true });
if (log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare'); if (log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
@@ -47,7 +44,9 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values());
// If the sha is missing and the file is uncommitted, then treat it as a DiffWithWorking // If the sha is missing and the file is uncommitted, then treat it as a DiffWithWorking
if (gitUri.sha === undefined && await this.git.isFileUncommitted(gitUri)) return commands.executeCommand(Commands.DiffWithWorking, uri, { commit: args.commit, showOptions: args.showOptions } as DiffWithWorkingCommandArgs); if (gitUri.sha === undefined && await this.git.isFileUncommitted(gitUri)) {
return commands.executeCommand(Commands.DiffWithWorking, uri, { commit: args.commit, showOptions: args.showOptions } as DiffWithWorkingCommandArgs);
}
} }
catch (ex) { catch (ex) {
Logger.error(ex, 'DiffWithPreviousCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`); Logger.error(ex, 'DiffWithPreviousCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`);
@@ -55,32 +54,19 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
} }
} }
if (args.commit.previousSha === undefined && !args.allowMissingPrevious) return Messages.showCommitHasNoPreviousCommitWarningMessage(args.commit); const diffArgs: DiffWithCommandArgs = {
repoPath: args.commit.repoPath,
try { lhs: {
const [rhs, lhs] = await Promise.all([ sha: args.commit.previousSha !== undefined ? args.commit.previousSha : GitService.fakeSha,
this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha), uri: args.commit.previousUri
this.git.getVersionedFile(args.commit.repoPath, args.commit.previousUri.fsPath, args.commit.previousSha === undefined ? FakeSha : args.commit.previousSha) },
]); rhs: {
sha: args.commit.sha,
if (args.line !== undefined && args.line !== 0) { uri: args.commit.uri
if (args.showOptions === undefined) { },
args.showOptions = {}; line: args.line,
} showOptions: args.showOptions
args.showOptions.selection = new Range(args.line, 0, args.line, 0); };
} return commands.executeCommand(Commands.DiffWith, diffArgs);
await commands.executeCommand(BuiltInCommands.Diff,
Uri.file(lhs),
Uri.file(rhs),
args.commit.previousShortSha === undefined
? `${path.basename(args.commit.uri.fsPath)} (${args.rightTitlePrefix || ''}${args.commit.shortSha})`
: `${path.basename(args.commit.previousUri.fsPath)} (${args.leftTitlePrefix || ''}${args.commit.previousShortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(args.commit.uri.fsPath)} (${args.rightTitlePrefix || ''}${args.commit.shortSha})`,
args.showOptions);
}
catch (ex) {
Logger.error(ex, 'DiffWithPreviousCommand', 'getVersionedFile');
return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
}
} }
} }

View File

@@ -1,16 +1,16 @@
'use strict'; 'use strict';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands, GlyphChars } from '../constants'; import { DiffWithCommandArgs } from './diffWith';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages'; import { Messages } from '../messages';
import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks';
import * as path from 'path';
export interface DiffWithRevisionCommandArgs { export interface DiffWithRevisionCommandArgs {
line?: number;
maxCount?: number; maxCount?: number;
line?: number;
showOptions?: TextDocumentShowOptions; showOptions?: TextDocumentShowOptions;
} }
@@ -46,24 +46,24 @@ export class DiffWithRevisionCommand extends ActiveEditorCommand {
if (pick instanceof CommandQuickPickItem) return pick.execute(); if (pick instanceof CommandQuickPickItem) return pick.execute();
const compare = await this.git.getVersionedFile(gitUri.repoPath, gitUri.fsPath, pick.commit.sha); const diffArgs: DiffWithCommandArgs = {
repoPath: gitUri.repoPath,
if (args.line !== undefined && args.line !== 0) { lhs: {
if (args.showOptions === undefined) { sha: pick.commit.sha,
args.showOptions = {}; uri: gitUri as Uri
} },
args.showOptions.selection = new Range(args.line, 0, args.line, 0); rhs: {
} sha: '',
uri: gitUri as Uri
await commands.executeCommand(BuiltInCommands.Diff, },
Uri.file(compare), line: args.line,
gitUri.fileUri(), showOptions: args.showOptions
`${path.basename(gitUri.fsPath)} (${pick.commit.shortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(gitUri.fsPath)}`, };
args.showOptions); return await commands.executeCommand(Commands.DiffWith, diffArgs);
} }
catch (ex) { catch (ex) {
Logger.error(ex, 'DiffWithRevisionCommand', 'getVersionedFile'); Logger.error(ex, 'DiffWithRevisionCommand');
return window.showErrorMessage(`Unable to open history compare. See output channel for more details`); return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
} }
} }
} }

View File

@@ -1,14 +1,14 @@
'use strict'; 'use strict';
import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands, GlyphChars } from '../constants'; import { DiffWithCommandArgs } from './diffWith';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages'; import { Messages } from '../messages';
import * as path from 'path';
export interface DiffWithWorkingCommandArgs { export interface DiffWithWorkingCommandArgs {
commit?: GitCommit; commit?: GitCommit;
line?: number; line?: number;
showOptions?: TextDocumentShowOptions; showOptions?: TextDocumentShowOptions;
} }
@@ -48,25 +48,19 @@ export class DiffWithWorkingCommand extends ActiveEditorCommand {
const workingFileName = await this.git.findWorkingFileName(gitUri.repoPath, gitUri.fsPath); const workingFileName = await this.git.findWorkingFileName(gitUri.repoPath, gitUri.fsPath);
if (workingFileName === undefined) return undefined; if (workingFileName === undefined) return undefined;
try { const diffArgs: DiffWithCommandArgs = {
const compare = await this.git.getVersionedFile(args.commit.repoPath, args.commit.uri.fsPath, args.commit.sha); repoPath: args.commit.repoPath,
lhs: {
if (args.line !== undefined && args.line !== 0) { sha: args.commit.sha,
if (args.showOptions === undefined) { uri: args.commit.uri
args.showOptions = {}; },
} rhs: {
args.showOptions.selection = new Range(args.line, 0, args.line, 0); sha: '',
} uri: args.commit.uri
},
await commands.executeCommand(BuiltInCommands.Diff, line: args.line,
Uri.file(compare), showOptions: args.showOptions
Uri.file(path.resolve(gitUri.repoPath, workingFileName)), };
`${path.basename(args.commit.uri.fsPath)} (${args.commit.shortSha}) ${GlyphChars.ArrowLeftRight} ${path.basename(workingFileName)}`, return commands.executeCommand(Commands.DiffWith, diffArgs);
args.showOptions);
}
catch (ex) {
Logger.error(ex, 'DiffWithWorkingCommand', 'getVersionedFile');
return window.showErrorMessage(`Unable to open compare. See output channel for more details`);
}
} }
} }

View File

@@ -1,5 +1,4 @@
'use strict'; 'use strict';
import { Arrays } from '../system';
import { commands, TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithBranch } from './common'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithBranch } from './common';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
@@ -22,10 +21,8 @@ export class OpenBranchInRemoteCommand extends ActiveEditorCommand {
protected async preExecute(context: CommandContext, args: OpenBranchInRemoteCommandArgs = {}): Promise<any> { protected async preExecute(context: CommandContext, args: OpenBranchInRemoteCommandArgs = {}): Promise<any> {
if (isCommandViewContextWithBranch(context)) { if (isCommandViewContextWithBranch(context)) {
args = { ...args }; args = { ...args };
args.branch = context.node.branch.name; args.branch = context.node.branch.name;
args.remote = context.node.branch.getRemote(); args.remote = context.node.branch.getRemote();
return this.execute(context.editor, context.uri, args);
} }
return this.execute(context.editor, context.uri, args); return this.execute(context.editor, context.uri, args);
@@ -54,7 +51,8 @@ export class OpenBranchInRemoteCommand extends ActiveEditorCommand {
if (args.branch === undefined) return undefined; if (args.branch === undefined) return undefined;
} }
const remotes = Arrays.uniqueBy(await this.git.getRemotes(repoPath), _ => _.url, _ => !!_.provider); const remotes = (await this.git.getRemotes(repoPath)).filter(r => r.provider !== undefined);
return commands.executeCommand(Commands.OpenInRemote, uri, { return commands.executeCommand(Commands.OpenInRemote, uri, {
resource: { resource: {
type: 'branch', type: 'branch',

View File

@@ -1,5 +1,4 @@
'use strict'; 'use strict';
import { Arrays } from '../system';
import { commands, TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithRemote } from './common'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithRemote } from './common';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
@@ -20,7 +19,6 @@ export class OpenBranchesInRemoteCommand extends ActiveEditorCommand {
if (isCommandViewContextWithRemote(context)) { if (isCommandViewContextWithRemote(context)) {
args = { ...args }; args = { ...args };
args.remote = context.node.remote.name; args.remote = context.node.remote.name;
return this.execute(context.editor, context.uri, args);
} }
return this.execute(context.editor, context.uri, args); return this.execute(context.editor, context.uri, args);
@@ -35,7 +33,7 @@ export class OpenBranchesInRemoteCommand extends ActiveEditorCommand {
if (!repoPath) return undefined; if (!repoPath) return undefined;
try { try {
const remotes = Arrays.uniqueBy(await this.git.getRemotes(repoPath), r => r.url, r => !!r.provider); const remotes = (await this.git.getRemotes(repoPath)).filter(r => r.provider !== undefined);
return commands.executeCommand(Commands.OpenInRemote, uri, { return commands.executeCommand(Commands.OpenInRemote, uri, {
resource: { resource: {

View File

@@ -1,5 +1,4 @@
'use strict'; 'use strict';
import { Arrays } from '../system';
import { commands, TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common';
import { GitBlameCommit, GitService, GitUri } from '../gitService'; import { GitBlameCommit, GitService, GitUri } from '../gitService';
@@ -13,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);
} }
@@ -53,7 +61,8 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand {
args.sha = commit.sha; args.sha = commit.sha;
} }
const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider); const remotes = (await this.git.getRemotes(gitUri.repoPath)).filter(r => r.provider !== undefined);
return commands.executeCommand(Commands.OpenInRemote, uri, { return commands.executeCommand(Commands.OpenInRemote, uri, {
resource: { resource: {
type: 'commit', type: 'commit',

View File

@@ -1,12 +1,12 @@
'use strict'; 'use strict';
import { Arrays } from '../system';
import { commands, Range, TextEditor, Uri, window } from 'vscode'; import { commands, Range, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithBranch, isCommandViewContextWithCommit } from './common';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { OpenInRemoteCommandArgs } from './openInRemote'; import { OpenInRemoteCommandArgs } from './openInRemote';
export interface OpenFileInRemoteCommandArgs { export interface OpenFileInRemoteCommandArgs {
branch?: string;
range?: boolean; range?: boolean;
} }
@@ -16,10 +16,13 @@ export class OpenFileInRemoteCommand extends ActiveEditorCommand {
super(Commands.OpenFileInRemote); super(Commands.OpenFileInRemote);
} }
protected async preExecute(context: CommandContext, args: OpenFileInRemoteCommandArgs = {}): Promise<any> { protected async preExecute(context: CommandContext, args: OpenFileInRemoteCommandArgs = { range: true }): Promise<any> {
if (isCommandViewContextWithCommit(context)) { if (isCommandViewContextWithCommit(context)) {
args = { ...args }; args = { ...args };
args.range = false; args.range = false;
if (isCommandViewContextWithBranch(context)) {
args.branch = context.node.branch !== undefined ? context.node.branch.name : undefined;
}
return this.execute(context.editor, context.node.commit.uri, args); return this.execute(context.editor, context.node.commit.uri, args);
} }
@@ -33,18 +36,23 @@ export class OpenFileInRemoteCommand extends ActiveEditorCommand {
const gitUri = await GitUri.fromUri(uri, this.git); const gitUri = await GitUri.fromUri(uri, this.git);
if (!gitUri.repoPath) return undefined; if (!gitUri.repoPath) return undefined;
const branch = await this.git.getBranch(gitUri.repoPath); if (args.branch === undefined) {
const branch = await this.git.getBranch(gitUri.repoPath);
if (branch !== undefined) {
args.branch = branch.name;
}
}
try { try {
const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider); const remotes = (await this.git.getRemotes(gitUri.repoPath)).filter(r => r.provider !== undefined);
const range = (args.range && editor !== undefined) const range = (args.range && editor !== undefined)
? new Range(editor.selection.start.with({ line: editor.selection.start.line + 1 }), editor.selection.end.with({ line: editor.selection.end.line + 1 })) ? new Range(editor.selection.start.with({ line: editor.selection.start.line + 1 }), editor.selection.end.with({ line: editor.selection.end.line + 1 }))
: undefined; : undefined;
return commands.executeCommand(Commands.OpenInRemote, uri, { return commands.executeCommand(Commands.OpenInRemote, uri, {
resource: { resource: {
type: 'file', type: gitUri.sha === undefined ? 'file' : 'revision',
branch: branch === undefined ? 'Current' : branch.name, branch: args.branch === undefined ? 'Current' : args.branch,
fileName: gitUri.getRelativePath(), fileName: gitUri.getRelativePath(),
range: range, range: range,
sha: gitUri.sha sha: gitUri.sha

View File

@@ -3,7 +3,7 @@ import { Strings } from '../system';
import { TextEditor, Uri, window } from 'vscode'; import { TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { GitLogCommit, GitRemote, RemoteResource } from '../gitService'; import { GitLogCommit, GitRemote, GitService, RemoteResource } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { CommandQuickPickItem, OpenRemoteCommandQuickPickItem, RemotesQuickPick } from '../quickPicks'; import { CommandQuickPickItem, OpenRemoteCommandQuickPickItem, RemotesQuickPick } from '../quickPicks';
@@ -50,7 +50,7 @@ export class OpenInRemoteCommand extends ActiveEditorCommand {
break; break;
case 'commit': case 'commit':
const shortSha = args.resource.sha.substring(0, 8); const shortSha = GitService.shortenSha(args.resource.sha);
placeHolder = `open commit ${shortSha} in${GlyphChars.Ellipsis}`; placeHolder = `open commit ${shortSha} in${GlyphChars.Ellipsis}`;
break; break;
@@ -70,7 +70,7 @@ export class OpenInRemoteCommand extends ActiveEditorCommand {
} }
} }
else { else {
const shortFileSha = args.resource.sha === undefined ? '' : args.resource.sha.substring(0, 8); const shortFileSha = args.resource.sha === undefined ? '' : GitService.shortenSha(args.resource.sha);
const shaSuffix = shortFileSha ? ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${shortFileSha}` : ''; const shaSuffix = shortFileSha ? ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${shortFileSha}` : '';
placeHolder = `open ${args.resource.fileName}${shaSuffix} in${GlyphChars.Ellipsis}`; placeHolder = `open ${args.resource.fileName}${shaSuffix} in${GlyphChars.Ellipsis}`;

View File

@@ -1,5 +1,4 @@
'use strict'; 'use strict';
import { Arrays } from '../system';
import { commands, TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithRemote } from './common'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithRemote } from './common';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
@@ -20,7 +19,6 @@ export class OpenRepoInRemoteCommand extends ActiveEditorCommand {
if (isCommandViewContextWithRemote(context)) { if (isCommandViewContextWithRemote(context)) {
args = { ...args }; args = { ...args };
args.remote = context.node.remote.name; args.remote = context.node.remote.name;
return this.execute(context.editor, context.uri, args);
} }
return this.execute(context.editor, context.uri, args); return this.execute(context.editor, context.uri, args);
@@ -35,7 +33,7 @@ export class OpenRepoInRemoteCommand extends ActiveEditorCommand {
if (!repoPath) return undefined; if (!repoPath) return undefined;
try { try {
const remotes = Arrays.uniqueBy(await this.git.getRemotes(repoPath), r => r.url, r => !!r.provider); const remotes = (await this.git.getRemotes(repoPath)).filter(r => r.provider !== undefined);
return commands.executeCommand(Commands.OpenInRemote, uri, { return commands.executeCommand(Commands.OpenInRemote, uri, {
resource: { resource: {

View File

@@ -20,20 +20,29 @@ export interface ShowQuickCommitDetailsCommandArgs {
export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand { export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
static getMarkdownCommandArgs(sha: string): string;
static getMarkdownCommandArgs(args: ShowQuickCommitDetailsCommandArgs): string;
static getMarkdownCommandArgs(argsOrSha: ShowQuickCommitDetailsCommandArgs | string): string {
const args = typeof argsOrSha === 'string'
? { sha: argsOrSha }
: argsOrSha;
return super.getMarkdownCommandArgsCore<ShowQuickCommitDetailsCommandArgs>(Commands.ShowQuickCommitDetails, args);
}
constructor(private git: GitService) { constructor(private git: GitService) {
super(Commands.ShowQuickCommitDetails); super(Commands.ShowQuickCommitDetails);
} }
protected async preExecute(context: CommandContext, ...args: any[]): Promise<any> { protected async preExecute(context: CommandContext, args: ShowQuickCommitDetailsCommandArgs = {}): Promise<any> {
if (context.type === 'view') { if (context.type === 'view') {
args = { ...args };
args.sha = context.node.uri.sha;
if (isCommandViewContextWithCommit(context)) { if (isCommandViewContextWithCommit(context)) {
args = [{ sha: context.node.uri.sha, commit: context.node.commit }]; args.commit = context.node.commit;
}
else {
args = [{ sha: context.node.uri.sha }];
} }
} }
return this.execute(context.editor, context.uri, ...args); return this.execute(context.editor, context.uri, args);
} }
async execute(editor?: TextEditor, uri?: Uri, args: ShowQuickCommitDetailsCommandArgs = {}) { async execute(editor?: TextEditor, uri?: Uri, args: ShowQuickCommitDetailsCommandArgs = {}) {

View File

@@ -20,20 +20,29 @@ export interface ShowQuickCommitFileDetailsCommandArgs {
export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand { export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand {
static getMarkdownCommandArgs(sha: string): string;
static getMarkdownCommandArgs(args: ShowQuickCommitFileDetailsCommandArgs): string;
static getMarkdownCommandArgs(argsOrSha: ShowQuickCommitFileDetailsCommandArgs | string): string {
const args = typeof argsOrSha === 'string'
? { sha: argsOrSha }
: argsOrSha;
return super.getMarkdownCommandArgsCore<ShowQuickCommitFileDetailsCommandArgs>(Commands.ShowQuickCommitFileDetails, args);
}
constructor(private git: GitService) { constructor(private git: GitService) {
super(Commands.ShowQuickCommitFileDetails); super(Commands.ShowQuickCommitFileDetails);
} }
protected async preExecute(context: CommandContext, ...args: any[]): Promise<any> { protected async preExecute(context: CommandContext, args: ShowQuickCommitFileDetailsCommandArgs = {}): Promise<any> {
if (context.type === 'view') { if (context.type === 'view') {
args = { ...args };
args.sha = context.node.uri.sha;
if (isCommandViewContextWithCommit(context)) { if (isCommandViewContextWithCommit(context)) {
args = [{ sha: context.node.uri.sha, commit: context.node.commit }]; args.commit = context.node.commit;
}
else {
args = [{ sha: context.node.uri.sha }];
} }
} }
return this.execute(context.editor, context.uri, ...args); return this.execute(context.editor, context.uri, args);
} }
async execute(editor?: TextEditor, uri?: Uri, args: ShowQuickCommitFileDetailsCommandArgs = {}) { async execute(editor?: TextEditor, uri?: Uri, args: ShowQuickCommitFileDetailsCommandArgs = {}) {
@@ -95,7 +104,7 @@ export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand
args.commit.workingFileName = workingFileName; args.commit.workingFileName = workingFileName;
args.commit.workingFileName = await this.git.findWorkingFileName(args.commit); args.commit.workingFileName = await this.git.findWorkingFileName(args.commit);
const shortSha = args.sha!.substring(0, 8); const shortSha = GitService.shortenSha(args.sha!);
if (args.goBackCommand === undefined) { if (args.goBackCommand === undefined) {
// Create a command to get back to the commit details // Create a command to get back to the commit details

View File

@@ -28,7 +28,7 @@ export class StashApplyCommand extends Command {
return this.execute(args); return this.execute(args);
} }
return super.preExecute(context, args); return this.execute(args);
} }
async execute(args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) { async execute(args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) {

View File

@@ -26,7 +26,7 @@ export class StashDeleteCommand extends Command {
return this.execute(args); return this.execute(args);
} }
return super.preExecute(context, args); return this.execute(args);
} }
async execute(args: StashDeleteCommandArgs = { confirm: true }) { async execute(args: StashDeleteCommandArgs = { confirm: true }) {

View File

@@ -35,7 +35,7 @@ export class StashSaveCommand extends Command {
return this.execute(args); return this.execute(args);
} }
async execute(args: StashSaveCommandArgs = { }) { async execute(args: StashSaveCommandArgs = {}) {
if (!this.git.repoPath) return undefined; if (!this.git.repoPath) return undefined;
try { try {

View File

@@ -7,7 +7,8 @@ import { OutputLevel } from './logger';
export { ExtensionKey } from './constants'; export { ExtensionKey } from './constants';
export type CodeLensCommand = 'gitlens.toggleFileBlame' | export type CodeLensCommand =
'gitlens.toggleFileBlame' |
'gitlens.showBlameHistory' | 'gitlens.showBlameHistory' |
'gitlens.showFileHistory' | 'gitlens.showFileHistory' |
'gitlens.diffWithPrevious' | 'gitlens.diffWithPrevious' |
@@ -41,7 +42,19 @@ export const LineHighlightLocations = {
OverviewRuler: 'overviewRuler' as LineHighlightLocations OverviewRuler: 'overviewRuler' as LineHighlightLocations
}; };
export type StatusBarCommand = 'gitlens.toggleFileBlame' | export type CustomRemoteType =
'Bitbucket' |
'GitHub' |
'GitLab';
export const CustomRemoteType = {
Bitbucket: 'Bitbucket' as CustomRemoteType,
BitbucketServer: 'BitbucketServer' as CustomRemoteType,
GitHub: 'GitHub' as CustomRemoteType,
GitLab: 'GitLab' as CustomRemoteType
};
export type StatusBarCommand =
'gitlens.toggleFileBlame' |
'gitlens.showBlameHistory' | 'gitlens.showBlameHistory' |
'gitlens.showFileHistory' | 'gitlens.showFileHistory' |
'gitlens.toggleCodeLens' | 'gitlens.toggleCodeLens' |
@@ -119,6 +132,11 @@ export interface ICodeLensLanguageLocation {
customSymbols?: string[]; customSymbols?: string[];
} }
export interface IRemotesConfig {
type: CustomRemoteType;
domain: string;
}
export interface IThemeConfig { export interface IThemeConfig {
annotations: { annotations: {
file: { file: {
@@ -233,7 +251,6 @@ export interface IConfig {
hover: { hover: {
details: boolean; details: boolean;
changes: boolean; changes: boolean;
wholeLine: boolean;
}; };
}; };
}; };
@@ -257,6 +274,8 @@ export interface IConfig {
}; };
blame: { blame: {
ignoreWhitespace: boolean;
file: { file: {
annotationType: FileAnnotationType; annotationType: FileAnnotationType;
lineHighlight: { lineHighlight: {
@@ -307,6 +326,8 @@ export interface IConfig {
// dateFormat: string | null; // dateFormat: string | null;
}; };
remotes: IRemotesConfig[];
statusBar: { statusBar: {
enabled: boolean; enabled: boolean;
alignment: 'left' | 'right'; alignment: 'left' | 'right';

View File

@@ -8,8 +8,6 @@ export const QualifiedExtensionId = `eamodio.${ExtensionId}`;
export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679'; export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679';
export const FakeSha = 'ffffffffffffffffffffffffffffffffffffffff';
export type BuiltInCommands = 'cursorMove' | export type BuiltInCommands = 'cursorMove' |
'editor.action.showReferences' | 'editor.action.showReferences' |
'editor.action.toggleRenderWhitespace' | 'editor.action.toggleRenderWhitespace' |
@@ -79,8 +77,10 @@ export type GlyphChars = '\u21a9' |
'\u2937' | '\u2937' |
'\u2190' | '\u2190' |
'\u2194' | '\u2194' |
'\u2192' |
'\u21e8' | '\u21e8' |
'\u2191' | '\u2191' |
'\u2197' |
'\u2713' | '\u2713' |
'\u2014' | '\u2014' |
'\u2022' | '\u2022' |
@@ -93,8 +93,10 @@ export const GlyphChars = {
ArrowDropRight: '\u2937' as GlyphChars, ArrowDropRight: '\u2937' as GlyphChars,
ArrowLeft: '\u2190' as GlyphChars, ArrowLeft: '\u2190' as GlyphChars,
ArrowLeftRight: '\u2194' as GlyphChars, ArrowLeftRight: '\u2194' 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

@@ -7,7 +7,7 @@ import { Commands } from './commands';
import { TextEditorComparer } from './comparers'; import { TextEditorComparer } from './comparers';
import { IConfig, StatusBarCommand } from './configuration'; import { IConfig, StatusBarCommand } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants'; import { DocumentSchemes, ExtensionKey } from './constants';
import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri } from './gitService'; import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri, ICommitFormatOptions } from './gitService';
import { Logger } from './logger'; import { Logger } from './logger';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
@@ -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) { 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) { if (showAtStart) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace)); decorationOptions.push(Annotations.withRange(decoration, 0, 0));
} }
} }
} }
@@ -462,7 +408,10 @@ export class CurrentLineController extends Disposable {
const cfg = this._config.statusBar; const cfg = this._config.statusBar;
if (!cfg.enabled || this._statusBarItem === undefined) return; if (!cfg.enabled || this._statusBarItem === undefined) return;
this._statusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, cfg.dateFormat === null ? this._config.defaultDateFormat : cfg.dateFormat)}`; this._statusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, {
truncateMessageAtNewLine: true,
dateFormat: cfg.dateFormat === null ? this._config.defaultDateFormat : cfg.dateFormat
} as ICommitFormatOptions)}`;
switch (cfg.command) { switch (cfg.command) {
case StatusBarCommand.BlameAnnotate: case StatusBarCommand.BlameAnnotate:

View File

@@ -5,7 +5,7 @@ import { AnnotationController } from './annotations/annotationController';
import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands';
import { OpenBranchesInRemoteCommand, OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands'; import { OpenBranchesInRemoteCommand, OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands';
import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands';
import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands';
import { ResetSuppressedWarningsCommand } from './commands'; import { ResetSuppressedWarningsCommand } from './commands';
import { ClearFileAnnotationsCommand, ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleFileRecentChangesCommand, ToggleLineBlameCommand } from './commands'; import { ClearFileAnnotationsCommand, ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleFileRecentChangesCommand, ToggleLineBlameCommand } from './commands';
import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands';
@@ -19,6 +19,7 @@ import { CodeLensLocations, IConfig, LineHighlightLocations } from './configurat
import { ApplicationInsightsKey, CommandContext, ExtensionKey, QualifiedExtensionId, setCommandContext, WorkspaceState } from './constants'; import { ApplicationInsightsKey, CommandContext, ExtensionKey, QualifiedExtensionId, setCommandContext, WorkspaceState } from './constants';
import { CodeLensController } from './codeLensController'; import { CodeLensController } from './codeLensController';
import { CurrentLineController, LineAnnotationType } from './currentLineController'; import { CurrentLineController, LineAnnotationType } from './currentLineController';
import { RemoteProviderFactory } from './git/remotes/factory';
import { GitContentProvider } from './gitContentProvider'; import { GitContentProvider } from './gitContentProvider';
import { GitExplorer } from './views/gitExplorer'; import { GitExplorer } from './views/gitExplorer';
import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider';
@@ -33,6 +34,7 @@ export async function activate(context: ExtensionContext) {
Logger.configure(context); Logger.configure(context);
Messages.configure(context); Messages.configure(context);
Telemetry.configure(ApplicationInsightsKey); Telemetry.configure(ApplicationInsightsKey);
RemoteProviderFactory.configure(context);
const gitlens = extensions.getExtension(QualifiedExtensionId)!; const gitlens = extensions.getExtension(QualifiedExtensionId)!;
const gitlensVersion = gitlens.packageJSON.version; const gitlensVersion = gitlens.packageJSON.version;
@@ -103,6 +105,7 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(new DiffDirectoryCommand(git)); context.subscriptions.push(new DiffDirectoryCommand(git));
context.subscriptions.push(new DiffLineWithPreviousCommand(git)); context.subscriptions.push(new DiffLineWithPreviousCommand(git));
context.subscriptions.push(new DiffLineWithWorkingCommand(git)); context.subscriptions.push(new DiffLineWithWorkingCommand(git));
context.subscriptions.push(new DiffWithCommand(git));
context.subscriptions.push(new DiffWithBranchCommand(git)); context.subscriptions.push(new DiffWithBranchCommand(git));
context.subscriptions.push(new DiffWithNextCommand(git)); context.subscriptions.push(new DiffWithNextCommand(git));
context.subscriptions.push(new DiffWithPreviousCommand(git)); context.subscriptions.push(new DiffWithPreviousCommand(git));

View File

@@ -2,9 +2,11 @@
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';
export interface ICommitFormatOptions extends IFormatOptions { export interface ICommitFormatOptions extends IFormatOptions {
truncateMessageAtNewLine?: boolean;
tokenOptions?: { tokenOptions?: {
ago?: Strings.ITokenOptions; ago?: Strings.ITokenOptions;
author?: Strings.ITokenOptions; author?: Strings.ITokenOptions;
@@ -17,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);
} }
@@ -27,12 +29,12 @@ 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);
} }
@@ -41,7 +43,14 @@ export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions>
} }
get message() { get message() {
const message = this._item.isUncommitted ? 'Uncommitted change' : this._item.message; let message = this._item.isUncommitted ? 'Uncommitted change' : this._item.message;
if (this._options.truncateMessageAtNewLine) {
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
}
return this._padOrTruncate(message, this._options.tokenOptions!.message); return this._padOrTruncate(message, this._options.tokenOptions!.message);
} }

View File

@@ -1,4 +1,5 @@
'use strict'; 'use strict';
import { Strings } from '../system';
import { findGitPath, IGit } from './gitLocator'; import { findGitPath, IGit } from './gitLocator';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { spawnPromise } from 'spawn-rx'; import { spawnPromise } from 'spawn-rx';
@@ -13,15 +14,16 @@ export * from './parsers/blameParser';
export * from './parsers/branchParser'; export * from './parsers/branchParser';
export * from './parsers/diffParser'; export * from './parsers/diffParser';
export * from './parsers/logParser'; export * from './parsers/logParser';
export * from './parsers/remoteParser';
export * from './parsers/stashParser'; export * from './parsers/stashParser';
export * from './parsers/statusParser'; export * from './parsers/statusParser';
export * from './remotes/provider'; 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) {
@@ -38,38 +40,49 @@ const GitWarnings = [
/no upstream configured for branch/ /no upstream configured for branch/
]; ];
async function gitCommand(options: { cwd: string, encoding?: string, onError?: (ex: Error) => string | undefined }, ...args: any[]) { interface GitCommandOptions {
cwd: string;
encoding?: string;
overrideErrorHandling?: boolean;
}
async function gitCommand(options: GitCommandOptions, ...args: any[]): Promise<string> {
if (options.overrideErrorHandling) return gitCommandCore(options, ...args);
try { try {
// Fixes https://github.com/eamodio/vscode-gitlens/issues/73 return await gitCommandCore(options, ...args);
// See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x
args.splice(0, 0, '-c', 'core.quotepath=false');
const opts = { encoding: 'utf8', ...options };
const s = await spawnPromise(git.path, args, { cwd: options.cwd, encoding: (opts.encoding === 'utf8') ? 'utf8' : 'binary' });
Logger.log('git', ...args, ` cwd='${options.cwd}'`);
if (opts.encoding === 'utf8' || opts.encoding === 'binary') return s;
return iconv.decode(Buffer.from(s, 'binary'), opts.encoding);
} }
catch (ex) { catch (ex) {
if (options.onError !== undefined) { return gitCommandDefaultErrorHandler(ex, options, ...args);
const result = options.onError(ex); }
if (result !== undefined) return result; }
}
const msg = ex && ex.toString(); async function gitCommandCore(options: GitCommandOptions, ...args: any[]): Promise<string> {
if (msg) { // Fixes https://github.com/eamodio/vscode-gitlens/issues/73
for (const warning of GitWarnings) { // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x
if (warning.test(msg)) { args.splice(0, 0, '-c', 'core.quotepath=false');
Logger.warn('git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
return ''; const opts = { encoding: 'utf8', ...options };
} const s = await spawnPromise(git.path, args, { cwd: options.cwd, encoding: (opts.encoding === 'utf8') ? 'utf8' : 'binary' });
Logger.log('git', ...args, ` cwd='${options.cwd}'`);
if (opts.encoding === 'utf8' || opts.encoding === 'binary') return s;
return iconv.decode(Buffer.from(s, 'binary'), opts.encoding);
}
function gitCommandDefaultErrorHandler(ex: Error, options: GitCommandOptions, ...args: any[]): string {
const msg = ex && ex.toString();
if (msg) {
for (const warning of GitWarnings) {
if (warning.test(msg)) {
Logger.warn('git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
return '';
} }
} }
Logger.error(ex, 'git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
throw ex;
} }
Logger.error(ex, 'git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
throw ex;
} }
export class Git { export class Git {
@@ -98,8 +111,9 @@ export class Git {
static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) { static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) {
const data = await Git.show(repoPath, fileName, branchOrSha, 'binary'); const data = await Git.show(repoPath, fileName, branchOrSha, 'binary');
if (data === undefined) return undefined;
const suffix = Git.isSha(branchOrSha) ? branchOrSha.substring(0, 8) : branchOrSha; const suffix = Strings.truncate(Strings.sanitizeForFS(Git.isSha(branchOrSha) ? Git.shortenSha(branchOrSha) : branchOrSha), 50, '');
const ext = path.extname(fileName); const ext = path.extname(fileName);
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
tmp.file({ prefix: `${path.basename(fileName, ext)}-${suffix}__`, postfix: ext }, tmp.file({ prefix: `${path.basename(fileName, ext)}-${suffix}__`, postfix: ext },
@@ -134,6 +148,10 @@ export class Git {
return fileName && fileName.replace(/\\/g, '/'); return fileName && fileName.replace(/\\/g, '/');
} }
static shortenSha(sha: string) {
return sha.substring(0, 8);
}
static splitPath(fileName: string, repoPath: string | undefined, extract: boolean = true): [string, string] { static splitPath(fileName: string, repoPath: string | undefined, extract: boolean = true): [string, string] {
if (repoPath) { if (repoPath) {
fileName = this.normalizePath(fileName); fileName = this.normalizePath(fileName);
@@ -159,15 +177,17 @@ export class Git {
// Git commands // Git commands
static blame(repoPath: string | undefined, fileName: string, sha?: string, 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 (startLine != null && endLine != null) { if (options.ignoreWhitespace) {
params.push(`-L ${startLine},${endLine}`); params.push('-w');
}
if (options.startLine != null && options.endLine != null) {
params.push(`-L ${options.startLine},${options.endLine}`);
} }
if (sha) { if (sha) {
params.push(sha); params.push(sha);
} }
@@ -184,16 +204,20 @@ export class Git {
return gitCommand({ cwd: repoPath }, ...params); return gitCommand({ cwd: repoPath }, ...params);
} }
static branch_current(repoPath: string) { static async branch_current(repoPath: string) {
const params = [`rev-parse`, `--abbrev-ref`, `--symbolic-full-name`, `@`, `@{u}`]; const params = [`rev-parse`, `--abbrev-ref`, `--symbolic-full-name`, `@`, `@{u}`];
const onError = (ex: Error) => {
const opts = { cwd: repoPath, overrideErrorHandling: true };
try {
return await gitCommand(opts, ...params);
}
catch (ex) {
if (/no upstream configured for branch/.test(ex && ex.toString())) { if (/no upstream configured for branch/.test(ex && ex.toString())) {
return ex.message.split('\n')[0]; return ex.message.split('\n')[0];
} }
return undefined; return gitCommandDefaultErrorHandler(ex, opts, ...params);
}; }
return gitCommand({ cwd: repoPath, onError: onError }, ...params);
} }
static checkout(repoPath: string, fileName: string, sha: string) { static checkout(repoPath: string, fileName: string, sha: string) {
@@ -204,9 +228,9 @@ export class Git {
static async config_get(key: string, repoPath?: string) { static async config_get(key: string, repoPath?: string) {
try { try {
return await gitCommand({ cwd: repoPath || '' }, `config`, `--get`, key); return await gitCommand({ cwd: repoPath || '', overrideErrorHandling: true }, `config`, `--get`, key);
} }
catch (ex) { catch {
return ''; return '';
} }
} }
@@ -310,9 +334,9 @@ export class Git {
static async ls_files(repoPath: string, fileName: string): Promise<string> { static async ls_files(repoPath: string, fileName: string): Promise<string> {
try { try {
return await gitCommand({ cwd: repoPath }, 'ls-files', fileName); return await gitCommand({ cwd: repoPath, overrideErrorHandling: true }, 'ls-files', fileName);
} }
catch (ex) { catch {
return ''; return '';
} }
} }
@@ -325,12 +349,25 @@ export class Git {
return gitCommand({ cwd: repoPath }, 'remote', 'get-url', remote); return gitCommand({ cwd: repoPath }, 'remote', 'get-url', remote);
} }
static show(repoPath: string | undefined, fileName: string, branchOrSha: string, encoding?: string) { static async show(repoPath: string | undefined, fileName: string, branchOrSha: string, encoding?: string) {
const [file, root] = Git.splitPath(fileName, repoPath); const [file, root] = Git.splitPath(fileName, repoPath);
branchOrSha = branchOrSha.replace('^', ''); branchOrSha = branchOrSha.replace('^', '');
if (Git.isUncommitted(branchOrSha)) return Promise.reject(new Error(`sha=${branchOrSha} is uncommitted`)); if (Git.isUncommitted(branchOrSha)) throw new Error(`sha=${branchOrSha} is uncommitted`);
return gitCommand({ cwd: root, encoding: encoding || defaultEncoding }, 'show', `${branchOrSha}:./${file}`);
const opts = { cwd: root, encoding: encoding || defaultEncoding, overrideErrorHandling: true };
const args = `${branchOrSha}:./${file}`;
try {
return await gitCommand(opts, 'show', args);
}
catch (ex) {
const msg = ex && ex.toString();
if (/Path \'.*?\' does not exist in/.test(msg) || /Path \'.*?\' exists on disk, but not in /.test(msg)) {
return undefined;
}
return gitCommandDefaultErrorHandler(ex, opts, args);
}
} }
static stash_apply(repoPath: string, stashName: string, deleteAfter: boolean) { static stash_apply(repoPath: string, stashName: string, deleteAfter: boolean) {

View File

@@ -2,7 +2,7 @@
import { Disposable, Event, EventEmitter, TextDocument, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; import { Disposable, Event, EventEmitter, TextDocument, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode';
import { TextDocumentComparer } from '../comparers'; import { TextDocumentComparer } from '../comparers';
import { CommandContext, setCommandContext } from '../constants'; import { CommandContext, setCommandContext } from '../constants';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri, RepoChangedReasons } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
export interface BlameabilityChangeEvent { export interface BlameabilityChangeEvent {
@@ -32,13 +32,13 @@ export class GitContextTracker extends Disposable {
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
subscriptions.push(workspace.onDidSaveTextDocument(this._onTextDocumentSaved, this)); subscriptions.push(workspace.onDidSaveTextDocument(this._onTextDocumentSaved, this));
subscriptions.push(this.git.onDidBlameFail(this._onBlameFailed, this)); subscriptions.push(this.git.onDidBlameFail(this._onBlameFailed, this));
subscriptions.push(this.git.onDidChangeRepo(this._onRepoChanged, this));
this._disposable = Disposable.from(...subscriptions); this._disposable = Disposable.from(...subscriptions);
setCommandContext(CommandContext.IsRepository, !!this.git.repoPath); setCommandContext(CommandContext.IsRepository, !!this.git.repoPath);
this._onConfigurationChanged(); this._onConfigurationChanged();
this._onActiveTextEditorChanged(window.activeTextEditor);
} }
dispose() { dispose() {
@@ -55,6 +55,13 @@ export class GitContextTracker extends Disposable {
} }
} }
async _onRepoChanged(reasons: RepoChangedReasons[]) {
if (!reasons.includes(RepoChangedReasons.Remotes)) return;
const gitUri = this._editor === undefined ? undefined : await GitUri.fromUri(this._editor.document.uri, this.git);
this._updateContextHasRemotes(gitUri);
}
private _onActiveTextEditorChanged(editor: TextEditor | undefined) { private _onActiveTextEditorChanged(editor: TextEditor | undefined) {
this._editor = editor; this._editor = editor;
this._updateContext(this._gitEnabled ? editor : undefined); this._updateContext(this._gitEnabled ? editor : undefined);

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;
@@ -56,7 +56,7 @@ export class GitUri extends Uri {
} }
get shortSha() { get shortSha() {
return this.sha && this.sha.substring(0, 8); return this.sha && GitService.shortenSha(this.sha);
} }
fileUri() { fileUri() {

View File

@@ -13,11 +13,6 @@ export class GitBranch {
this.remote = true; this.remote = true;
} }
const index = branch.indexOf(' ');
if (index !== -1) {
branch = branch.substring(0, index);
}
this.current = current; this.current = current;
this.name = branch; this.name = branch;
this.tracking = tracking; this.tracking = tracking;

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';
@@ -51,7 +51,7 @@ export class GitCommit {
} }
get shortSha() { get shortSha() {
return this.sha.substring(0, 8); return Git.shortenSha(this.sha);
} }
get isUncommitted(): boolean { get isUncommitted(): boolean {
@@ -62,7 +62,7 @@ export class GitCommit {
} }
get previousShortSha() { get previousShortSha() {
return this.previousSha && this.previousSha.substring(0, 8); return this.previousSha && Git.shortenSha(this.previousSha);
} }
get previousUri(): Uri { get previousUri(): Uri {
@@ -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

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
import { Uri } from 'vscode'; import { Uri } from 'vscode';
import { GitCommit, GitCommitType } from './commit'; import { GitCommit, GitCommitType } from './commit';
import { Git } from '../git';
import { GitStatusFileStatus, IGitStatusFile } from './status'; import { GitStatusFileStatus, IGitStatusFile } from './status';
import * as path from 'path'; import * as path from 'path';
@@ -39,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;
} }
} }
@@ -49,7 +55,7 @@ export class GitLogCommit extends GitCommit {
} }
get nextShortSha() { get nextShortSha() {
return this.nextSha && this.nextSha.substring(0, 8); return this.nextSha && Git.shortenSha(this.nextSha);
} }
get nextUri(): Uri { get nextUri(): Uri {

View File

@@ -5,23 +5,9 @@ export type GitRemoteType = 'fetch' | 'push';
export class GitRemote { export class GitRemote {
name: string;
url: string;
type: GitRemoteType;
provider?: RemoteProvider; provider?: RemoteProvider;
constructor(remote: string) { constructor(public readonly repoPath: string, public readonly name: string, public readonly url: string, public readonly domain: string, public readonly path: string, public readonly types: GitRemoteType[]) {
remote = remote.trim(); this.provider = RemoteProviderFactory.getRemoteProvider(this.domain, this.path);
const [name, info] = remote.split('\t');
this.name = name;
const [url, typeInfo] = info.split(' ');
this.url = url;
this.type = typeInfo.substring(1, typeInfo.length - 1) as GitRemoteType;
this.provider = RemoteProviderFactory.getRemoteProvider(this.url);
} }
} }

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

@@ -0,0 +1,50 @@
'use strict';
import { GitRemote } from './../git';
import { GitRemoteType } from '../models/remote';
const remoteRegex = /^(.*)\t(.*)\s\((.*)\)$/gm;
const urlRegex = /^(?:git:\/\/(.*?)\/|https:\/\/(.*?)\/|http:\/\/(.*?)\/|git@(.*):|ssh:\/\/(?:.*@)?(.*?)(?::.*?)?\/)(.*)$/;
export class GitRemoteParser {
static parse(data: string, repoPath: string): GitRemote[] {
if (!data) return [];
const remotes: GitRemote[] = [];
const groups = Object.create(null);
let match: RegExpExecArray | null = null;
do {
match = remoteRegex.exec(data);
if (match == null) break;
const url = match[2];
const [domain, path] = this.parseGitUrl(url);
let remote: GitRemote | undefined = groups[url];
if (remote === undefined) {
remote = new GitRemote(repoPath, match[1], url, domain, path, [match[3] as GitRemoteType]);
remotes.push(remote);
groups[url] = remote;
}
else {
remote.types.push(match[3] as GitRemoteType);
}
} while (match != null);
if (!remotes.length) return [];
return remotes;
}
static parseGitUrl(url: string): [string, string] {
const match = urlRegex.exec(url);
if (match == null) return ['', ''];
return [
match[1] || match[2] || match[3] || match[4] || match[5],
match[6].replace(/\.git\/?$/, '')
];
}
}

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

@@ -0,0 +1,47 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
export class BitbucketServerService extends RemoteProvider {
constructor(public domain: string, public path: string, public custom: boolean = false) {
super(domain, path);
}
get name() {
return this.formatName('Bitbucket Server');
}
protected get baseUrl() {
const [project, repo] = super.splitPath();
return `https://${this.domain}/projects/${project}/repos/${repo}`;
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}
protected getUrlForBranch(branch: string): string {
return `${this.baseUrl}/commits?until=${branch}`;
}
protected getUrlForCommit(sha: string): string {
return `${this.baseUrl}/commits/${sha}`;
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = `#${range.start.line}`;
}
else {
line = `#${range.start.line}-${range.end.line}`;
}
}
if (sha) return `${this.baseUrl}/browse/${fileName}?at=${sha}${line}`;
if (branch) return `${this.baseUrl}/browse/${fileName}?at=${branch}${line}`;
return `${this.baseUrl}/browse/${fileName}${line}`;
}
}

View File

@@ -4,12 +4,12 @@ import { RemoteProvider } from './provider';
export class BitbucketService extends RemoteProvider { export class BitbucketService extends RemoteProvider {
constructor(public domain: string, public path: string) { constructor(public domain: string, public path: string, public custom: boolean = false) {
super(domain, path); super(domain, path);
} }
get name() { get name() {
return 'Bitbucket'; return this.formatName('Bitbucket');
} }
protected getUrlForBranches(): string { protected getUrlForBranches(): string {

View File

@@ -1,38 +1,49 @@
'use strict'; 'use strict';
import { RemoteProvider } from './provider'; import { Objects } from '../../system';
import { Event, EventEmitter, ExtensionContext, workspace } from 'vscode';
import { BitbucketService } from './bitbucket'; import { BitbucketService } from './bitbucket';
import { BitbucketServerService } from './bitbucket-server';
import { CustomRemoteType, IConfig, IRemotesConfig } from '../../configuration';
import { ExtensionKey } from '../../constants';
import { GitHubService } from './github'; import { GitHubService } from './github';
import { GitLabService } from './gitlab'; import { GitLabService } from './gitlab';
import { VisualStudioService } from './visualStudio';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { RemoteProvider } from './provider';
import { VisualStudioService } from './visualStudio';
export { RemoteProvider }; export { RemoteProvider };
const providerMap = new Map<string, (domain: string, path: string) => RemoteProvider>([ const defaultProviderMap = new Map<string, (domain: string, path: string) => RemoteProvider>([
['bitbucket.org', (domain: string, path: string) => new BitbucketService(domain, path)], ['bitbucket.org', (domain: string, path: string) => new BitbucketService(domain, path)],
['github.com', (domain: string, path: string) => new GitHubService(domain, path)], ['github.com', (domain: string, path: string) => new GitHubService(domain, path)],
['gitlab.com', (domain: string, path: string) => new GitLabService(domain, path)], ['gitlab.com', (domain: string, path: string) => new GitLabService(domain, path)],
['visualstudio.com', (domain: string, path: string) => new VisualStudioService(domain, path)] ['visualstudio.com', (domain: string, path: string) => new VisualStudioService(domain, path)]
]); ]);
const UrlRegex = /^(?:git:\/\/(.*?)\/|https:\/\/(.*?)\/|http:\/\/(.*?)\/|git@(.*):|ssh:\/\/(?:.*@)?(.*?)(?::.*?)?\/)(.*)$/;
export class RemoteProviderFactory { export class RemoteProviderFactory {
static getRemoteProvider(url: string): RemoteProvider | undefined { private static _providerMap: Map<string, (domain: string, path: string) => RemoteProvider>;
private static _remotesCfg: IRemotesConfig[];
private static _onDidChange = new EventEmitter<void>();
public static get onDidChange(): Event<void> {
return this._onDidChange.event;
}
static configure(context: ExtensionContext) {
context.subscriptions.push(workspace.onDidChangeConfiguration(() => this.onConfigurationChanged()));
this.onConfigurationChanged(true);
}
static getRemoteProvider(domain: string, path: string): RemoteProvider | undefined {
try { try {
const match = UrlRegex.exec(url); let key = domain.toLowerCase();
if (match == null) return undefined; if (key.endsWith('visualstudio.com')) {
key = 'visualstudio.com';
}
const domain = match[1] || match[2] || match[3] || match[4] || match[5]; const creator = this._providerMap.get(key);
const path = match[6].replace(/\.git\/?$/, ''); if (creator === undefined) return undefined;
const key = domain.toLowerCase().endsWith('visualstudio.com')
? 'visualstudio.com'
: domain;
const creator = providerMap.get(key.toLowerCase());
if (!creator) return undefined;
return creator(domain, path); return creator(domain, path);
} }
@@ -41,4 +52,37 @@ export class RemoteProviderFactory {
return undefined; return undefined;
} }
} }
private static onConfigurationChanged(silent: boolean = false) {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey);
if (cfg === undefined) return;
if (!Objects.areEquivalent(cfg.remotes, this._remotesCfg)) {
this._providerMap = new Map(defaultProviderMap);
this._remotesCfg = cfg.remotes;
if (this._remotesCfg != null && this._remotesCfg.length > 0) {
for (const svc of this._remotesCfg) {
const provider = this.getCustomProvider(svc.type);
if (provider === undefined) continue;
this._providerMap.set(svc.domain.toLowerCase(), provider);
}
if (!silent) {
this._onDidChange.fire();
}
}
}
}
private static getCustomProvider(type: CustomRemoteType) {
switch (type) {
case CustomRemoteType.Bitbucket: return (domain: string, path: string) => new BitbucketService(domain, path, true);
case CustomRemoteType.BitbucketServer: return (domain: string, path: string) => new BitbucketServerService(domain, path, true);
case CustomRemoteType.GitHub: return (domain: string, path: string) => new GitHubService(domain, path, true);
case CustomRemoteType.GitLab: return (domain: string, path: string) => new GitLabService(domain, path, true);
}
return undefined;
}
} }

View File

@@ -4,12 +4,12 @@ import { RemoteProvider } from './provider';
export class GitHubService extends RemoteProvider { export class GitHubService extends RemoteProvider {
constructor(public domain: string, public path: string) { constructor(public domain: string, public path: string, public custom: boolean = false) {
super(domain, path); super(domain, path);
} }
get name() { get name() {
return 'GitHub'; return this.formatName('GitHub');
} }
protected getUrlForBranches(): string { protected getUrlForBranches(): string {

View File

@@ -3,11 +3,11 @@ import { GitHubService } from './github';
export class GitLabService extends GitHubService { export class GitLabService extends GitHubService {
constructor(public domain: string, public path: string) { constructor(public domain: string, public path: string, public custom: boolean = false) {
super(domain, path); super(domain, path);
} }
get name() { get name() {
return 'GitLab'; return this.formatName('GitLab');
} }
} }

View File

@@ -26,7 +26,7 @@ export function getNameFromRemoteResource(resource: RemoteResource) {
export abstract class RemoteProvider { export abstract class RemoteProvider {
constructor(public domain: string, public path: string) { } constructor(public domain: string, public path: string, public custom: boolean = false) { }
abstract get name(): string; abstract get name(): string;
@@ -34,6 +34,15 @@ export abstract class RemoteProvider {
return `https://${this.domain}/${this.path}`; return `https://${this.domain}/${this.path}`;
} }
protected formatName(name: string) {
return `${name}${this.custom ? ` (${this.domain})` : ''}`;
}
protected splitPath(): [string, string] {
const index = this.path.indexOf('/');
return [ this.path.substring(0, index), this.path.substring(index + 1) ];
}
protected abstract getUrlForBranches(): string; protected abstract getUrlForBranches(): string;
protected abstract getUrlForBranch(branch: string): string; protected abstract getUrlForBranch(branch: string): string;
protected abstract getUrlForCommit(sha: string): string; protected abstract getUrlForCommit(sha: string): string;

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

@@ -15,7 +15,9 @@ export class GitContentProvider implements TextDocumentContentProvider {
const data = GitService.fromGitContentUri(uri); const data = GitService.fromGitContentUri(uri);
const fileName = data.originalFileName || data.fileName; const fileName = data.originalFileName || data.fileName;
try { try {
let text = await this.git.getVersionedFileText(data.repoPath, fileName, data.sha); let text = data.sha !== GitService.fakeSha
? await this.git.getVersionedFileText(data.repoPath, fileName, data.sha)
: '';
if (data.decoration) { if (data.decoration) {
text = `${data.decoration}\n${text}`; text = `${data.decoration}\n${text}`;
} }
@@ -23,7 +25,7 @@ export class GitContentProvider implements TextDocumentContentProvider {
} }
catch (ex) { catch (ex) {
Logger.error(ex, 'GitContentProvider', 'getVersionedFileText'); Logger.error(ex, 'GitContentProvider', 'getVersionedFileText');
window.showErrorMessage(`Unable to show Git revision ${data.sha.substring(0, 8)} of '${path.relative(data.repoPath, fileName)}'`); window.showErrorMessage(`Unable to show Git revision ${GitService.shortenSha(data.sha)} of '${path.relative(data.repoPath, fileName)}'`);
return undefined; return undefined;
} }
} }

View File

@@ -3,12 +3,12 @@ import { Functions, Iterables, Objects } from './system';
import { Disposable, Event, EventEmitter, FileSystemWatcher, Location, Position, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, workspace } from 'vscode'; import { Disposable, Event, EventEmitter, FileSystemWatcher, Location, Position, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, workspace } from 'vscode';
import { IConfig } from './configuration'; import { IConfig } from './configuration';
import { DocumentSchemes, ExtensionKey, GlyphChars } from './constants'; import { DocumentSchemes, ExtensionKey, GlyphChars } from './constants';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitDiff, GitDiffChunkLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git'; import { RemoteProviderFactory } from './git/remotes/factory';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitDiff, GitDiffChunkLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git';
import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; 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 };
@@ -64,14 +64,17 @@ export const GitRepoSearchBy = {
Sha: 'sha' as GitRepoSearchBy Sha: 'sha' as GitRepoSearchBy
}; };
export type RepoChangedReasons = 'stash' | 'unknown'; export type RepoChangedReasons = 'remotes' | 'stash' | 'unknown';
export const RepoChangedReasons = { export const RepoChangedReasons = {
Remotes: 'remotes' as RepoChangedReasons,
Stash: 'stash' as RepoChangedReasons, Stash: 'stash' as RepoChangedReasons,
Unknown: 'unknown' as RepoChangedReasons Unknown: 'unknown' as RepoChangedReasons
}; };
export class GitService extends Disposable { export class GitService extends Disposable {
static fakeSha = 'ffffffffffffffffffffffffffffffffffffffff';
private _onDidBlameFail = new EventEmitter<string>(); private _onDidBlameFail = new EventEmitter<string>();
get onDidBlameFail(): Event<string> { get onDidBlameFail(): Event<string> {
return this._onDidBlameFail.event; return this._onDidBlameFail.event;
@@ -96,7 +99,6 @@ export class GitService extends Disposable {
private _disposable: Disposable | undefined; private _disposable: Disposable | undefined;
private _gitignore: Promise<ignore.Ignore | undefined>; private _gitignore: Promise<ignore.Ignore | undefined>;
private _repoWatcher: FileSystemWatcher | undefined; private _repoWatcher: FileSystemWatcher | undefined;
private _stashWatcher: FileSystemWatcher | undefined;
static EmptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined); static EmptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined);
@@ -112,6 +114,7 @@ export class GitService extends Disposable {
const subscriptions: Disposable[] = []; const subscriptions: Disposable[] = [];
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
subscriptions.push(RemoteProviderFactory.onDidChange(this._onRemoteProviderChanged, this));
this._disposable = Disposable.from(...subscriptions); this._disposable = Disposable.from(...subscriptions);
} }
@@ -125,9 +128,6 @@ export class GitService extends Disposable {
this._repoWatcher && this._repoWatcher.dispose(); this._repoWatcher && this._repoWatcher.dispose();
this._repoWatcher = undefined; this._repoWatcher = undefined;
this._stashWatcher && this._stashWatcher.dispose();
this._stashWatcher = undefined;
this._gitCache.clear(); this._gitCache.clear();
this._remotesCache.clear(); this._remotesCache.clear();
this._uriCache.clear(); this._uriCache.clear();
@@ -147,8 +147,7 @@ export class GitService extends Disposable {
if (cfg.advanced.caching.enabled) { if (cfg.advanced.caching.enabled) {
this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable && this._cacheDisposable.dispose();
this._repoWatcher = this._repoWatcher || workspace.createFileSystemWatcher('**/.git/index', true, false, true); this._repoWatcher = this._repoWatcher || workspace.createFileSystemWatcher('**/.git/{index,HEAD,refs/stash,refs/heads/**,refs/remotes/**}');
this._stashWatcher = this._stashWatcher || workspace.createFileSystemWatcher('**/.git/refs/stash', true, false, true);
const disposables: Disposable[] = []; const disposables: Disposable[] = [];
@@ -156,7 +155,6 @@ export class GitService extends Disposable {
disposables.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this)); disposables.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this));
disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved))); disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved)));
disposables.push(this._repoWatcher.onDidChange(this._onRepoChanged, this)); disposables.push(this._repoWatcher.onDidChange(this._onRepoChanged, this));
disposables.push(this._stashWatcher.onDidChange(this._onStashChanged, this));
this._cacheDisposable = Disposable.from(...disposables); this._cacheDisposable = Disposable.from(...disposables);
} }
@@ -167,11 +165,7 @@ export class GitService extends Disposable {
this._repoWatcher && this._repoWatcher.dispose(); this._repoWatcher && this._repoWatcher.dispose();
this._repoWatcher = undefined; this._repoWatcher = undefined;
this._stashWatcher && this._stashWatcher.dispose();
this._stashWatcher = 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) => {
@@ -197,7 +191,19 @@ export class GitService extends Disposable {
}); });
} }
const ignoreWhitespace = this.config && this.config.blame.ignoreWhitespace;
this.config = cfg; this.config = cfg;
if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) {
this._gitCache.clear();
this._fireGitCacheChange();
}
}
private _onRemoteProviderChanged() {
this._remotesCache.clear();
this._fireRepoChange(RepoChangedReasons.Remotes);
} }
private _onTextDocumentChanged(e: TextDocumentChangeEvent) { private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
@@ -216,17 +222,19 @@ export class GitService extends Disposable {
}, 1); }, 1);
} }
private _onRepoChanged() { private _onRepoChanged(uri: Uri) {
if (uri !== undefined && uri.path.endsWith('ref/stash')) {
this._fireRepoChange('stash');
return;
}
this._gitCache.clear(); this._gitCache.clear();
this._fireRepoChange(); this._fireRepoChange();
this._fireGitCacheChange(); this._fireGitCacheChange();
} }
private _onStashChanged() {
this._fireRepoChange('stash');
}
private _fireGitCacheChangeDebounced: (() => void) | undefined = undefined; private _fireGitCacheChangeDebounced: (() => void) | undefined = undefined;
private _fireGitCacheChange() { private _fireGitCacheChange() {
@@ -425,7 +433,7 @@ export class GitService extends Disposable {
} }
try { try {
const data = await Git.blame(root, file, uri.sha); const data = await Git.blame(root, file, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
const blame = GitBlameParser.parse(data, root, file); const blame = GitBlameParser.parse(data, root, file);
return blame; return blame;
} }
@@ -474,7 +482,7 @@ export class GitService extends Disposable {
const fileName = uri.fsPath; const fileName = uri.fsPath;
try { try {
const data = await Git.blame(uri.repoPath, fileName, uri.sha, line + 1, line + 1); const data = await Git.blame(uri.repoPath, fileName, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: line + 1, endLine: line + 1 });
const blame = GitBlameParser.parse(data, uri.repoPath, fileName); const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
if (blame === undefined) return undefined; if (blame === undefined) return undefined;
@@ -558,7 +566,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) {
@@ -878,7 +886,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) {
@@ -889,21 +897,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 = data.split('\n').filter(_ => !!_).map(_ => new GitRemote(_)); 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;
} }
@@ -961,6 +974,8 @@ export class GitService extends Disposable {
Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${sha})`); Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${sha})`);
const file = await Git.getVersionedFile(repoPath, fileName, sha); const file = await Git.getVersionedFile(repoPath, fileName, sha);
if (file === undefined) return undefined;
const cacheKey = this.getCacheEntryKey(file); const cacheKey = this.getCacheEntryKey(file);
const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath: repoPath!, fileName })); const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath: repoPath!, fileName }));
this._uriCache.set(cacheKey, entry); this._uriCache.set(cacheKey, entry);
@@ -1068,11 +1083,17 @@ export class GitService extends Disposable {
return Git.normalizePath(fileName, repoPath); return Git.normalizePath(fileName, repoPath);
} }
static toGitContentUri(sha: string, shortSha: string, fileName: string, repoPath: string, originalFileName?: string): Uri; static shortenSha(sha: string | undefined) {
if (sha === undefined) return undefined;
return Git.shortenSha(sha);
}
static toGitContentUri(sha: string, fileName: string, repoPath: string, originalFileName?: string): Uri;
static toGitContentUri(commit: GitCommit): Uri; static toGitContentUri(commit: GitCommit): Uri;
static toGitContentUri(uri: GitUri): Uri; static toGitContentUri(uri: GitUri): Uri;
static toGitContentUri(shaOrcommitOrUri: string | GitCommit | GitUri, shortSha?: string, fileName?: string, repoPath?: string, originalFileName?: string): Uri { static toGitContentUri(shaOrcommitOrUri: string | GitCommit | GitUri, fileName?: string, repoPath?: string, originalFileName?: string): Uri {
let data: IGitUriData; let data: IGitUriData;
let shortSha: string | undefined;
if (typeof shaOrcommitOrUri === 'string') { if (typeof shaOrcommitOrUri === 'string') {
data = GitService._toGitUriData({ data = GitService._toGitUriData({
sha: shaOrcommitOrUri, sha: shaOrcommitOrUri,
@@ -1080,6 +1101,7 @@ export class GitService extends Disposable {
repoPath: repoPath!, repoPath: repoPath!,
originalFileName: originalFileName originalFileName: originalFileName
}); });
shortSha = GitService.shortenSha(shaOrcommitOrUri);
} }
else if (shaOrcommitOrUri instanceof GitCommit) { else if (shaOrcommitOrUri instanceof GitCommit) {
data = GitService._toGitUriData(shaOrcommitOrUri, undefined, shaOrcommitOrUri.originalFileName); data = GitService._toGitUriData(shaOrcommitOrUri, undefined, shaOrcommitOrUri.originalFileName);
@@ -1119,7 +1141,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' |
@@ -11,7 +10,8 @@ export type SuppressedKeys = 'suppressCommitHasNoPreviousCommitWarning' |
'suppressGitVersionWarning' | 'suppressGitVersionWarning' |
'suppressLineUncommittedWarning' | 'suppressLineUncommittedWarning' |
'suppressNoRepositoryWarning' | 'suppressNoRepositoryWarning' |
'suppressUpdateNotice'; 'suppressUpdateNotice' |
'suppressWelcomeNotice';
export const SuppressedKeys = { export const SuppressedKeys = {
CommitHasNoPreviousCommitWarning: 'suppressCommitHasNoPreviousCommitWarning' as SuppressedKeys, CommitHasNoPreviousCommitWarning: 'suppressCommitHasNoPreviousCommitWarning' as SuppressedKeys,
CommitNotFoundWarning: 'suppressCommitNotFoundWarning' as SuppressedKeys, CommitNotFoundWarning: 'suppressCommitNotFoundWarning' as SuppressedKeys,
@@ -19,7 +19,8 @@ export const SuppressedKeys = {
GitVersionWarning: 'suppressGitVersionWarning' as SuppressedKeys, GitVersionWarning: 'suppressGitVersionWarning' as SuppressedKeys,
LineUncommittedWarning: 'suppressLineUncommittedWarning' as SuppressedKeys, LineUncommittedWarning: 'suppressLineUncommittedWarning' as SuppressedKeys,
NoRepositoryWarning: 'suppressNoRepositoryWarning' as SuppressedKeys, NoRepositoryWarning: 'suppressNoRepositoryWarning' as SuppressedKeys,
UpdateNotice: 'suppressUpdateNotice' as SuppressedKeys UpdateNotice: 'suppressUpdateNotice' as SuppressedKeys,
WelcomeNotice: 'suppressWelcomeNotice' as SuppressedKeys
}; };
export class Messages { export class Messages {
@@ -30,8 +31,9 @@ export class Messages {
this.context = context; this.context = context;
} }
static showCommitHasNoPreviousCommitWarningMessage(commit: GitCommit): Promise<string | undefined> { static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise<string | undefined> {
return Messages._showMessage('info', `Commit ${commit.shortSha} (${commit.author}, ${moment(commit.date).fromNow()}) 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}, ${commit.fromNow()}) has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
} }
static showCommitNotFoundWarningMessage(message: string): Promise<string | undefined> { static showCommitNotFoundWarningMessage(message: string): Promise<string | undefined> {
@@ -65,7 +67,7 @@ export class Messages {
static async showWelcomeMessage(): Promise<string | undefined> { static async showWelcomeMessage(): Promise<string | undefined> {
const viewDocs = 'View Docs'; const viewDocs = 'View Docs';
const result = await window.showInformationMessage(`Thank you for choosing GitLens! GitLens is powerful, feature rich, and highly configurable, so please be sure to view the docs and tailor it to suit your needs.`, viewDocs); const result = await Messages._showMessage('info', `Thank you for choosing GitLens! GitLens is powerful, feature rich, and highly configurable, so please be sure to view the docs and tailor it to suit your needs.`, SuppressedKeys.WelcomeNotice, null, viewDocs);
if (result === viewDocs) { if (result === viewDocs) {
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens')); commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens'));
} }
@@ -73,7 +75,7 @@ export class Messages {
} }
private static async _showMessage(type: 'info' | 'warn' | 'error', message: string, suppressionKey: SuppressedKeys, dontShowAgain: string | null = 'Don\'t Show Again', ...actions: any[]): Promise<string | undefined> { private static async _showMessage(type: 'info' | 'warn' | 'error', message: string, suppressionKey: SuppressedKeys, dontShowAgain: string | null = 'Don\'t Show Again', ...actions: any[]): Promise<string | undefined> {
Logger.log(`ShowMessage(${type}, "${message}", ${suppressionKey}, ${dontShowAgain})`); Logger.log(`ShowMessage(${type}, '${message}', ${suppressionKey}, ${dontShowAgain})`);
if (Messages.context.globalState.get(suppressionKey, false)) { if (Messages.context.globalState.get(suppressionKey, false)) {
Logger.log(`ShowMessage(${type}, ${message}, ${suppressionKey}, ${dontShowAgain}) skipped`); Logger.log(`ShowMessage(${type}, ${message}, ${suppressionKey}, ${dontShowAgain}) skipped`);
@@ -100,12 +102,13 @@ export class Messages {
} }
if (dontShowAgain === null || result === dontShowAgain) { if (dontShowAgain === null || result === dontShowAgain) {
Logger.log(`ShowMessage(${type}, ${message}, ${suppressionKey}, ${dontShowAgain}) don't show again requested`); Logger.log(`ShowMessage(${type}, '${message}', ${suppressionKey}, ${dontShowAgain}) don't show again requested`);
await Messages.context.globalState.update(suppressionKey, true); await Messages.context.globalState.update(suppressionKey, true);
return undefined;
if (result === dontShowAgain) return undefined;
} }
Logger.log(`ShowMessage(${type}, ${message}, ${suppressionKey}, ${dontShowAgain}) returned ${result}`); Logger.log(`ShowMessage(${type}, '${message}', ${suppressionKey}, ${dontShowAgain}) returned ${result}`);
return result; return result;
} }
} }

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Arrays, Iterables, Strings } from '../system'; import { Iterables, Strings } from '../system';
import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, ShowCommitSearchCommandArgs, ShowQuickBranchHistoryCommandArgs } from '../commands'; import { Commands, ShowCommitSearchCommandArgs, ShowQuickBranchHistoryCommandArgs } from '../commands';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common';
@@ -35,7 +35,7 @@ export class BranchHistoryQuickPick {
} as ShowQuickBranchHistoryCommandArgs } as ShowQuickBranchHistoryCommandArgs
]); ]);
const remotes = Arrays.uniqueBy(await git.getRemotes((uri && uri.repoPath) || git.repoPath), _ => _.url, _ => !!_.provider); const remotes = (await git.getRemotes((uri && uri.repoPath) || git.repoPath)).filter(r => r.provider !== undefined);
if (remotes.length) { if (remotes.length) {
items.splice(0, 0, new OpenRemotesCommandQuickPickItem(remotes, { items.splice(0, 0, new OpenRemotesCommandQuickPickItem(remotes, {
type: 'branch', type: 'branch',

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Arrays, Iterables, Strings } from '../system'; import { Iterables, Strings } from '../system';
import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode'; import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode';
import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandCommandArgs, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandCommandArgs, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, QuickPickItem } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, QuickPickItem } from './common';
@@ -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 {
@@ -24,17 +23,14 @@ export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickI
const description = GitStatusFile.getFormattedDirectory(status, true); const description = GitStatusFile.getFormattedDirectory(status, true);
let sha; let sha;
let shortSha;
if (status.status === 'D') { if (status.status === 'D') {
sha = commit.previousSha!; sha = commit.previousSha!;
shortSha = commit.previousShortSha!;
} }
else { else {
sha = commit.sha; sha = commit.sha;
shortSha = commit.shortSha;
} }
super(GitService.toGitContentUri(sha, shortSha, status.fileName, commit.repoPath, status.originalFileName), { super(GitService.toGitContentUri(sha, status.fileName, commit.repoPath, status.originalFileName), {
label: `${Strings.pad(octicon, 4, 2)} ${path.basename(status.fileName)}`, label: `${Strings.pad(octicon, 4, 2)} ${path.basename(status.fileName)}`,
description: description description: description
}); });
@@ -48,7 +44,7 @@ export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickI
originalFileName: status.originalFileName originalFileName: status.originalFileName
} as IGitCommitInfo); } as IGitCommitInfo);
this.sha = sha; this.sha = sha;
this.shortSha = shortSha; this.shortSha = GitService.shortenSha(sha)!;
this.status = status.status; this.status = status.status;
} }
@@ -88,7 +84,7 @@ export class OpenCommitFileRevisionsCommandQuickPickItem extends OpenFilesComman
constructor(commit: GitLogCommit, item?: QuickPickItem) { constructor(commit: GitLogCommit, item?: QuickPickItem) {
const uris = commit.fileStatuses const uris = commit.fileStatuses
.filter(s => s.status !== 'D') .filter(s => s.status !== 'D')
.map(s => GitService.toGitContentUri(commit.sha, commit.shortSha, s.fileName, commit.repoPath, s.originalFileName)); .map(s => GitService.toGitContentUri(commit.sha, s.fileName, commit.repoPath, s.originalFileName));
super(uris, item || { super(uris, item || {
label: `$(file-symlink-file) Open Changed Revisions`, label: `$(file-symlink-file) Open Changed Revisions`,
@@ -156,7 +152,7 @@ export class CommitDetailsQuickPick {
])); ]));
if (!stash) { if (!stash) {
const remotes = Arrays.uniqueBy(await git.getRemotes(commit.repoPath), _ => _.url, _ => !!_.provider); const remotes = (await git.getRemotes(commit.repoPath)).filter(r => r.provider !== undefined);
if (remotes.length) { if (remotes.length) {
items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, { items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, {
type: 'commit', type: 'commit',
@@ -302,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

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Arrays, Iterables, Strings } from '../system'; import { Iterables, Strings } from '../system';
import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem } from './common';
@@ -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 {
@@ -27,7 +26,7 @@ export class OpenCommitFileRevisionCommandQuickPickItem extends OpenFileCommandQ
let description: string; let description: string;
let uri: Uri; let uri: Uri;
if (commit.status === 'D') { if (commit.status === 'D') {
uri = GitService.toGitContentUri(commit.previousSha!, commit.previousShortSha!, commit.previousFileName!, commit.repoPath, undefined); uri = GitService.toGitContentUri(commit.previousSha!, commit.previousFileName!, commit.repoPath, undefined);
description = `${Strings.pad(GlyphChars.Dash, 2, 3)} ${path.basename(commit.fileName)} in ${GlyphChars.Space}$(git-commit) ${commit.previousShortSha} (deleted in ${GlyphChars.Space}$(git-commit) ${commit.shortSha})`; description = `${Strings.pad(GlyphChars.Dash, 2, 3)} ${path.basename(commit.fileName)} in ${GlyphChars.Space}$(git-commit) ${commit.previousShortSha} (deleted in ${GlyphChars.Space}$(git-commit) ${commit.shortSha})`;
} }
else { else {
@@ -125,7 +124,7 @@ export class CommitFileDetailsQuickPick {
} }
items.push(new OpenCommitFileRevisionCommandQuickPickItem(commit)); items.push(new OpenCommitFileRevisionCommandQuickPickItem(commit));
const remotes = Arrays.uniqueBy(await git.getRemotes(commit.repoPath), _ => _.url, _ => !!_.provider); const remotes = (await git.getRemotes(commit.repoPath)).filter(r => r.provider !== undefined);
if (remotes.length) { if (remotes.length) {
if (commit.workingFileName && commit.status !== 'D') { if (commit.workingFileName && commit.status !== 'D') {
const branch = await git.getBranch(commit.repoPath || git.repoPath); const branch = await git.getBranch(commit.repoPath || git.repoPath);
@@ -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,5 @@
'use strict'; 'use strict';
import { Arrays, Iterables, Strings } from '../system'; import { Iterables, Strings } from '../system';
import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, ShowQuickCurrentBranchHistoryCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { Commands, ShowQuickCurrentBranchHistoryCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common';
@@ -136,7 +136,7 @@ export class FileHistoryQuickPick {
])); ]));
} }
const remotes = Arrays.uniqueBy(await git.getRemotes(uri.repoPath!), _ => _.url, _ => !!_.provider); const remotes = (await git.getRemotes(uri.repoPath!)).filter(r => r.provider !== undefined);
if (remotes.length) { if (remotes.length) {
items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, { items.splice(index++, 0, new OpenRemotesCommandQuickPickItem(remotes, {
type: 'revision', type: 'revision',

View File

@@ -4,7 +4,7 @@ import { QuickPickOptions, window } from 'vscode';
import { Commands, OpenInRemoteCommandArgs } from '../commands'; import { Commands, OpenInRemoteCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './common';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { getNameFromRemoteResource, GitLogCommit, GitRemote, RemoteResource } from '../gitService'; import { getNameFromRemoteResource, GitLogCommit, GitRemote, GitService, RemoteResource } from '../gitService';
import * as path from 'path'; import * as path from 'path';
export class OpenRemoteCommandQuickPickItem extends CommandQuickPickItem { export class OpenRemoteCommandQuickPickItem extends CommandQuickPickItem {
@@ -43,7 +43,7 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem {
break; break;
case 'commit': case 'commit':
const shortSha = resource.sha.substring(0, 8); const shortSha = GitService.shortenSha(resource.sha);
description = `$(git-commit) ${shortSha}`; description = `$(git-commit) ${shortSha}`;
break; break;
@@ -67,7 +67,7 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem {
} }
} }
else { else {
const shortFileSha = resource.sha === undefined ? '' : resource.sha.substring(0, 8); const shortFileSha = resource.sha === undefined ? '' : GitService.shortenSha(resource.sha);
description = `$(file-text) ${path.basename(resource.fileName)}${shortFileSha ? ` in ${GlyphChars.Space}$(git-commit) ${shortFileSha}` : ''}`; description = `$(file-text) ${path.basename(resource.fileName)}${shortFileSha ? ` in ${GlyphChars.Space}$(git-commit) ${shortFileSha}` : ''}`;
} }
break; break;

View File

@@ -4,7 +4,7 @@ import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from
import { Commands, DiffWithWorkingCommandArgs, OpenChangedFilesCommandArgs, ShowQuickBranchHistoryCommandArgs, ShowQuickRepoStatusCommandArgs, ShowQuickStashListCommandArgs } from '../commands'; import { Commands, DiffWithWorkingCommandArgs, OpenChangedFilesCommandArgs, ShowQuickBranchHistoryCommandArgs, ShowQuickRepoStatusCommandArgs, ShowQuickStashListCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, OpenFileCommandQuickPickItem, QuickPickItem } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, OpenFileCommandQuickPickItem, QuickPickItem } from './common';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { GitStatus, GitStatusFile, GitUri } from '../gitService'; import { GitService, GitStatus, GitStatusFile, GitUri } from '../gitService';
import { Keyboard, Keys } from '../keyboard'; import { Keyboard, Keys } from '../keyboard';
import * as path from 'path'; import * as path from 'path';
@@ -182,7 +182,7 @@ export class RepoStatusQuickPick {
if (status.upstream && status.state.behind) { if (status.upstream && status.state.behind) {
items.splice(0, 0, new CommandQuickPickItem({ items.splice(0, 0, new CommandQuickPickItem({
label: `$(cloud-download)${GlyphChars.Space} ${status.state.behind} Commit${status.state.behind > 1 ? 's' : ''} behind ${GlyphChars.Space}$(git-branch) ${status.upstream}`, label: `$(cloud-download)${GlyphChars.Space} ${status.state.behind} Commit${status.state.behind > 1 ? 's' : ''} behind ${GlyphChars.Space}$(git-branch) ${status.upstream}`,
description: `${Strings.pad(GlyphChars.Dash, 2, 3)} shows commits in ${GlyphChars.Space}$(git-branch) ${status.upstream} but not ${GlyphChars.Space}$(git-branch) ${status.branch}${status.sha ? ` (since ${GlyphChars.Space}$(git-commit) ${status.sha.substring(0, 8)})` : ''}` description: `${Strings.pad(GlyphChars.Dash, 2, 3)} shows commits in ${GlyphChars.Space}$(git-branch) ${status.upstream} but not ${GlyphChars.Space}$(git-branch) ${status.branch}${status.sha ? ` (since ${GlyphChars.Space}$(git-commit) ${GitService.shortenSha(status.sha)})` : ''}`
}, Commands.ShowQuickBranchHistory, [ }, Commands.ShowQuickBranchHistory, [
new GitUri(Uri.file(status.repoPath), { fileName: '', repoPath: status.repoPath, sha: `${status.branch}..${status.upstream}` }), new GitUri(Uri.file(status.repoPath), { fileName: '', repoPath: status.repoPath, sha: `${status.branch}..${status.upstream}` }),
{ {

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,6 +1,15 @@
'use strict'; 'use strict';
export namespace Arrays { export namespace Arrays {
export function groupBy<T>(array: T[], accessor: (item: T) => any): T[] {
return array.reduce((previous, current) => {
const value = accessor(current);
previous[value] = previous[value] || [];
previous[value].push(current);
return previous;
}, Object.create(null));
}
export function uniqueBy<T>(array: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] { export function uniqueBy<T>(array: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] {
const uniqueValues = Object.create(null); const uniqueValues = Object.create(null);
return array.filter(_ => { return array.filter(_ => {

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

@@ -101,12 +101,20 @@ export namespace Strings {
return s; return s;
} }
export function truncate(s: string, truncateTo?: number) { // Removes \ / : * ? " < > | and C0 and C1 control codes
if (!s || truncateTo === undefined) return s; const illegalCharsForFSRegEx = /[\\/:*?"<>|\x00-\x1f\x80-\x9f]/g;
export function sanitizeForFS(s: string, replacement: string = '_') {
if (!s) return s;
return s.replace(illegalCharsForFSRegEx, replacement);
}
export function truncate(s: string, truncateTo: number, ellipsis: string = '\u2026') {
if (!s) return s;
const len = getWidth(s); const len = getWidth(s);
if (len <= truncateTo) return s; if (len <= truncateTo) return s;
if (len === s.length) return `${s.substring(0, truncateTo - 1)}\u2026`; if (len === s.length) return `${s.substring(0, truncateTo - 1)}${ellipsis}`;
// Skip ahead to start as far as we can by assuming all the double-width characters won't be truncated // Skip ahead to start as far as we can by assuming all the double-width characters won't be truncated
let chars = Math.floor(truncateTo / (len / s.length)); let chars = Math.floor(truncateTo / (len / s.length));
@@ -119,6 +127,6 @@ export namespace Strings {
chars--; chars--;
} }
return `${s.substring(0, chars)}\u2026`; return `${s.substring(0, chars)}${ellipsis}`;
} }
} }

View File

@@ -3,22 +3,27 @@ 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 } from './explorerNode'; import { ExplorerNode, ResourceType, ShowAllCommitsNode } from './explorerNode';
import { GitBranch, GitService, GitUri } from '../gitService'; import { GitBranch, GitService, GitUri } from '../gitService';
export class BranchHistoryNode extends ExplorerNode { export class BranchHistoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:branch-history'; readonly resourceType: ResourceType = 'gitlens:branch-history';
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, private readonly template: string, 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.getLogForRepo(this.uri.repoPath!, this.branch.name); const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount);
if (log === undefined) return []; if (log === undefined) return [];
return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git))]; const children = Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git, this.branch));
if (!log.truncated) return [...children];
return [...children, new ShowAllCommitsNode(this, this.context)];
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
@@ -27,7 +32,7 @@ export class BranchHistoryNode extends ExplorerNode {
name += ` ${GlyphChars.Space}${GlyphChars.ArrowLeftRight}${GlyphChars.Space} ${this.branch.tracking}`; name += ` ${GlyphChars.Space}${GlyphChars.ArrowLeftRight}${GlyphChars.Space} ${this.branch.tracking}`;
} }
const item = new TreeItem(`${this.branch!.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`, TreeItemCollapsibleState.Collapsed); const item = new TreeItem(`${this.branch!.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType; item.contextValue = this.branch.tracking ? `${this.resourceType}:remote` : this.resourceType;
item.iconPath = { item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'), dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'),

View File

@@ -21,9 +21,13 @@ export class BranchesNode extends ExplorerNode {
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.git.config.gitExplorer.commitFormat, this.context, this.git))];
} }
getTreeItem(): TreeItem { async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Expanded); const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Expanded);
item.contextValue = this.resourceType;
const remotes = await this.git.getRemotes(this.uri.repoPath!);
item.contextValue = (remotes !== undefined && remotes.length > 0)
? `${this.resourceType}:remote`
: this.resourceType;
item.iconPath = { item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'), dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'),

View File

@@ -2,14 +2,14 @@
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, GitCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService'; import { getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService';
import * as path from 'path'; import * as path from 'path';
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) { constructor(public readonly status: IGitStatusFile, public commit: GitCommit, protected readonly context: ExtensionContext, protected readonly git: GitService, 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 }));
} }
@@ -40,16 +40,6 @@ export class CommitFileNode extends ExplorerNode {
} }
getCommand(): Command | undefined { getCommand(): Command | undefined {
let allowMissingPrevious = false;
let prefix = undefined;
if (this.status.status === 'A') {
allowMissingPrevious = true;
prefix = 'added in ';
}
else if (this.status.status === 'D') {
prefix = 'deleted in ';
}
return { return {
title: 'Compare File with Previous Revision', title: 'Compare File with Previous Revision',
command: Commands.DiffWithPrevious, command: Commands.DiffWithPrevious,
@@ -61,9 +51,7 @@ export class CommitFileNode extends ExplorerNode {
showOptions: { showOptions: {
preserveFocus: true, preserveFocus: true,
preview: true preview: true
}, }
allowMissingPrevious: allowMissingPrevious,
rightTitlePrefix: prefix
} as DiffWithPreviousCommandArgs } as DiffWithPreviousCommandArgs
] ]
}; };

View File

@@ -4,14 +4,14 @@ import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'v
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode } from './commitFileNode'; import { CommitFileNode } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, getGitStatusIcon, GitLogCommit, GitService, GitUri } from '../gitService'; import { CommitFormatter, getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as path from 'path'; 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) { constructor(public readonly commit: GitLogCommit, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService, public readonly branch?: GitBranch) {
super(new GitUri(commit.uri, commit)); super(new GitUri(commit.uri, commit));
} }
@@ -24,11 +24,15 @@ export class CommitNode extends ExplorerNode {
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))]; return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, this.branch))];
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {
const item = new TreeItem(CommitFormatter.fromTemplate(this.template, this.commit, this.git.config.defaultDateFormat)); const item = new TreeItem(CommitFormatter.fromTemplate(this.template, this.commit, {
truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions));
if (this.commit.type === 'file') { if (this.commit.type === 'file') {
item.collapsibleState = TreeItemCollapsibleState.None; item.collapsibleState = TreeItemCollapsibleState.None;
item.command = this.getCommand(); item.command = this.getCommand();
@@ -55,17 +59,6 @@ export class CommitNode extends ExplorerNode {
} }
getCommand(): Command | undefined { getCommand(): Command | undefined {
let allowMissingPrevious = false;
let prefix = undefined;
const status = this.commit.fileStatuses[0];
if (status.status === 'A') {
allowMissingPrevious = true;
prefix = 'added in ';
}
else if (status.status === 'D') {
prefix = 'deleted in ';
}
return { return {
title: 'Compare File with Previous Revision', title: 'Compare File with Previous Revision',
command: Commands.DiffWithPrevious, command: Commands.DiffWithPrevious,
@@ -77,9 +70,7 @@ export class CommitNode extends ExplorerNode {
showOptions: { showOptions: {
preserveFocus: true, preserveFocus: true,
preview: true preview: true
}, }
allowMissingPrevious: allowMissingPrevious,
rightTitlePrefix: prefix
} as DiffWithPreviousCommandArgs } as DiffWithPreviousCommandArgs
] ]
}; };

View File

@@ -1,6 +1,8 @@
'use strict'; 'use strict';
import { Command, Event, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GlyphChars } from '../constants';
import { GitUri } from '../gitService'; import { GitUri } from '../gitService';
import { RefreshNodeCommandArgs } from './gitExplorer';
export declare type ResourceType = export declare type ResourceType =
'gitlens:branches' | 'gitlens:branches' |
@@ -10,6 +12,7 @@ export declare type ResourceType =
'gitlens:file-history' | 'gitlens:file-history' |
'gitlens:history' | 'gitlens:history' |
'gitlens:message' | 'gitlens:message' |
'gitlens:pager' |
'gitlens:remote' | 'gitlens:remote' |
'gitlens:remotes' | 'gitlens:remotes' |
'gitlens:repository' | 'gitlens:repository' |
@@ -31,10 +34,6 @@ export abstract class ExplorerNode {
getCommand(): Command | undefined { getCommand(): Command | undefined {
return undefined; return undefined;
} }
onDidChangeTreeData?: Event<ExplorerNode>;
refresh?(): void;
} }
export class MessageNode extends ExplorerNode { export class MessageNode extends ExplorerNode {
@@ -54,4 +53,46 @@ export class MessageNode extends ExplorerNode {
item.contextValue = this.resourceType; item.contextValue = this.resourceType;
return item; return item;
} }
}
export class PagerNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:pager';
args: RefreshNodeCommandArgs = {};
constructor(private message: string, private node: ExplorerNode, protected readonly context: ExtensionContext) {
super(new GitUri());
}
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> {
return [];
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this.message, TreeItemCollapsibleState.None);
item.contextValue = this.resourceType;
item.command = this.getCommand();
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-unfold.svg'),
light: this.context.asAbsolutePath('images/light/icon-unfold.svg')
};
return item;
}
getCommand(): Command | undefined {
return {
title: 'Refresh',
command: 'gitlens.gitExplorer.refreshNode',
arguments: [this.node, this.args]
} as Command;
}
}
export class ShowAllCommitsNode extends PagerNode {
args: RefreshNodeCommandArgs = { maxCount: 0 };
constructor(node: ExplorerNode, context: ExtensionContext) {
super(`Show All Commits ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context);
}
} }

View File

@@ -1,11 +1,11 @@
'use strict'; 'use strict';
import { Functions, Objects } from '../system'; import { Functions, Objects } from '../system';
import { commands, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode'; import { commands, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands'; import { Commands, DiffWithCommandArgs, DiffWithCommandArgsRevision, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands';
import { UriComparer } from '../comparers'; import { UriComparer } from '../comparers';
import { ExtensionKey, IConfig } from '../configuration'; import { ExtensionKey, IConfig } from '../configuration';
import { CommandContext, setCommandContext } from '../constants'; import { CommandContext, setCommandContext } from '../constants';
import { CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoryNode, StashNode } from './explorerNodes'; import { BranchHistoryNode, CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoryNode, StashNode } from './explorerNodes';
import { GitService, GitUri, RepoChangedReasons } from '../gitService'; import { GitService, GitUri, RepoChangedReasons } from '../gitService';
export * from './explorerNodes'; export * from './explorerNodes';
@@ -23,6 +23,10 @@ export interface OpenFileRevisionCommandArgs {
showOptions?: TextDocumentShowOptions; showOptions?: TextDocumentShowOptions;
} }
export interface RefreshNodeCommandArgs {
maxCount?: number;
}
export class GitExplorer implements TreeDataProvider<ExplorerNode> { export class GitExplorer implements TreeDataProvider<ExplorerNode> {
private _config: IConfig; private _config: IConfig;
@@ -38,19 +42,23 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
commands.registerCommand('gitlens.gitExplorer.switchToHistoryView', () => this.switchTo(GitExplorerView.History), this); commands.registerCommand('gitlens.gitExplorer.switchToHistoryView', () => this.switchTo(GitExplorerView.History), this);
commands.registerCommand('gitlens.gitExplorer.switchToRepositoryView', () => this.switchTo(GitExplorerView.Repository), this); commands.registerCommand('gitlens.gitExplorer.switchToRepositoryView', () => this.switchTo(GitExplorerView.Repository), this);
commands.registerCommand('gitlens.gitExplorer.refresh', this.refresh, this); commands.registerCommand('gitlens.gitExplorer.refresh', this.refresh, this);
commands.registerCommand('gitlens.gitExplorer.refreshNode', this.refreshNode, this);
commands.registerCommand('gitlens.gitExplorer.openChanges', this.openChanges, this); commands.registerCommand('gitlens.gitExplorer.openChanges', this.openChanges, this);
commands.registerCommand('gitlens.gitExplorer.openChangesWithWorking', this.openChangesWithWorking, this); commands.registerCommand('gitlens.gitExplorer.openChangesWithWorking', this.openChangesWithWorking, this);
commands.registerCommand('gitlens.gitExplorer.openFile', this.openFile, this); commands.registerCommand('gitlens.gitExplorer.openFile', this.openFile, this);
commands.registerCommand('gitlens.gitExplorer.openFileRevision', this.openFileRevision, this); commands.registerCommand('gitlens.gitExplorer.openFileRevision', this.openFileRevision, this);
commands.registerCommand('gitlens.gitExplorer.openFileRevisionInRemote', this.openFileRevisionInRemote, this); commands.registerCommand('gitlens.gitExplorer.openFileRevisionInRemote', this.openFileRevisionInRemote, this);
commands.registerCommand('gitlens.gitExplorer.openChangedFiles', this.openChangedFiles, this); commands.registerCommand('gitlens.gitExplorer.openChangedFiles', this.openChangedFiles, this);
commands.registerCommand('gitlens.gitExplorer.openChangedFileChanges', this.openChangedFileChanges, this);
commands.registerCommand('gitlens.gitExplorer.openChangedFileChangesWithWorking', this.openChangedFileChangesWithWorking, this);
commands.registerCommand('gitlens.gitExplorer.openChangedFileRevisions', this.openChangedFileRevisions, this); commands.registerCommand('gitlens.gitExplorer.openChangedFileRevisions', this.openChangedFileRevisions, this);
commands.registerCommand('gitlens.gitExplorer.applyChanges', this.applyChanges, this); commands.registerCommand('gitlens.gitExplorer.applyChanges', this.applyChanges, this);
context.subscriptions.push(this.git.onDidChangeRepo(this.onRepoChanged, this)); const repoChangedFn = Functions.debounce(this.onRepoChanged, 250);
context.subscriptions.push(this.git.onDidChangeRepo(repoChangedFn, this));
const fn = Functions.debounce(this.onActiveEditorChanged, 500); const editorChangedFn = Functions.debounce(this.onActiveEditorChanged, 500);
context.subscriptions.push(window.onDidChangeActiveTextEditor(fn, this)); context.subscriptions.push(window.onDidChangeActiveTextEditor(editorChangedFn, this));
context.subscriptions.push(workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)); context.subscriptions.push(workspace.onDidChangeConfiguration(this.onConfigurationChanged, this));
this.onConfigurationChanged(); this.onConfigurationChanged();
@@ -132,6 +140,14 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
this._onDidChangeTreeData.fire(node); this._onDidChangeTreeData.fire(node);
} }
refreshNode(node: ExplorerNode, args: RefreshNodeCommandArgs) {
if (node instanceof BranchHistoryNode) {
node.maxCount = args.maxCount;
}
this.refresh(node);
}
switchTo(view: GitExplorerView) { switchTo(view: GitExplorerView) {
if (this._view === view) return; if (this._view === view) return;
@@ -176,6 +192,29 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
return openEditor(options.uri || GitService.toGitContentUri(node.uri), options.showOptions || { preserveFocus: true, preview: false }); return openEditor(options.uri || GitService.toGitContentUri(node.uri), options.showOptions || { preserveFocus: true, preview: false });
} }
private async openChangedFileChanges(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) {
const repoPath = node.commit.repoPath;
const uris = node.commit.fileStatuses
.map(s => GitUri.fromFileStatus(s, repoPath));
for (const uri of uris) {
await this.openDiffWith(repoPath,
{ uri: uri, sha: node.commit.previousSha !== undefined ? node.commit.previousSha : GitService.fakeSha },
{ uri: uri, sha: node.commit.sha }, options);
}
}
private async openChangedFileChangesWithWorking(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) {
const repoPath = node.commit.repoPath;
const uris = node.commit.fileStatuses
.filter(s => s.status !== 'D')
.map(s => GitUri.fromFileStatus(s, repoPath));
for (const uri of uris) {
await this.openDiffWith(repoPath,
{ uri: uri, sha: node.commit.sha },
{ uri: uri, sha: '' }, options);
}
}
private async openChangedFiles(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { private async openChangedFiles(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) {
const repoPath = node.commit.repoPath; const repoPath = node.commit.repoPath;
const uris = node.commit.fileStatuses.filter(s => s.status !== 'D').map(s => GitUri.fromFileStatus(s, repoPath)); const uris = node.commit.fileStatuses.filter(s => s.status !== 'D').map(s => GitUri.fromFileStatus(s, repoPath));
@@ -187,12 +226,22 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
private async openChangedFileRevisions(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) { private async openChangedFileRevisions(node: CommitNode | StashNode, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) {
const uris = node.commit.fileStatuses const uris = node.commit.fileStatuses
.filter(s => s.status !== 'D') .filter(s => s.status !== 'D')
.map(s => GitService.toGitContentUri(node.commit.sha, node.commit.shortSha, s.fileName, node.commit.repoPath, s.originalFileName)); .map(s => GitService.toGitContentUri(node.commit.sha, s.fileName, node.commit.repoPath, s.originalFileName));
for (const uri of uris) { for (const uri of uris) {
await openEditor(uri, options); await openEditor(uri, options);
} }
} }
private async openDiffWith(repoPath: string, lhs: DiffWithCommandArgsRevision, rhs: DiffWithCommandArgsRevision, options: TextDocumentShowOptions = { preserveFocus: false, preview: false }) {
const diffArgs: DiffWithCommandArgs = {
repoPath: repoPath,
lhs: lhs,
rhs: rhs,
showOptions: options
};
return commands.executeCommand(Commands.DiffWith, diffArgs);
}
private async openFileRevisionInRemote(node: CommitNode | StashNode) { private async openFileRevisionInRemote(node: CommitNode | StashNode) {
return commands.executeCommand(Commands.OpenFileInRemote, new GitUri(node.commit.uri, node.commit), { range: false } as OpenFileInRemoteCommandArgs); return commands.executeCommand(Commands.OpenFileInRemote, new GitUri(node.commit.uri, node.commit), { range: false } as OpenFileInRemoteCommandArgs);
} }

View File

@@ -2,6 +2,7 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchHistoryNode } from './branchHistoryNode'; import { BranchHistoryNode } from './branchHistoryNode';
import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { GitRemote, GitService, GitUri } from '../gitService'; import { GitRemote, GitService, GitUri } from '../gitService';
@@ -22,7 +23,26 @@ export class RemoteNode extends ExplorerNode {
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {
const item = new TreeItem(this.remote.name, TreeItemCollapsibleState.Collapsed); const fetch = this.remote.types.includes('push');
const push = this.remote.types.includes('push');
let separator;
if (fetch && push) {
separator = GlyphChars.ArrowLeftRight;
}
else if (fetch) {
separator = GlyphChars.ArrowLeft;
}
else if (push) {
separator = GlyphChars.ArrowRight;
}
else {
separator = GlyphChars.Dash;
}
const label = `${this.remote.name} ${GlyphChars.Space}${separator}${GlyphChars.Space} ${(this.remote.provider !== undefined) ? this.remote.provider.name : this.remote.domain} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${this.remote.path}`;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType; item.contextValue = this.resourceType;
// item.iconPath = { // item.iconPath = {

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Arrays, Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
@@ -14,7 +14,7 @@ export class RemotesNode extends ExplorerNode {
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
const remotes = Arrays.uniqueBy(await this.git.getRemotes(this.uri.repoPath!), r => r.url, r => !!r.provider); const remotes = await this.git.getRemotes(this.uri.repoPath!);
if (remotes === undefined || remotes.length === 0) return [new MessageNode('No remotes configured')]; if (remotes === undefined || remotes.length === 0) return [new MessageNode('No remotes configured')];
remotes.sort((a, b) => a.name.localeCompare(b.name)); remotes.sort((a, b) => a.name.localeCompare(b.name));

View File

@@ -1,35 +1,41 @@
'use strict'; 'use strict';
import { Event, EventEmitter, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, GitService, GitStashCommit, GitUri } from '../gitService'; import { CommitFormatter, GitService, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService';
import { StashFileNode } from './stashFileNode'; import { StashFileNode } from './stashFileNode';
export class StashNode extends ExplorerNode { export class StashNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:stash'; readonly resourceType: ResourceType = 'gitlens:stash';
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>();
public get onDidChangeTreeData(): Event<ExplorerNode> {
return this._onDidChangeTreeData.event;
}
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 Promise.resolve(statuses.map(s => new StashFileNode(s, this.commit, this.context, this.git)));
} }
getTreeItem(): TreeItem { getTreeItem(): TreeItem {
const label = CommitFormatter.fromTemplate(this.git.config.gitExplorer.stashFormat, this.commit, this.git.config.defaultDateFormat); const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.stashFormat, this.commit, {
truncateMessageAtNewLine: true,
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType; item.contextValue = this.resourceType;
return item; return item;
} }
refresh() {
this._onDidChangeTreeData.fire();
}
} }

View File

@@ -17,7 +17,7 @@ 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); 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.template, this.context, this.git))];