67 Commits

Author SHA1 Message Date
Eric Amodio
c5f57172e4 Fixes issue where work tree files could no show up 2017-09-23 23:36:43 -04:00
Eric Amodio
dd0a636e24 Preps v5.2.0 2017-09-23 23:29:49 -04:00
Eric Amodio
393ec351f0 Adds gitlens.gitExplorer.includeWorkingTree setting
Adds auto-update for working trree
Fixes issues with working tree status
2017-09-23 16:29:56 -04:00
Eric Amodio
99d6da9c90 Fixes issue with whitespace collapsing in decorations 2017-09-22 01:01:43 -04:00
Eric Amodio
0eb202b8ae Closes #150 - adds auto view to custom view 2017-09-22 00:48:57 -04:00
Eric Amodio
65736a6ce7 Adds watching of creates/deletes inside .git 2017-09-21 18:43:46 -04:00
Eric Amodio
ed42eba8b8 Fixes issue with untracked files being counted properly 2017-09-21 18:43:12 -04:00
Eric Amodio
2245d82319 Preps v5.2.0-beta 2017-09-20 01:41:32 -04:00
Eric Amodio
f7df845dfe Adds working tree status to custom view (insiders)
Hides working changed files behind insiders flag
Unhides Changed Files node from behind insiders flag
Adds changed file count to Changed Files node label
Adds icon to Changed Files node
Adds upstream branch to upstream status nodes
Sorts files in the Changed Files node
2017-09-20 01:37:18 -04:00
Eric Amodio
712544fab8 Adds git diff --shortstat support 2017-09-20 01:10:47 -04:00
Eric Amodio
a114e2de87 Changes the file sort in the custom view 2017-09-20 01:09:19 -04:00
Eric Amodio
70071448d6 Closes #146 - Attempts to deal with emoji in gutter
Adds ability to automagically set the width of the gutter annotations
2017-09-20 01:01:21 -04:00
Eric Amodio
a10376385a Preps v5.1.1-beta 2017-09-17 22:41:47 -04:00
Eric Amodio
41d25803d8 Updates slack links 2017-09-17 14:38:59 -04:00
Eric Amodio
3802b43027 Adds merged PR to changelog
Adds new contributor to readme
2017-09-17 11:28:42 -04:00
Amanda Cameron
04ea3b7971 Apply Review Comments. 2017-09-17 11:20:21 -04:00
Amanda Cameron
6d7f44e091 Fix GitLab integration's multi-line selection. 2017-09-17 11:20:21 -04:00
Eric Amodio
3a1caa2e0d Disables a set of context menu items by default 2017-09-17 02:44:10 -04:00
Eric Amodio
3f7058bd48 Fixes wrong setting used to control menu commands 2017-09-17 02:43:46 -04:00
Eric Amodio
71d17bcc2f Closes #139 - adds changed files node to repository status
Reworks commit-file nodes
2017-09-17 02:34:09 -04:00
Eric Amodio
a69afdb6ef Closes #144 - support disabling the custom view 2017-09-17 01:43:41 -04:00
Eric Amodio
26c6346b84 Changes default commit format in custom view 2017-09-16 12:06:18 -04:00
Eric Amodio
3a17605017 Preps v5.1.0 2017-09-15 21:03:52 -04:00
Eric Amodio
2c9a26e47b Fixes untracked files not showing in stash list 2017-09-15 18:12:22 -04:00
Eric Amodio
1c7785fd52 Adds note for closed issue in changelog 2017-09-15 17:39:39 -04:00
Eric Amodio
079f7b7f36 Switches to use a unicode arrow for the external link icon 2017-09-15 17:39:39 -04:00
Eric Amodio
bedc1a05f5 Preps v5.1.0-beta 2017-09-15 17:39:39 -04:00
Eric Amodio
858d9ec578 Fixes issue with stashes w/ only untracked files 2017-09-15 17:39:38 -04:00
Eric Amodio
2809991096 Closes #116 - adds full commit msg to annotations
Switches to use HoverProvider for hovers in file blames
2017-09-15 17:38:37 -04:00
Eric Amodio
f6019454b6 Adds open in remote to hover annotations
Optimizes annotation computation (cache by commit)
2017-09-14 23:43:41 -04:00
Eric Amodio
f0bdf3e2c3 Always caches remotes 2017-09-14 22:46:40 -04:00
Eric Amodio
0fdf856c27 Adds performance logging 2017-09-14 22:45:23 -04:00
Eric Amodio
aacf7cc2b5 Reworks date parsing, formatting etc for perf
Isolates moment.js
2017-09-14 21:52:51 -04:00
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
106 changed files with 2373 additions and 1008 deletions

View File

@@ -4,13 +4,53 @@ 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 ## [Unreleased]
## [5.2.0] - 2017-09-23
### Added
- Adds new `Changed Files` node to the `Repository Status` node of the `GitLens` custom view's `Repository View` -- closes [#139](https://github.com/eamodio/vscode-gitlens/issues/139)
- Provides a at-a-glance view of all "working" changes
- Expands to a file-based view of all changed files in the working tree (enabled via `"gitlens.insiders": true`) and/or all files in all commits ahead of the upstream
- Adds optional (on by default) working tree status information to the `Repository Status` node in the `GitLens` custom view
- Adds `auto` value to `gitlens.gitExplorer.view` setting - closes [#150](https://github.com/eamodio/vscode-gitlens/issues/150)
- Adds `gitlens.gitExplorer.enabled` setting to specify whether or not to show the `GitLens` custom view - closes [#144](https://github.com/eamodio/vscode-gitlens/issues/144)
- Adds `gitlens.gitExplorer.includeWorkingTree` setting to specify whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view
- Adds `gitlens.gitExplorer.statusFileFormat` setting to the format of the status of a working or committed file in the `GitLens` custom view
### Changed
- Changes the sorting (now alphabetical) of files shown in the `GitLens` custom view
- Changes the default of the `gitlens.gitExplorer.view` setting to `auto`
- Changes the default of the `gitlens.gitExplorer.commitFormat` setting to add parentheses around the commit id
- Removes many menu items from `editor/title` & `editor/title/context` by default -- can be re-enabled via the `gitlens.advanced.menus` setting
### Fixed
- Fixes [#146](https://github.com/eamodio/vscode-gitlens/issues/146) - Blame gutter annotation issue when commit contains emoji
- Fixes an issue when running `Open File in Remote` with a multi-line selection wasn't properly opening the selection in GitLab -- thanks to [PR #145](https://github.com/eamodio/vscode-gitlens/pull/145) by Amanda Cameron ([@AmandaCameron](https://github.com/AmandaCameron))!
- Fixes an issue where the `gitlens.advanced.menus` setting wasn't controlling all the menu items properly
## [5.1.0] - 2017-09-15
### Added
- Adds full (multi-line) commit message to the `details` hover annotations -- closes [#116](https://github.com/eamodio/vscode-gitlens/issues/116)
- Adds an external link icon to the `details` hover annotations to run the `Open Commit in Remote` command (`gitlens.openCommitInRemote`)
### Changed
- Optimizes performance of the providing blame annotations, especially for large files (saw a ~78% improvement on some files)
- Optimizes date handling (parsing and formatting) for better performance and reduced memory consumption
### Removed
- Removes `gitlens.annotations.file.recentChanges.hover.wholeLine` setting as it didn't really make sense
### Fixed
- Fixes an issue where stashes with only untracked files would not show in the `Stashes` node of the GitLens custom view
- Fixes an issue where stashes with untracked files would not show its untracked files in the GitLens custom view
## [5.0.0] - 2017-09-12
### 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 +66,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 +98,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 +140,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 +205,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 +213,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 +267,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 +351,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))!
@@ -589,7 +645,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

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-dev-community.slack.com/](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/chat-badge.png)](https://join.slack.com/t/vscode-dev-community/shared_invite/enQtMjIxOTgxNDE3NzM0LWU5M2ZiZDU1YjBlMzdlZjA2YjBjYzRhYTM5NTgzMTAxMjdiNWU0ZmQzYWI3MWU5N2Q1YjBiYmQ4MzY0NDE1MzY)
# GitLens # GitLens
@@ -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,16 +123,20 @@ 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, [optionally](#gitlens-custom-view-settings) its working tree status, and its upstream tracking branch and status (if available)
- Provides indicator dots on the repository icon which denote the following: - Provides indicator dots on the repository icon which denote the following:
- `None` - up-to-date with the upstream - `None` - up-to-date with the upstream
- `Green` - ahead of the upstream - `Green` - ahead of the upstream
- `Red` - behind the upstream - `Red` - behind the upstream
- `Yellow` - both ahead of and behind the upstream - `Yellow` - both ahead of and behind the upstream
- Provides additional nodes, if the current branch is not synchronized with the upstream, to quickly see and explore the specific commits ahead and/or behind the upstream - Provides additional upstream status nodes, if the current branch is tracking a remote branch and
- is behind the upstream — quickly see and explore the specific commits behind the upstream (i.e. commits that haven't been pulled)
- is ahead of the upstream — quickly see and explore the specific commits ahead of the upstream (i.e. commits that haven't been pushed)
- `Changed Files` node — provides a at-a-glance view of all "working" changes
- Expands to a file-based view of all changed files in the working tree ([optionally](#gitlens-custom-view-settings)) and/or all files in all commits ahead of the upstream
- Provides a context menu with `Open Repository in Remote`, and `Refresh` commands - Provides a context menu with `Open Repository in Remote`, and `Refresh` commands
- `Branches` node — provides a list of the local branches - `Branches` node — provides a list of the local branches
@@ -132,28 +144,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 +179,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 +188,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 +199,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 +207,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 +217,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 +225,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 +237,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 +292,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 +336,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
@@ -336,12 +354,21 @@ GitLens is highly customizable and provides many configuration settings to allow
|Name | Description |Name | Description
|-----|------------ |-----|------------
|`gitlens.gitExplorer.view`|Specifies the starting view (mode) of the `GitLens` custom view<br />`history` - shows the commit history of the active file<br />`repository` - shows a repository explorer" |`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view"
|`gitlens.gitExplorer.view`|Specifies the starting view (mode) of the `GitLens` custom view<br /> `auto` - shows the last selected view, defaults to `repository`<br />`history` - shows the commit history of the active file<br />`repository` - shows a repository explorer"
|`gitlens.gitExplorer.includeWorkingTree`|Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view
|`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view" |`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view"
|`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` custom view<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${authorAgo} - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` custom view<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${authorAgo} - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting
|`gitlens.gitExplorer.commitFileFormat`|Specifies the format of a committed 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.commitFileFormat`|Specifies the format of a committed 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.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
|`gitlens.gitExplorer.statusFileFormat`|Specifies the format of the status of a working or committed file in the `GitLens` custom view<br />Available tokens<br /> ${file} - file name<br /> ${filePath} - file name and path<br /> ${path} - file path<br />${working} - optional indicator if the file is uncommitted
### 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
@@ -402,6 +429,7 @@ GitLens is highly customizable and provides many configuration settings to allow
A big thanks to the people that have contributed to this project: A big thanks to the people that have contributed to this project:
- Amanda Cameron ([@AmandaCameron](https://github.com/AmandaCameron)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=AmandaCameron))
- Peng Lyu ([@rebornix](https://github.com/rebornix)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=rebornix)) - Peng Lyu ([@rebornix](https://github.com/rebornix)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=rebornix))
- Aurelio Ogliari ([@nobitagit](https://github.com/nobitagit)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=nobitagit) - Aurelio Ogliari ([@nobitagit](https://github.com/nobitagit)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=nobitagit)
- Johannes Rieken ([@jrieken](https://github.com/jrieken)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=jrieken)) - Johannes Rieken ([@jrieken](https://github.com/jrieken)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=jrieken))

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="m7.5,10l2,0l0,1l-2,0l0,2l-1,0l0,-2l-2,0l0,-1l2,0l0,-2l1,0l0,2l0,0zm-3,6l5,0l0,-1l-5,0l0,1l0,0zm4.5,-11l3.5,3.5l0,9.5c0,0.55 -0.45,1 -1,1l-9,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l6.5,0l0,0zm2.5,4l-3,-3l-6,0l0,12l9,0l0,-9l0,0zm-1.5,-6l-5.5,0l0,1l5,0l4,4l0,8l1,0l0,-8.5l-4.5,-4.5l0,0z" />
</svg>

After

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

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="m7.5,10l2,0l0,1l-2,0l0,2l-1,0l0,-2l-2,0l0,-1l2,0l0,-2l1,0l0,2l0,0zm-3,6l5,0l0,-1l-5,0l0,1l0,0zm4.5,-11l3.5,3.5l0,9.5c0,0.55 -0.45,1 -1,1l-9,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l6.5,0l0,0zm2.5,4l-3,-3l-6,0l0,12l9,0l0,-9l0,0zm-1.5,-6l-5.5,0l0,1l5,0l4,4l0,8l1,0l0,-8.5l-4.5,-4.5l0,0z" />
</svg>

After

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

74
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.0.0-alpha.2", "version": "5.2.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": {
@@ -47,11 +47,6 @@
"json-stable-stringify": "1.0.1" "json-stable-stringify": "1.0.1"
} }
}, },
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"ansi-styles": { "ansi-styles": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
@@ -348,7 +343,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 +1243,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 +1267,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",
@@ -1330,11 +1331,6 @@
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true "dev": true
}, },
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"is-glob": { "is-glob": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
@@ -1788,9 +1784,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 +1796,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 +2231,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"
@@ -2336,29 +2333,12 @@
"safe-buffer": "5.1.1" "safe-buffer": "5.1.1"
} }
}, },
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "2.0.0",
"strip-ansi": "4.0.0"
}
},
"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",
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
"dev": true "dev": true
}, },
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "3.0.0"
}
},
"strip-bom": { "strip-bom": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
@@ -2707,10 +2687,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.2.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",
@@ -15,8 +15,8 @@
"badges": [ "badges": [
{ {
"url": "https://img.shields.io/badge/chat-on%20slack-brightgreen.svg", "url": "https://img.shields.io/badge/chat-on%20slack-brightgreen.svg",
"href": "https://join.slack.com/t/vscode-gitlens/shared_invite/MjIxOTgxNDE3NzM0LTE1MDE2Nzk1MTgtMjkwMmZjMzcxNQ", "href": "https://join.slack.com/t/vscode-dev-community/shared_invite/enQtMjIxOTgxNDE3NzM0LWU5M2ZiZDU1YjBlMzdlZjA2YjBjYzRhYTM5NTgzMTAxMjdiNWU0ZmQzYWI3MWU5N2Q1YjBiYmQ4MzY0NDE1MzY",
"description": "Chat at https://vscode-gitlens.slack.com/" "description": "Chat at https://vscode-dev-community.slack.com/"
} }
], ],
"categories": [ "categories": [
@@ -132,11 +132,6 @@
"default": true, "default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation" "description": "Specifies whether or not to provide a changes (diff) hover annotation"
}, },
"gitlens.annotations.file.recentChanges.hover.wholeLine": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to trigger hover annotations over the whole line"
},
"gitlens.annotations.line.hover.details": { "gitlens.annotations.line.hover.details": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
@@ -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",
@@ -415,7 +415,7 @@
}, },
"gitlens.gitExplorer.commitFormat": { "gitlens.gitExplorer.commitFormat": {
"type": "string", "type": "string",
"default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0\u2022\u00a0 ${id}", "default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0 (${id})",
"description": "Specifies the format of committed changes in the `GitLens` custom view\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting" "description": "Specifies the format of committed changes in the `GitLens` custom view\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting"
}, },
"gitlens.gitExplorer.commitFileFormat": { "gitlens.gitExplorer.commitFileFormat": {
@@ -423,6 +423,16 @@
"default": "${filePath}", "default": "${filePath}",
"description": "Specifies the format of a committed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path" "description": "Specifies the format of a committed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path"
}, },
"gitlens.gitExplorer.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to show the `GitLens` custom view"
},
"gitlens.gitExplorer.includeWorkingTree": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view"
},
"gitlens.gitExplorer.showTrackingBranch": { "gitlens.gitExplorer.showTrackingBranch": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
@@ -438,14 +448,49 @@
"default": "${filePath}", "default": "${filePath}",
"description": "Specifies the format of a stashed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path" "description": "Specifies the format of a stashed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path"
}, },
"gitlens.gitExplorer.statusFileFormat": {
"type": "string",
"default": "${working}${filePath}",
"description": "Specifies the format of the status of a working or committed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path\n ${working} - optional indicator if the file is uncommitted"
},
"gitlens.gitExplorer.view": { "gitlens.gitExplorer.view": {
"type": "string", "type": "string",
"default": "repository", "default": "auto",
"enum": [ "enum": [
"auto",
"history", "history",
"repository" "repository"
], ],
"description": "Specifies the starting view (mode) of the `GitLens` custom view\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer" "description": "Specifies the starting view (mode) of the `GitLens` custom view\n `auto` - shows the last selected view, defaults to `repository`\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",
@@ -617,16 +662,16 @@
}, },
"editorTitle": { "editorTitle": {
"blame": true, "blame": true,
"fileDiff": true, "fileDiff": false,
"history": true, "history": false,
"remote": true, "remote": false,
"status": true "status": false
}, },
"editorTitleContext": { "editorTitleContext": {
"blame": true, "blame": false,
"fileDiff": true, "fileDiff": false,
"history": true, "history": false,
"remote": true "remote": false
}, },
"explorerContext": { "explorerContext": {
"fileDiff": true, "fileDiff": true,
@@ -784,6 +829,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 +1110,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 +1137,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 +1325,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"
@@ -1341,12 +1413,12 @@
}, },
{ {
"command": "gitlens.openFileInRemote", "command": "gitlens.openFileInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitleContext.remote", "when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitle.remote",
"group": "1_gitlens" "group": "1_gitlens"
}, },
{ {
"command": "gitlens.openRepoInRemote", "command": "gitlens.openRepoInRemote",
"when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitleContext.remote", "when": "gitlens:enabled && gitlens:hasRemotes && config.gitlens.advanced.menus.editorTitle.remote",
"group": "1_gitlens" "group": "1_gitlens"
}, },
{ {
@@ -1466,204 +1538,269 @@
"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.openChanges",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "1_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openChangesWithWorking",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "1_gitlens@2"
},
{
"command": "gitlens.gitExplorer.openFile",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "2_gitlens@1"
},
{
"command": "gitlens.gitExplorer.openFileRevision",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "2_gitlens@2"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "3_gitlens@1"
},
{
"command": "gitlens.showQuickFileHistory",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file && gitlens:gitExplorer:view == repository",
"group": "5_gitlens@1"
},
{
"command": "gitlens.showQuickCommitFileDetails",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file",
"group": "5_gitlens@2"
},
{
"command": "gitlens.gitExplorer.openFile",
"when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file-commits",
"group": "1_gitlens@1"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status-file-commits",
"group": "1_gitlens@2"
},
{ {
"command": "gitlens.gitExplorer.refresh", "command": "gitlens.gitExplorer.refresh",
"when": "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 && viewItem != gitlens:status-file",
"group": "9_gitlens@1" "group": "9_gitlens@1"
} }
] ]
@@ -1765,7 +1902,7 @@
{ {
"id": "gitlens.gitExplorer", "id": "gitlens.gitExplorer",
"name": "GitLens", "name": "GitLens",
"when": "gitlens:enabled" "when": "gitlens:enabled && config.gitlens.gitExplorer.enabled"
} }
] ]
} }
@@ -1787,7 +1924,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",
@@ -1795,16 +1932,15 @@
"lodash.once": "4.1.1", "lodash.once": "4.1.1",
"moment": "2.18.1", "moment": "2.18.1",
"spawn-rx": "2.0.11", "spawn-rx": "2.0.11",
"string-width": "2.1.1",
"tmp": "0.0.33" "tmp": "0.0.33"
}, },
"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, Objects, 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,22 +101,22 @@ 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;
} }
static gutter(commit: GitCommit, format: string, dateFormatOrFormatOptions: string | null | ICommitFormatOptions, renderOptions: IRenderOptions): DecorationOptions { static gutter(commit: GitCommit, format: string, dateFormatOrFormatOptions: string | null | ICommitFormatOptions, renderOptions: IRenderOptions): DecorationOptions {
const content = Strings.pad(CommitFormatter.fromTemplate(format, commit, dateFormatOrFormatOptions), 1, 1); const message = CommitFormatter.fromTemplate(format, commit, dateFormatOrFormatOptions);
return { return {
renderOptions: { renderOptions: {
before: { before: {
...renderOptions.before, ...renderOptions.before,
...{ ...{
contentText: content contentText: Strings.pad(message.replace(/ /g, GlyphChars.Space), 1, 1)
} }
}, },
dark: { dark: {
@@ -124,9 +133,23 @@ export class Annotations {
} as DecorationOptions; } as DecorationOptions;
} }
static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions { static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig, options: ICommitFormatOptions): IRenderOptions {
const cfgFileTheme = cfgTheme.annotations.file.gutter; const cfgFileTheme = cfgTheme.annotations.file.gutter;
// Try to get the width of the string, if there is a cap
let width = 4; // Start with a padding
for (const token of Objects.values<Strings.ITokenOptions | undefined>(options.tokenOptions)) {
if (token === undefined) continue;
// If any token is uncapped, kick out and set no max
if (token.truncateTo == null) {
width = 0;
break;
}
width += token.truncateTo;
}
let borderStyle = undefined; let borderStyle = undefined;
let borderWidth = undefined; let borderWidth = undefined;
if (heatmap.enabled) { if (heatmap.enabled) {
@@ -143,7 +166,8 @@ export class Annotations {
borderStyle: borderStyle, borderStyle: borderStyle,
borderWidth: borderWidth, borderWidth: borderWidth,
height: '100%', height: '100%',
margin: '0 26px -1px 0' margin: '0 26px -1px 0',
width: (width > 4) ? `${width}ch` : undefined
}, },
dark: { dark: {
backgroundColor: cfgFileTheme.dark.backgroundColor || undefined, backgroundColor: cfgFileTheme.dark.backgroundColor || undefined,
@@ -158,11 +182,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,11 +206,15 @@ 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: {
contentText: Strings.pad(message, 1, 1) contentText: Strings.pad(message.replace(/ /g, GlyphChars.Space), 1, 1)
}, },
dark: { dark: {
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, options);
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.width(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);
if (blame === undefined) return false;
if (cfg.heatmap.enabled) {
const start = process.hrtime();
const now = Date.now();
const offset = this.uri.offset; const offset = this.uri.offset;
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap); const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
const dateFormat = this._config.defaultDateFormat;
const decorations: DecorationOptions[] = []; const decorations: DecorationOptions[] = [];
const document = this.document; const decorationsMap: { [sha: string]: DecorationOptions } = Object.create(null);
let commit: GitBlameCommit | undefined; let commit: GitBlameCommit | undefined;
let hover: DecorationOptions | undefined; let hover: DecorationOptions | undefined;
for (const l of blame.lines) { 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); commit = blame.commits.get(l.sha);
if (commit === undefined) continue; if (commit === undefined) continue;
const line = l.line + offset; hover = Annotations.hover(commit, renderOptions, now);
hover.range = new Range(line, 0, line, 0);
hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled, dateFormat);
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) {
Annotations.applyHeatmap(hover, commit.date, now);
}
decorations.push(hover); decorations.push(hover);
decorationsMap[l.sha] = hover;
} }
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 hover blame annotations`);
}
this.registerHoverProvider();
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;
if (args.branch === undefined) {
const branch = await this.git.getBranch(gitUri.repoPath); 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

@@ -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: {
@@ -298,15 +317,20 @@ export interface IConfig {
defaultDateFormat: string | null; defaultDateFormat: string | null;
gitExplorer: { gitExplorer: {
enabled: boolean;
view: GitExplorerView; view: GitExplorerView;
includeWorkingTree: boolean;
showTrackingBranch: boolean; showTrackingBranch: boolean;
commitFormat: string; commitFormat: string;
commitFileFormat: string; commitFileFormat: string;
stashFormat: string; stashFormat: string;
stashFileFormat: string; stashFileFormat: string;
statusFileFormat: string;
// 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,12 +77,16 @@ export type GlyphChars = '\u21a9' |
'\u2937' | '\u2937' |
'\u2190' | '\u2190' |
'\u2194' | '\u2194' |
'\u2192' |
'\u21e8' | '\u21e8' |
'\u2191' | '\u2191' |
'\u2197' |
'\u2217' |
'\u2713' | '\u2713' |
'\u2014' | '\u2014' |
'\u2022' | '\u2022' |
'\u2026' | '\u2026' |
'\u270E' |
'\u00a0' | '\u00a0' |
'\u200b'; '\u200b';
export const GlyphChars = { export const GlyphChars = {
@@ -93,17 +95,26 @@ 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,
Asterisk: '\u2217' 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,
Ellipsis: '\u2026' as GlyphChars, Ellipsis: '\u2026' as GlyphChars,
Pensil: '\u270E' as GlyphChars,
Space: '\u00a0' as GlyphChars, Space: '\u00a0' as GlyphChars,
ZeroWidthSpace: '\u200b' as GlyphChars ZeroWidthSpace: '\u200b' as GlyphChars
}; };
export type WorkspaceState = 'gitlensVersion'; export type GlobalState = 'gitlensVersion';
export const WorkspaceState = { export const GlobalState = {
GitLensVersion: 'gitlensVersion' as WorkspaceState GitLensVersion: 'gitlensVersion' as GlobalState
};
export type WorkspaceState = 'gitlens:gitExplorer:view';
export const WorkspaceState = {
GitExplorerView: 'gitlens:gitExplorer:view' as WorkspaceState
}; };

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 {
if (showDetailsStartIndex === 0) {
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
} }
else if (showStartIndex !== 0) {
showAtStart = true;
} }
} }
@@ -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 {
if (showDetailsStartIndex === 0) {
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
} }
else if (showStartIndex !== 0) {
showAtStart = true;
} }
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';
@@ -16,9 +16,10 @@ import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './command
import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands'; import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands';
import { ToggleCodeLensCommand } from './commands'; import { ToggleCodeLensCommand } from './commands';
import { CodeLensLocations, IConfig, LineHighlightLocations } from './configuration'; import { CodeLensLocations, IConfig, LineHighlightLocations } from './configuration';
import { ApplicationInsightsKey, CommandContext, ExtensionKey, QualifiedExtensionId, setCommandContext, WorkspaceState } from './constants'; import { ApplicationInsightsKey, CommandContext, ExtensionKey, GlobalState, QualifiedExtensionId, setCommandContext } 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;
@@ -69,7 +71,7 @@ export async function activate(context: ExtensionContext) {
notifyOnUnsupportedGitVersion(context, gitVersion); notifyOnUnsupportedGitVersion(context, gitVersion);
notifyOnNewGitLensVersion(context, gitlensVersion); notifyOnNewGitLensVersion(context, gitlensVersion);
await context.globalState.update(WorkspaceState.GitLensVersion, gitlensVersion); await context.globalState.update(GlobalState.GitLensVersion, gitlensVersion);
const git = new GitService(repoPath); const git = new GitService(repoPath);
context.subscriptions.push(git); context.subscriptions.push(git);
@@ -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));
@@ -145,7 +148,7 @@ export async function activate(context: ExtensionContext) {
export function deactivate() { } export function deactivate() { }
async function migrateSettings(context: ExtensionContext) { async function migrateSettings(context: ExtensionContext) {
const previousVersion = context.globalState.get<string>(WorkspaceState.GitLensVersion); const previousVersion = context.globalState.get<string>(GlobalState.GitLensVersion);
if (previousVersion === undefined) return; if (previousVersion === undefined) return;
const [major] = previousVersion.split('.'); const [major] = previousVersion.split('.');
@@ -271,7 +274,7 @@ async function migrateSettings(context: ExtensionContext) {
async function notifyOnNewGitLensVersion(context: ExtensionContext, version: string) { async function notifyOnNewGitLensVersion(context: ExtensionContext, version: string) {
if (context.globalState.get(SuppressedKeys.UpdateNotice, false)) return; if (context.globalState.get(SuppressedKeys.UpdateNotice, false)) return;
const previousVersion = context.globalState.get<string>(WorkspaceState.GitLensVersion); const previousVersion = context.globalState.get<string>(GlobalState.GitLensVersion);
if (previousVersion === undefined) { if (previousVersion === undefined) {
Logger.log(`GitLens first-time install`); Logger.log(`GitLens first-time install`);

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,21 +29,28 @@ export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions>
} }
get authorAgo() { get authorAgo() {
const authorAgo = `${this._item.author}, ${moment(this._item.date).fromNow()}`; const authorAgo = `${this._item.author}, ${this._item.fromNow()}`;
return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo); return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo);
} }
get date() { get date() {
const date = moment(this._item.date).format(this._options.dateFormat!); const date = this._item.formatDate(this._options.dateFormat!);
return this._padOrTruncate(date, this._options.tokenOptions!.date); return this._padOrTruncate(date, this._options.tokenOptions!.date);
} }
get id() { get id() {
return this._item.shortSha; return this._item.isUncommitted ? 'index' : this._item.shortSha;
} }
get message() { get message() {
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

@@ -51,7 +51,7 @@ export abstract class Formatter<TItem = any, TOptions extends IFormatOptions = I
let max = options.truncateTo; let max = options.truncateTo;
const width = Strings.getWidth(s); const width = Strings.width(s);
if (max === undefined) { if (max === undefined) {
if (this.collapsableWhitespace === 0) return s; if (this.collapsableWhitespace === 0) return s;

View File

@@ -1,7 +1,8 @@
'use strict'; 'use strict';
import { Strings } from '../../system'; import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
import { Formatter, IFormatOptions } from './formatter'; import { Formatter, IFormatOptions } from './formatter';
import { GitStatusFile, IGitStatusFile } from '../models/status'; import { GitStatusFile, IGitStatusFile, IGitStatusFileWithCommit } from '../models/status';
import * as path from 'path'; import * as path from 'path';
export interface IStatusFormatOptions extends IFormatOptions { export interface IStatusFormatOptions extends IFormatOptions {
@@ -29,6 +30,11 @@ export class StatusFileFormatter extends Formatter<IGitStatusFile, IStatusFormat
return this._padOrTruncate(directory, this._options.tokenOptions!.file); return this._padOrTruncate(directory, this._options.tokenOptions!.file);
} }
get working() {
const commit = (this._item as IGitStatusFileWithCommit).commit;
return (commit !== undefined && commit.isUncommitted) ? `${GlyphChars.Pensil} ${GlyphChars.Space}` : '';
}
static fromTemplate(template: string, status: IGitStatusFile, dateFormat: string | null): string; static fromTemplate(template: string, status: IGitStatusFile, dateFormat: string | null): string;
static fromTemplate(template: string, status: IGitStatusFile, options?: IStatusFormatOptions): string; static fromTemplate(template: string, status: IGitStatusFile, options?: IStatusFormatOptions): string;
static fromTemplate(template: string, status: IGitStatusFile, dateFormatOrOptions?: string | null | IStatusFormatOptions): string; static fromTemplate(template: string, status: IGitStatusFile, dateFormatOrOptions?: string | null | IStatusFormatOptions): string;

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,8 +40,24 @@ 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 {
return await gitCommandCore(options, ...args);
}
catch (ex) {
return gitCommandDefaultErrorHandler(ex, options, ...args);
}
}
async function gitCommandCore(options: GitCommandOptions, ...args: any[]): Promise<string> {
// Fixes https://github.com/eamodio/vscode-gitlens/issues/73 // Fixes https://github.com/eamodio/vscode-gitlens/issues/73
// See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x // 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'); args.splice(0, 0, '-c', 'core.quotepath=false');
@@ -51,12 +69,8 @@ async function gitCommand(options: { cwd: string, encoding?: string, onError?: (
return iconv.decode(Buffer.from(s, 'binary'), opts.encoding); return iconv.decode(Buffer.from(s, 'binary'), opts.encoding);
} }
catch (ex) {
if (options.onError !== undefined) {
const result = options.onError(ex);
if (result !== undefined) return result;
}
function gitCommandDefaultErrorHandler(ex: Error, options: GitCommandOptions, ...args: any[]): string {
const msg = ex && ex.toString(); const msg = ex && ex.toString();
if (msg) { if (msg) {
for (const warning of GitWarnings) { for (const warning of GitWarnings) {
@@ -70,7 +84,6 @@ async function gitCommand(options: { cwd: string, encoding?: string, onError?: (
Logger.error(ex, 'git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`); Logger.error(ex, 'git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
throw ex; 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 '';
} }
} }
@@ -235,6 +259,14 @@ export class Git {
return gitCommand({ cwd: repoPath }, ...params); return gitCommand({ cwd: repoPath }, ...params);
} }
static diff_shortstat(repoPath: string, sha?: string) {
const params = [`diff`, `--shortstat`, `--no-ext-diff`];
if (sha) {
params.push(sha);
}
return gitCommand({ cwd: repoPath }, ...params);
}
static difftool_dirDiff(repoPath: string, sha1: string, sha2?: string) { static difftool_dirDiff(repoPath: string, sha1: string, sha2?: string) {
const params = [`difftool`, `--dir-diff`, sha1]; const params = [`difftool`, `--dir-diff`, sha1];
if (sha2) { if (sha2) {
@@ -308,11 +340,19 @@ export class Git {
return gitCommand({ cwd: repoPath }, ...params, ...search); return gitCommand({ cwd: repoPath }, ...params, ...search);
} }
static log_shortstat(repoPath: string, sha?: string) {
const params = [`log`, `--shortstat`, `--oneline`];
if (sha) {
params.push(sha);
}
return gitCommand({ cwd: repoPath }, ...params);
}
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 +365,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

@@ -34,3 +34,9 @@ export interface GitDiff {
diff?: string; diff?: string;
} }
export interface GitDiffShortStat {
files: number;
insertions: number;
deletions: number;
}

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';
@@ -38,8 +39,13 @@ export class GitLogCommit extends GitCommit {
this.fileName = fileStatus.fileName; this.fileName = fileStatus.fileName;
this.status = fileStatus.status; this.status = fileStatus.status;
} }
else {
if (fileName === undefined) {
this.fileStatuses = [];
}
else { else {
this.fileStatuses = [{ status: status, fileName: fileName, originalFileName: originalFileName } as IGitStatusFile]; 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

@@ -3,6 +3,7 @@ import { Strings } from '../../system';
import { Uri } from 'vscode'; import { Uri } from 'vscode';
import { GlyphChars } from '../../constants'; import { GlyphChars } from '../../constants';
import { GitUri } from '../gitUri'; import { GitUri } from '../gitUri';
import { GitLogCommit } from './logCommit';
import * as path from 'path'; import * as path from 'path';
export interface GitStatus { export interface GitStatus {
@@ -19,7 +20,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;
@@ -27,6 +28,10 @@ export interface IGitStatusFile {
originalFileName?: string; originalFileName?: string;
} }
export interface IGitStatusFileWithCommit extends IGitStatusFile {
commit: GitLogCommit;
}
export class GitStatusFile implements IGitStatusFile { export class GitStatusFile implements IGitStatusFile {
originalFileName?: string; originalFileName?: string;
@@ -71,7 +76,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 +94,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

@@ -1,8 +1,9 @@
'use strict'; 'use strict';
import { Iterables, Strings } from '../../system'; import { Iterables, Strings } from '../../system';
import { GitDiff, GitDiffChunk, GitDiffChunkLine, GitDiffLine } from './../git'; import { GitDiff, GitDiffChunk, GitDiffChunkLine, GitDiffLine, GitDiffShortStat } from './../git';
const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm; const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm;
const shortStatDiffRegex = /^\s*(\d+)\sfiles? changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/;
export class GitDiffParser { export class GitDiffParser {
@@ -116,4 +117,20 @@ export class GitDiffParser {
return chunkLines; return chunkLines;
} }
static parseShortStat(data: string): GitDiffShortStat | undefined {
if (!data) return undefined;
const match = shortStatDiffRegex.exec(data);
if (match == null) return undefined;
const files = match[1];
const insertions = match[2];
const deletions = match[3];
return {
files: files == null ? 0 : parseInt(files, 10),
insertions: insertions == null ? 0 : parseInt(insertions, 10),
deletions: deletions == null ? 0 : parseInt(deletions, 10)
} as GitDiffShortStat;
}
} }

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

@@ -1,13 +1,42 @@
'use strict'; 'use strict';
import { GitHubService } from './github'; import { Range } from 'vscode';
import { RemoteProvider } from './provider';
export class GitLabService extends GitHubService { export class GitLabService extends RemoteProvider {
constructor(public domain: string, public path: string) { 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');
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}
protected getUrlForBranch(branch: string): string {
return `${this.baseUrl}/commits/${branch}`;
}
protected getUrlForCommit(sha: string): string {
return `${this.baseUrl}/commit/${sha}`;
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = `#L${range.start.line}`;
}
else {
line = `#L${range.start.line}-${range.end.line}`;
}
}
if (sha) return `${this.baseUrl}/blob/${sha}/${fileName}${line}`;
if (branch) return `${this.baseUrl}/blob/${branch}/${fileName}${line}`;
return `${this.baseUrl}?path=${fileName}${line}`;
} }
} }

View File

@@ -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, GitDiffShortStat, 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,18 @@ 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';
static uncommittedSha = '0000000000000000000000000000000000000000';
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;
@@ -82,6 +86,11 @@ export class GitService extends Disposable {
return this._onDidChangeGitCache.event; return this._onDidChangeGitCache.event;
} }
private _onDidChangeFileSystem = new EventEmitter<Uri>();
get onDidChangeFileSystem(): Event<Uri> {
return this._onDidChangeFileSystem.event;
}
private _onDidChangeRepo = new EventEmitter<RepoChangedReasons[]>(); private _onDidChangeRepo = new EventEmitter<RepoChangedReasons[]>();
get onDidChangeRepo(): Event<RepoChangedReasons[]> { get onDidChangeRepo(): Event<RepoChangedReasons[]> {
return this._onDidChangeRepo.event; return this._onDidChangeRepo.event;
@@ -96,7 +105,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,21 +120,21 @@ 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);
} }
dispose() { dispose() {
this._disposable && this._disposable.dispose(); this.stopWatchingFileSystem();
this._cacheDisposable && this._cacheDisposable.dispose();
this._cacheDisposable = undefined;
this._repoWatcher && this._repoWatcher.dispose(); this._repoWatcher && this._repoWatcher.dispose();
this._repoWatcher = undefined; this._repoWatcher = undefined;
this._stashWatcher && this._stashWatcher.dispose(); this._disposable && this._disposable.dispose();
this._stashWatcher = undefined;
this._cacheDisposable && this._cacheDisposable.dispose();
this._cacheDisposable = undefined;
this._gitCache.clear(); this._gitCache.clear();
this._remotesCache.clear(); this._remotesCache.clear();
@@ -147,8 +155,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 +163,8 @@ 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)); disposables.push(this._repoWatcher.onDidCreate(this._onRepoChanged, this));
disposables.push(this._repoWatcher.onDidDelete(this._onRepoChanged, this));
this._cacheDisposable = Disposable.from(...disposables); this._cacheDisposable = Disposable.from(...disposables);
} }
@@ -167,11 +175,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 +201,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 +232,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 +443,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 +492,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 +576,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) {
@@ -590,6 +608,11 @@ export class GitService extends Disposable {
return Git.normalizePath(typeof fileNameOrUri === 'string' ? fileNameOrUri : fileNameOrUri.fsPath).toLowerCase(); return Git.normalizePath(typeof fileNameOrUri === 'string' ? fileNameOrUri : fileNameOrUri.fsPath).toLowerCase();
} }
async getChangedFilesCount(repoPath: string, sha?: string): Promise<GitDiffShortStat | undefined> {
const data = await Git.diff_shortstat(repoPath, sha);
return GitDiffParser.parseShortStat(data);
}
async getConfig(key: string, repoPath?: string): Promise<string> { async getConfig(key: string, repoPath?: string): Promise<string> {
Logger.log(`getConfig('${key}', '${repoPath}')`); Logger.log(`getConfig('${key}', '${repoPath}')`);
@@ -878,7 +901,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 +912,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 +989,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);
@@ -1012,6 +1042,33 @@ export class GitService extends Disposable {
return Git.difftool_dirDiff(repoPath, sha1, sha2); return Git.difftool_dirDiff(repoPath, sha1, sha2);
} }
private _fsWatcherDisposable: Disposable | undefined;
startWatchingFileSystem() {
if (this._fsWatcherDisposable !== undefined) return;
const debouncedFn = Functions.debounce((uri: Uri) => this._onDidChangeFileSystem.fire(uri), 2500);
const fn = (uri: Uri) => {
// Ignore .git changes
if (/\.git/.test(uri.fsPath)) return;
debouncedFn(uri);
};
const watcher = workspace.createFileSystemWatcher(`**`);
this._fsWatcherDisposable = Disposable.from(
watcher,
watcher.onDidChange(fn),
watcher.onDidCreate(fn),
watcher.onDidDelete(fn)
);
}
stopWatchingFileSystem() {
this._fsWatcherDisposable && this._fsWatcherDisposable.dispose();
this._fsWatcherDisposable = undefined;
}
stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) { stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) {
Logger.log(`stashApply('${repoPath}', ${stashName}, ${deleteAfter})`); Logger.log(`stashApply('${repoPath}', ${stashName}, ${deleteAfter})`);
@@ -1068,11 +1125,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 +1143,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 +1183,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) => string): { [key: string]: 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

@@ -1,16 +1,11 @@
'use strict'; 'use strict';
const _escapeRegExp = require('lodash.escaperegexp'); const _escapeRegExp = require('lodash.escaperegexp');
const stringWidth = require('string-width');
export namespace Strings { export namespace Strings {
export function escapeRegExp(s: string): string { export function escapeRegExp(s: string): string {
return _escapeRegExp(s); return _escapeRegExp(s);
} }
export function getWidth(s: string): number {
return stringWidth(s);
}
const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g; const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g;
const TokenSanitizeRegex = /\$\{(\w*?)(?:\W|\d)*?\}/g; const TokenSanitizeRegex = /\$\{(\w*?)(?:\W|\d)*?\}/g;
@@ -68,19 +63,19 @@ export namespace Strings {
} }
export function padLeft(s: string, padTo: number, padding: string = '\u00a0') { export function padLeft(s: string, padTo: number, padding: string = '\u00a0') {
const diff = padTo - getWidth(s); const diff = padTo - width(s);
return (diff <= 0) ? s : '\u00a0'.repeat(diff) + s; return (diff <= 0) ? s : '\u00a0'.repeat(diff) + s;
} }
export function padLeftOrTruncate(s: string, max: number, padding?: string) { export function padLeftOrTruncate(s: string, max: number, padding?: string) {
const len = getWidth(s); const len = width(s);
if (len < max) return padLeft(s, max, padding); if (len < max) return padLeft(s, max, padding);
if (len > max) return truncate(s, max); if (len > max) return truncate(s, max);
return s; return s;
} }
export function padRight(s: string, padTo: number, padding: string = '\u00a0') { export function padRight(s: string, padTo: number, padding: string = '\u00a0') {
const diff = padTo - getWidth(s); const diff = padTo - width(s);
return (diff <= 0) ? s : s + '\u00a0'.repeat(diff); return (diff <= 0) ? s : s + '\u00a0'.repeat(diff);
} }
@@ -88,37 +83,148 @@ export namespace Strings {
const left = max < 0; const left = max < 0;
max = Math.abs(max); max = Math.abs(max);
const len = getWidth(s); const len = width(s);
if (len < max) return left ? padLeft(s, max, padding) : padRight(s, max, padding); if (len < max) return left ? padLeft(s, max, padding) : padRight(s, max, padding);
if (len > max) return truncate(s, max); if (len > max) return truncate(s, max);
return s; return s;
} }
export function padRightOrTruncate(s: string, max: number, padding?: string) { export function padRightOrTruncate(s: string, max: number, padding?: string) {
const len = getWidth(s); const len = width(s);
if (len < max) return padRight(s, max, padding); if (len < max) return padRight(s, max, padding);
if (len > max) return truncate(s, max); if (len > max) return truncate(s, max);
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;
const len = getWidth(s); 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 = width(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));
let count = getWidth(s.substring(0, chars)); let count = width(s.substring(0, chars));
while (count < truncateTo) { while (count < truncateTo) {
count += getWidth(s[chars++]); count += width(s[chars++]);
} }
if (count >= truncateTo) { if (count >= truncateTo) {
chars--; chars--;
} }
return `${s.substring(0, chars)}\u2026`; return `${s.substring(0, chars)}${ellipsis}`;
}
const ansiRegex = /[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))/g;
export function width(s: string): number {
if (!s || s.length === 0) return 0;
s = s.replace(ansiRegex, '');
let count = 0;
let emoji = 0;
let joiners = 0;
const graphemes = [...s];
for (let i = 0; i < graphemes.length; i++) {
const code = graphemes[i].codePointAt(0)!;
// Ignore control characters
if (code <= 0x1F || (code >= 0x7F && code <= 0x9F)) continue;
// Ignore combining characters
if (code >= 0x300 && code <= 0x36F) continue;
// https://stackoverflow.com/questions/30757193/find-out-if-character-in-string-is-emoji
if (
(code >= 0x1F600 && code <= 0x1F64F) || // Emoticons
(code >= 0x1F300 && code <= 0x1F5FF) || // Misc Symbols and Pictographs
(code >= 0x1F680 && code <= 0x1F6FF) || // Transport and Map
(code >= 0x2600 && code <= 0x26FF) || // Misc symbols
(code >= 0x2700 && code <= 0x27BF) || // Dingbats
(code >= 0xFE00 && code <= 0xFE0F) || // Variation Selectors
(code >= 0x1F900 && code <= 0x1F9FF) || // Supplemental Symbols and Pictographs
(code >= 65024 && code <= 65039) || // Variation selector
(code >= 8400 && code <= 8447) // Combining Diacritical Marks for Symbols
) {
if (code >= 0x1F3FB && code <= 0x1F3FF) continue; // emoji modifier fitzpatrick type
emoji++;
count += 2;
continue;
}
// Ignore zero-width joiners '\u200d'
if (code === 8205) {
joiners++;
count -= 2;
continue;
}
// Surrogates
if (code > 0xFFFF) {
i++;
}
count += isFullwidthCodePoint(code) ? 2 : 1;
}
const offset = emoji - joiners;
if (offset > 1) {
count += offset - 1;
}
return count;
}
function isFullwidthCodePoint(cp: number) {
// code points are derived from:
// http://www.unix.org/Public/UNIDATA/EastAsianWidth.txt
if (
cp >= 0x1100 && (
cp <= 0x115f || // Hangul Jamo
cp === 0x2329 || // LEFT-POINTING ANGLE BRACKET
cp === 0x232a || // RIGHT-POINTING ANGLE BRACKET
// CJK Radicals Supplement .. Enclosed CJK Letters and Months
(0x2e80 <= cp && cp <= 0x3247 && cp !== 0x303f) ||
// Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
(0x3250 <= cp && cp <= 0x4dbf) ||
// CJK Unified Ideographs .. Yi Radicals
(0x4e00 <= cp && cp <= 0xa4c6) ||
// Hangul Jamo Extended-A
(0xa960 <= cp && cp <= 0xa97c) ||
// Hangul Syllables
(0xac00 <= cp && cp <= 0xd7a3) ||
// CJK Compatibility Ideographs
(0xf900 <= cp && cp <= 0xfaff) ||
// Vertical Forms
(0xfe10 <= cp && cp <= 0xfe19) ||
// CJK Compatibility Forms .. Small Form Variants
(0xfe30 <= cp && cp <= 0xfe6b) ||
// Halfwidth and Fullwidth Forms
(0xff01 <= cp && cp <= 0xff60) ||
(0xffe0 <= cp && cp <= 0xffe6) ||
// Kana Supplement
(0x1b000 <= cp && cp <= 0x1b001) ||
// Enclosed Ideographic Supplement
(0x1f200 <= cp && cp <= 0x1f251) ||
// CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
(0x20000 <= cp && cp <= 0x3fffd)
)
) {
return true;
}
return false;
} }
} }

View File

@@ -3,22 +3,33 @@ 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, ShowAllNode } from './explorerNode';
import { GitBranch, GitService, GitUri } from '../gitService'; import { GitBranch, GitService, GitUri } from '../gitService';
export class BranchHistoryNode extends ExplorerNode { export class BranchHistoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:branch-history'; readonly resourceType: ResourceType = 'gitlens:branch-history';
constructor(public readonly branch: GitBranch, uri: GitUri, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { maxCount: number | undefined = undefined;
constructor(
public readonly branch: GitBranch,
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
const log = await this.git.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: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git, this.branch))];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.context));
}
return children;
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
@@ -27,7 +38,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

@@ -9,7 +9,11 @@ export class BranchesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:branches'; readonly resourceType: ResourceType = 'gitlens:branches';
constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
) {
super(uri); super(uri);
} }
@@ -18,12 +22,16 @@ export class BranchesNode extends ExplorerNode {
if (branches === undefined) return []; if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name)); branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name));
return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))];
} }
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,19 +2,36 @@
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { getGitStatusIcon, GitCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService'; import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, StatusFileFormatter } from '../gitService';
import * as path from 'path'; import * as path from 'path';
export enum CommitFileNodeDisplayAs {
CommitLabel = 1 << 0,
CommitIcon = 1 << 1,
FileLabel = 1 << 2,
StatusIcon = 1 << 3,
Commit = CommitLabel | CommitIcon,
File = FileLabel | StatusIcon
}
export class CommitFileNode extends ExplorerNode { export class CommitFileNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:commit-file'; readonly resourceType: ResourceType = 'gitlens:commit-file';
constructor(public readonly status: IGitStatusFile, public commit: GitCommit, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
public readonly status: IGitStatusFile,
public commit: GitCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
private displayAs: CommitFileNodeDisplayAs = CommitFileNodeDisplayAs.Commit,
public readonly branch?: GitBranch
) {
super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha })); super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha }));
} }
getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
return Promise.resolve([]); return [];
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
@@ -25,10 +42,13 @@ export class CommitFileNode extends ExplorerNode {
} }
} }
const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.None); const item = new TreeItem(this.label, TreeItemCollapsibleState.None);
item.contextValue = this.resourceType; item.contextValue = this.resourceType;
const icon = getGitStatusIcon(this.status.status); const icon = (this.displayAs & CommitFileNodeDisplayAs.CommitIcon)
? 'icon-commit.svg'
: getGitStatusIcon(this.status.status);
item.iconPath = { item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon)) light: this.context.asAbsolutePath(path.join('images', 'light', icon))
@@ -36,20 +56,34 @@ export class CommitFileNode extends ExplorerNode {
item.command = this.getCommand(); item.command = this.getCommand();
// Only cache the label for a single refresh
this._label = undefined;
return item; return item;
} }
getCommand(): Command | undefined { private _label: string | undefined;
let allowMissingPrevious = false; get label() {
let prefix = undefined; if (this._label === undefined) {
if (this.status.status === 'A') { this._label = (this.displayAs & CommitFileNodeDisplayAs.CommitLabel)
allowMissingPrevious = true; ? CommitFormatter.fromTemplate(this.getCommitTemplate(), this.commit, {
prefix = 'added in '; truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions)
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status);
} }
else if (this.status.status === 'D') { return this._label;
prefix = 'deleted in ';
} }
protected getCommitTemplate() {
return this.git.config.gitExplorer.commitFormat;
}
protected getCommitFileTemplate() {
return this.git.config.gitExplorer.commitFileFormat;
}
getCommand(): Command | undefined {
return { return {
title: 'Compare File with Previous Revision', title: 'Compare File with Previous Revision',
command: Commands.DiffWithPrevious, command: Commands.DiffWithPrevious,
@@ -61,9 +95,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

@@ -2,70 +2,51 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, getGitStatusIcon, GitLogCommit, GitService, GitUri } from '../gitService'; import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as path from 'path';
export class CommitNode extends ExplorerNode { export class CommitNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:commit'; readonly resourceType: ResourceType = 'gitlens:commit';
constructor(public readonly commit: GitLogCommit, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { constructor(
public readonly commit: GitLogCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
public readonly branch?: GitBranch
) {
super(new GitUri(commit.uri, commit)); super(new GitUri(commit.uri, commit));
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
if (this.commit.type === 'file') Promise.resolve([]);
const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1); const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1);
if (log === undefined) return []; if (log === undefined) return [];
const commit = Iterables.first(log.commits.values()); const commit = Iterables.first(log.commits.values());
if (commit === undefined) return []; if (commit === undefined) return [];
return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git))]; const children = [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))];
children.sort((a, b) => a.label!.localeCompare(b.label!));
return children;
} }
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.git.config.gitExplorer.commitFormat, this.commit, {
if (this.commit.type === 'file') { truncateMessageAtNewLine: true,
item.collapsibleState = TreeItemCollapsibleState.None; dataFormat: this.git.config.defaultDateFormat
item.command = this.getCommand(); } as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed);
const resourceType: ResourceType = 'gitlens:commit-file';
item.contextValue = resourceType;
const icon = getGitStatusIcon(this.commit.status!);
item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon))
};
}
else {
item.collapsibleState = TreeItemCollapsibleState.Collapsed;
item.contextValue = this.resourceType; item.contextValue = this.resourceType;
item.iconPath = { item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'), dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'),
light: this.context.asAbsolutePath('images/light/icon-commit.svg') light: this.context.asAbsolutePath('images/light/icon-commit.svg')
}; };
}
return item; return item;
} }
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 +58,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' |
@@ -17,6 +20,9 @@ export declare type ResourceType =
'gitlens:stash-file' | 'gitlens:stash-file' |
'gitlens:stashes' | 'gitlens:stashes' |
'gitlens:status' | 'gitlens:status' |
'gitlens:status-file' |
'gitlens:status-files' |
'gitlens:status-file-commits' |
'gitlens:status-upstream'; 'gitlens:status-upstream';
export abstract class ExplorerNode { export abstract class ExplorerNode {
@@ -31,10 +37,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 {
@@ -55,3 +57,45 @@ export class MessageNode extends ExplorerNode {
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 ShowAllNode extends PagerNode {
args: RefreshNodeCommandArgs = { maxCount: 0 };
constructor(message: string, node: ExplorerNode, context: ExtensionContext) {
super(`${message} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context);
}
}

View File

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

View File

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

View File

@@ -1,19 +1,21 @@
'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, WorkspaceState } 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';
export type GitExplorerView = export type GitExplorerView =
'auto' |
'history' | 'history' |
'repository'; 'repository';
export const GitExplorerView = { export const GitExplorerView = {
Auto: 'auto' as GitExplorerView,
History: 'history' as GitExplorerView, History: 'history' as GitExplorerView,
Repository: 'repository' as GitExplorerView Repository: 'repository' as GitExplorerView
}; };
@@ -23,11 +25,15 @@ 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;
private _root?: ExplorerNode; private _root?: ExplorerNode;
private _view: GitExplorerView = GitExplorerView.Repository; private _view: GitExplorerView | undefined;
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>(); private _onDidChangeTreeData = new EventEmitter<ExplorerNode>();
public get onDidChangeTreeData(): Event<ExplorerNode> { public get onDidChangeTreeData(): Event<ExplorerNode> {
@@ -38,26 +44,26 @@ 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();
this._view = this._config.gitExplorer.view;
setCommandContext(CommandContext.GitExplorerView, this._view);
this._root = this.getRootNode();
} }
async getTreeItem(node: ExplorerNode): Promise<TreeItem> { async getTreeItem(node: ExplorerNode): Promise<TreeItem> {
@@ -75,14 +81,14 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
} }
private getRootNode(editor?: TextEditor): ExplorerNode | undefined { private getRootNode(editor?: TextEditor): ExplorerNode | undefined {
const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath });
switch (this._view) { switch (this._view) {
case GitExplorerView.History: return this.getHistoryNode(editor || window.activeTextEditor); case GitExplorerView.History:
case GitExplorerView.Repository: return new RepositoryNode(uri, this.context, this.git); return this.getHistoryNode(editor || window.activeTextEditor);
}
return undefined; default:
const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath });
return new RepositoryNode(uri, this.context, this.git);
}
} }
private getHistoryNode(editor: TextEditor | undefined): ExplorerNode | undefined { private getHistoryNode(editor: TextEditor | undefined): ExplorerNode | undefined {
@@ -108,14 +114,20 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
private onConfigurationChanged() { private onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
if (!Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer)) { const changed = !Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer);
setTimeout(() => {
this._root = this.getRootNode(window.activeTextEditor);
this.refresh();
}, 1);
}
this._config = cfg; this._config = cfg;
if (changed) {
let view = cfg.gitExplorer.view;
if (view === GitExplorerView.Auto) {
view = this.context.workspaceState.get<GitExplorerView>(WorkspaceState.GitExplorerView, GitExplorerView.Repository);
}
this.setView(view);
this._root = this.getRootNode(window.activeTextEditor);
this.refresh();
}
} }
private onRepoChanged(reasons: RepoChangedReasons[]) { private onRepoChanged(reasons: RepoChangedReasons[]) {
@@ -132,12 +144,34 @@ export class GitExplorer implements TreeDataProvider<ExplorerNode> {
this._onDidChangeTreeData.fire(node); this._onDidChangeTreeData.fire(node);
} }
switchTo(view: GitExplorerView) { refreshNode(node: ExplorerNode, args: RefreshNodeCommandArgs) {
if (node instanceof BranchHistoryNode) {
node.maxCount = args.maxCount;
}
this.refresh(node);
}
setView(view: GitExplorerView) {
if (this._view === view) return; if (this._view === view) return;
if (this._config.gitExplorer.view === GitExplorerView.Auto) {
this.context.workspaceState.update(WorkspaceState.GitExplorerView, view);
}
this._view = view; this._view = view;
setCommandContext(CommandContext.GitExplorerView, this._view); setCommandContext(CommandContext.GitExplorerView, this._view);
if (view !== GitExplorerView.Repository) {
this.git.stopWatchingFileSystem();
}
}
switchTo(view: GitExplorerView) {
if (this._view === view) return;
this.setView(view);
this._root = this.getRootNode(window.activeTextEditor); this._root = this.getRootNode(window.activeTextEditor);
this.refresh(); this.refresh();
} }
@@ -176,6 +210,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 +244,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

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

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

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

View File

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

View File

@@ -1,35 +1,47 @@
'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>(); constructor(
public get onDidChangeTreeData(): Event<ExplorerNode> { public readonly commit: GitStashCommit,
return this._onDidChangeTreeData.event; 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);
}
}
const children = statuses.map(s => new StashFileNode(s, this.commit, this.context, this.git));
children.sort((a, b) => a.label!.localeCompare(b.label!));
return children;
} }
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();
}
} }

Some files were not shown because too many files have changed in this diff Show More