7 Commits

Author SHA1 Message Date
3684629c9b Add "external diff" command to SCM groups and files 2017-09-29 14:48:05 -04:00
Eric Amodio
65a3b31ca5 Adds code of conduct file 2017-09-27 02:02:35 -04:00
Eric Amodio
4694fbc1ae Preps v5.3.0 2017-09-26 22:15:45 -04:00
Eric Amodio
b56d101f76 Fixes #153 - untracked folder files don't show properly 2017-09-26 21:52:00 -04:00
Eric Amodio
ce9394297d Adds new file layouts to the custom view 2017-09-26 18:19:53 -04:00
Eric Amodio
4d18bf708d Adds new ${directory} token
Changes ${path} token to be the full path
Adds relative formatting support
2017-09-26 18:19:33 -04:00
Eric Amodio
c44e4c6968 Fixes Objects.values types 2017-09-26 18:13:00 -04:00
26 changed files with 1264 additions and 586 deletions

View File

@@ -4,7 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] ## [5.3.0] - 2017-09-26
### Added
- Adds new file layouts to the `GitLens` custom view
- `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level
- `list` - displays files as a list
- `tree` - displays files as a tree
- Adds `gitlens.gitExplorer.files.layout` setting to specify how the `GitLens` custom view will display files
- Adds `gitlens.gitExplorer.files.compact` setting to specify whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view
- Adds `gitlens.gitExplorer.files.threshold` setting to specify when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view
- Adds `${directory}` token to the file formatting settings
### Changed
- Changes `${path}` token to be the full file path in the file formatting settings
### Fixed
- Fixes [#153](https://github.com/eamodio/vscode-gitlens/issues/153) - New folders treated as files in "Changed Files" section of the sidebar component
## [5.2.0] - 2017-09-23 ## [5.2.0] - 2017-09-23
### Added ### Added

46
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eamodio@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -288,7 +288,7 @@ GitLens is highly customizable and provides many configuration settings to allow
|Name | Description |Name | Description
|-----|------------ |-----|------------
|`gitlens.defaultDateFormat`|Specifies how all absolute dates will be formatted by default\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats |`gitlens.defaultDateFormat`|Specifies how all absolute dates will be formatted by default<br />See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`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
@@ -356,13 +356,16 @@ GitLens is highly customizable and provides many configuration settings to allow
|-----|------------ |-----|------------
|`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view" |`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.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.files.layout`|Specifies how the `GitLens` custom view will display files<br /> `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level<br /> `list` - displays files as a list<br /> `tree` - displays files as a tree
|`gitlens.gitExplorer.files.compact`|Specifies whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view<br />Only applies when displaying files as a `tree` or `auto`
|`gitlens.gitExplorer.files.threshold`|Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view<br />Only applies when displaying files as `auto`
|`gitlens.gitExplorer.includeWorkingTree`|Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view |`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 /> ${directory} - directory name<br /> ${file} - file name<br /> ${filePath} - formatted file name and path<br /> ${path} - full 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 /> ${directory} - directory name<br /> ${file} - file name<br /> ${filePath} - formatted file name and path<br /> ${path} - full 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 |`gitlens.gitExplorer.statusFileFormat`|Specifies the format of the status of a working or committed file in the `GitLens` custom view<br />Available tokens<br /> ${directory} - directory name<br /> ${file} - file name<br /> ${filePath} - formatted file name and path<br /> ${path} - full file path<br />${working} - optional indicator if the file is uncommitted
### Custom Remotes Settings ### Custom Remotes Settings

277
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.2.0", "version": "5.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -16,7 +16,7 @@
"integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=", "integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "8.0.28" "@types/node": "8.0.31"
} }
}, },
"@types/mocha": { "@types/mocha": {
@@ -26,9 +26,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "8.0.28", "version": "8.0.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.28.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.31.tgz",
"integrity": "sha512-HupkFXEv3O3KSzcr3Ylfajg0kaerBg1DyaZzRBBQfrU3NN1mTBRE7sCveqHwXLS5Yrjvww8qFzkzYQQakG9FuQ==", "integrity": "sha512-R+LdMJHJQwRd/Ca0Nr5KnwbSWHxTD3DWz4ivqoPeNH+YPcuirMWK+Ti9Mx32jOecmPhHOCd+6CefU5e1eVq2Ew==",
"dev": true "dev": true
}, },
"@types/tmp": { "@types/tmp": {
@@ -38,15 +38,23 @@
"dev": true "dev": true
}, },
"ajv": { "ajv": {
"version": "4.11.8", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz",
"integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=",
"dev": true, "dev": true,
"requires": { "requires": {
"co": "4.6.0", "co": "4.6.0",
"fast-deep-equal": "1.0.0",
"json-schema-traverse": "0.3.1",
"json-stable-stringify": "1.0.1" "json-stable-stringify": "1.0.1"
} }
}, },
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"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",
@@ -244,21 +252,6 @@
"supports-color": "2.0.0" "supports-color": "2.0.0"
}, },
"dependencies": { "dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "2.1.1"
}
},
"supports-color": { "supports-color": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@@ -380,15 +373,15 @@
} }
}, },
"dateformat": { "dateformat": {
"version": "2.0.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
"integrity": "sha1-J0Pjq7XD/CRi5SfcpEXgTp9N7hc=", "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
"dev": true "dev": true
}, },
"debug": { "debug": {
"version": "2.6.8", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": { "requires": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -592,6 +585,12 @@
"time-stamp": "1.1.0" "time-stamp": "1.1.0"
} }
}, },
"fast-deep-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
"dev": true
},
"fd-slicer": { "fd-slicer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
@@ -679,7 +678,7 @@
"graceful-fs": "4.1.11", "graceful-fs": "4.1.11",
"inherits": "2.0.3", "inherits": "2.0.3",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"rimraf": "2.6.1" "rimraf": "2.6.2"
} }
}, },
"generate-function": { "generate-function": {
@@ -998,7 +997,7 @@
"oauth-sign": "0.8.2", "oauth-sign": "0.8.2",
"qs": "6.3.2", "qs": "6.3.2",
"stringstream": "0.0.5", "stringstream": "0.0.5",
"tough-cookie": "2.3.2", "tough-cookie": "2.3.3",
"tunnel-agent": "0.4.3", "tunnel-agent": "0.4.3",
"uuid": "3.1.0" "uuid": "3.1.0"
} }
@@ -1081,7 +1080,7 @@
"array-uniq": "1.0.3", "array-uniq": "1.0.3",
"beeper": "1.1.1", "beeper": "1.1.1",
"chalk": "1.1.3", "chalk": "1.1.3",
"dateformat": "2.0.0", "dateformat": "2.2.0",
"fancy-log": "1.3.0", "fancy-log": "1.3.0",
"gulplog": "1.0.0", "gulplog": "1.0.0",
"has-gulplog": "0.1.0", "has-gulplog": "0.1.0",
@@ -1182,9 +1181,9 @@
} }
}, },
"har-schema": { "har-schema": {
"version": "1.0.5", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
"dev": true "dev": true
}, },
"har-validator": { "har-validator": {
@@ -1206,14 +1205,6 @@
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-regex": "2.1.1" "ansi-regex": "2.1.1"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
}
} }
}, },
"has-flag": { "has-flag": {
@@ -1449,6 +1440,12 @@
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"dev": true "dev": true
}, },
"json-schema-traverse": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
},
"json-stable-stringify": { "json-stable-stringify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
@@ -1784,9 +1781,9 @@
} }
}, },
"mocha": { "mocha": {
"version": "3.5.2", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.2.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz",
"integrity": "sha512-iH5zl7afRZl1GvD0pnrRlazgc9Z/o34pXWmTFi8xNIMFKXgNL1SoBTDDb9sJfbV/sJV/j8X+0gvwY1QS1He7Nw==", "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==",
"dev": true, "dev": true,
"requires": { "requires": {
"browser-stdout": "1.3.0", "browser-stdout": "1.3.0",
@@ -1801,6 +1798,17 @@
"lodash.create": "3.1.1", "lodash.create": "3.1.1",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"supports-color": "3.1.2" "supports-color": "3.1.2"
},
"dependencies": {
"debug": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
} }
}, },
"moment": { "moment": {
@@ -1961,9 +1969,9 @@
"dev": true "dev": true
}, },
"performance-now": { "performance-now": {
"version": "0.2.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true "dev": true
}, },
"pinkie": { "pinkie": {
@@ -2110,57 +2118,147 @@
"dev": true "dev": true
}, },
"request": { "request": {
"version": "2.81.0", "version": "2.82.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", "resolved": "https://registry.npmjs.org/request/-/request-2.82.0.tgz",
"integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", "integrity": "sha512-/QWqfmyTfQ4OYs6EhB1h2wQsX9ZxbuNePCvCm0Mdz/mxw73mjdg0D4QdIl0TQBFs35CZmMXLjk0iCGK395CUDg==",
"dev": true, "dev": true,
"requires": { "requires": {
"aws-sign2": "0.6.0", "aws-sign2": "0.7.0",
"aws4": "1.6.0", "aws4": "1.6.0",
"caseless": "0.12.0", "caseless": "0.12.0",
"combined-stream": "1.0.5", "combined-stream": "1.0.5",
"extend": "3.0.1", "extend": "3.0.1",
"forever-agent": "0.6.1", "forever-agent": "0.6.1",
"form-data": "2.1.4", "form-data": "2.3.1",
"har-validator": "4.2.1", "har-validator": "5.0.3",
"hawk": "3.1.3", "hawk": "6.0.2",
"http-signature": "1.1.1", "http-signature": "1.2.0",
"is-typedarray": "1.0.0", "is-typedarray": "1.0.0",
"isstream": "0.1.2", "isstream": "0.1.2",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"mime-types": "2.1.17", "mime-types": "2.1.17",
"oauth-sign": "0.8.2", "oauth-sign": "0.8.2",
"performance-now": "0.2.0", "performance-now": "2.1.0",
"qs": "6.4.0", "qs": "6.5.1",
"safe-buffer": "5.1.1", "safe-buffer": "5.1.1",
"stringstream": "0.0.5", "stringstream": "0.0.5",
"tough-cookie": "2.3.2", "tough-cookie": "2.3.3",
"tunnel-agent": "0.6.0", "tunnel-agent": "0.6.0",
"uuid": "3.1.0" "uuid": "3.1.0"
}, },
"dependencies": { "dependencies": {
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
"dev": true
},
"boom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
"dev": true,
"requires": {
"hoek": "4.2.0"
}
},
"caseless": { "caseless": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true "dev": true
}, },
"har-validator": { "cryptiles": {
"version": "4.2.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
"integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "4.11.8", "boom": "5.2.0"
"har-schema": "1.0.5" },
"dependencies": {
"boom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
"dev": true,
"requires": {
"hoek": "4.2.0"
}
}
}
},
"form-data": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
"dev": true,
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.5",
"mime-types": "2.1.17"
}
},
"har-validator": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
"dev": true,
"requires": {
"ajv": "5.2.3",
"har-schema": "2.0.0"
}
},
"hawk": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
"dev": true,
"requires": {
"boom": "4.3.1",
"cryptiles": "3.1.2",
"hoek": "4.2.0",
"sntp": "2.0.2"
}
},
"hoek": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==",
"dev": true
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"dev": true,
"requires": {
"assert-plus": "1.0.0",
"jsprim": "1.4.1",
"sshpk": "1.13.1"
} }
}, },
"qs": { "qs": {
"version": "6.4.0", "version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
"integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
"dev": true "dev": true
}, },
"sntp": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
"dev": true,
"requires": {
"hoek": "4.2.0"
}
},
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -2188,9 +2286,9 @@
} }
}, },
"rimraf": { "rimraf": {
"version": "2.6.1", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true, "dev": true,
"requires": { "requires": {
"glob": "7.1.1" "glob": "7.1.1"
@@ -2250,7 +2348,7 @@
"resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-2.0.11.tgz", "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-2.0.11.tgz",
"integrity": "sha1-ZUUa1lZigB2up1VJgyp4LeAEjb8=", "integrity": "sha1-ZUUa1lZigB2up1VJgyp4LeAEjb8=",
"requires": { "requires": {
"debug": "2.6.8", "debug": "2.6.9",
"lodash.assign": "4.2.0", "lodash.assign": "4.2.0",
"rxjs": "5.4.3" "rxjs": "5.4.3"
} }
@@ -2339,6 +2437,15 @@
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
"dev": true "dev": true
}, },
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "2.1.1"
}
},
"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",
@@ -2439,9 +2546,9 @@
} }
}, },
"tough-cookie": { "tough-cookie": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
"integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
"dev": true, "dev": true,
"requires": { "requires": {
"punycode": "1.4.1" "punycode": "1.4.1"
@@ -2468,13 +2575,13 @@
"resolve": "1.4.0", "resolve": "1.4.0",
"semver": "5.4.1", "semver": "5.4.1",
"tslib": "1.7.1", "tslib": "1.7.1",
"tsutils": "2.8.2" "tsutils": "2.9.0"
} }
}, },
"tsutils": { "tsutils": {
"version": "2.8.2", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.8.2.tgz", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.9.0.tgz",
"integrity": "sha1-LBSGukMSYIRbCsb5Aq/Z1wio6mo=", "integrity": "sha1-fhU3tVa6tocvp+ZIXf9FsHbVUz0=",
"dev": true, "dev": true,
"requires": { "requires": {
"tslib": "1.7.1" "tslib": "1.7.1"
@@ -2494,9 +2601,9 @@
"optional": true "optional": true
}, },
"typescript": { "typescript": {
"version": "2.5.2", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz",
"integrity": "sha1-A4qV99m7tCCxvzW6MdTFwd0//jQ=", "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==",
"dev": true "dev": true
}, },
"unique-stream": { "unique-stream": {
@@ -2687,8 +2794,8 @@
"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.2", "mocha": "3.5.3",
"request": "2.81.0", "request": "2.82.0",
"semver": "5.4.1", "semver": "5.4.1",
"source-map-support": "0.4.18", "source-map-support": "0.4.18",
"url-parse": "1.1.9", "url-parse": "1.1.9",

View File

@@ -1,6 +1,6 @@
{ {
"name": "gitlens", "name": "gitlens",
"version": "5.2.0", "version": "5.3.0",
"author": { "author": {
"name": "Eric Amodio", "name": "Eric Amodio",
"email": "eamodio@gmail.com" "email": "eamodio@gmail.com"
@@ -421,13 +421,33 @@
"gitlens.gitExplorer.commitFileFormat": { "gitlens.gitExplorer.commitFileFormat": {
"type": "string", "type": "string",
"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 ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path"
}, },
"gitlens.gitExplorer.enabled": { "gitlens.gitExplorer.enabled": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"description": "Specifies whether or not to show the `GitLens` custom view" "description": "Specifies whether or not to show the `GitLens` custom view"
}, },
"gitlens.gitExplorer.files.layout": {
"type": "string",
"default": "auto",
"enum": [
"auto",
"list",
"tree"
],
"description": "Specifies how the `GitLens` custom view will display files\n `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level\n `list` - displays files as a list\n `tree` - displays files as a tree"
},
"gitlens.gitExplorer.files.compact": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view\nOnly applies when displaying files as a `tree` or `auto`"
},
"gitlens.gitExplorer.files.threshold": {
"type": "number",
"default": 5,
"description": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view\nOnly applies when displaying files as `auto`"
},
"gitlens.gitExplorer.includeWorkingTree": { "gitlens.gitExplorer.includeWorkingTree": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
@@ -446,12 +466,12 @@
"gitlens.gitExplorer.stashFileFormat": { "gitlens.gitExplorer.stashFileFormat": {
"type": "string", "type": "string",
"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 ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path"
}, },
"gitlens.gitExplorer.statusFileFormat": { "gitlens.gitExplorer.statusFileFormat": {
"type": "string", "type": "string",
"default": "${working}${filePath}", "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" "description": "Specifies the format of the status of a working or committed file in the `GitLens` custom view\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path\n ${working} - optional indicator if the file is uncommitted"
}, },
"gitlens.gitExplorer.view": { "gitlens.gitExplorer.view": {
"type": "string", "type": "string",
@@ -1048,6 +1068,11 @@
"light": "images/light/icon-add.svg" "light": "images/light/icon-add.svg"
} }
}, },
{
"command": "gitlens.externalDiff",
"title": "Open Changes (with difftool)",
"category": "GitLens"
},
{ {
"command": "gitlens.resetSuppressedWarnings", "command": "gitlens.resetSuppressedWarnings",
"title": "Reset Suppressed Warnings", "title": "Reset Suppressed Warnings",
@@ -1500,17 +1525,22 @@
{ {
"command": "gitlens.openChangedFiles", "command": "gitlens.openChangedFiles",
"when": "gitlens:enabled", "when": "gitlens:enabled",
"group": "1_gitlens@1" "group": "2_gitlens@1"
}, },
{ {
"command": "gitlens.closeUnchangedFiles", "command": "gitlens.closeUnchangedFiles",
"when": "gitlens:enabled", "when": "gitlens:enabled",
"group": "1_gitlens@2" "group": "2_gitlens@2"
},
{
"command": "gitlens.externalDiff",
"when": "gitlens:enabled",
"group": "2_gitlens@3"
}, },
{ {
"command": "gitlens.stashSave", "command": "gitlens.stashSave",
"when": "gitlens:enabled", "when": "gitlens:enabled",
"group": "2_gitlens@1" "group": "3_gitlens@1"
} }
], ],
"scm/resourceState/context": [ "scm/resourceState/context": [
@@ -1519,6 +1549,11 @@
"when": "gitlens:enabled && gitlens:hasRemotes", "when": "gitlens:enabled && gitlens:hasRemotes",
"group": "navigation" "group": "navigation"
}, },
{
"command": "gitlens.externalDiff",
"when": "gitlens:enabled",
"group": "navigation"
},
{ {
"command": "gitlens.diffWithRevision", "command": "gitlens.diffWithRevision",
"when": "gitlens:enabled", "when": "gitlens:enabled",
@@ -1938,11 +1973,11 @@
"@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.43", "@types/mocha": "2.2.43",
"@types/node": "8.0.28", "@types/node": "8.0.31",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"mocha": "3.5.2", "mocha": "3.5.3",
"tslint": "5.7.0", "tslint": "5.7.0",
"typescript": "2.5.2", "typescript": "2.5.3",
"vscode": "1.1.5" "vscode": "1.1.5"
} }
} }

View File

@@ -138,7 +138,7 @@ export class Annotations {
// Try to get the width of the string, if there is a cap // Try to get the width of the string, if there is a cap
let width = 4; // Start with a padding let width = 4; // Start with a padding
for (const token of Objects.values<Strings.ITokenOptions | undefined>(options.tokenOptions)) { for (const token of Objects.values(options.tokenOptions!)) {
if (token === undefined) continue; if (token === undefined) continue;
// If any token is uncapped, kick out and set no max // If any token is uncapped, kick out and set no max

View File

@@ -14,6 +14,7 @@ export * from './commands/diffWithNext';
export * from './commands/diffWithPrevious'; export * from './commands/diffWithPrevious';
export * from './commands/diffWithRevision'; export * from './commands/diffWithRevision';
export * from './commands/diffWithWorking'; export * from './commands/diffWithWorking';
export * from './commands/externalDiff';
export * from './commands/openChangedFiles'; export * from './commands/openChangedFiles';
export * from './commands/openBranchesInRemote'; export * from './commands/openBranchesInRemote';
export * from './commands/openBranchInRemote'; export * from './commands/openBranchInRemote';

View File

@@ -19,6 +19,7 @@ export type Commands =
'gitlens.diffWithRevision' | 'gitlens.diffWithRevision' |
'gitlens.diffWithWorking' | 'gitlens.diffWithWorking' |
'gitlens.diffLineWithWorking' | 'gitlens.diffLineWithWorking' |
'gitlens.externalDiff' |
'gitlens.openChangedFiles' | 'gitlens.openChangedFiles' |
'gitlens.openBranchesInRemote' | 'gitlens.openBranchesInRemote' |
'gitlens.openBranchInRemote' | 'gitlens.openBranchInRemote' |
@@ -61,6 +62,7 @@ export const Commands = {
DiffWithRevision: 'gitlens.diffWithRevision' as Commands, DiffWithRevision: 'gitlens.diffWithRevision' as Commands,
DiffWithWorking: 'gitlens.diffWithWorking' as Commands, DiffWithWorking: 'gitlens.diffWithWorking' as Commands,
DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands, DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands,
ExternalDiff: 'gitlens.externalDiff' as Commands,
OpenChangedFiles: 'gitlens.openChangedFiles' as Commands, OpenChangedFiles: 'gitlens.openChangedFiles' as Commands,
OpenBranchesInRemote: 'gitlens.openBranchesInRemote' as Commands, OpenBranchesInRemote: 'gitlens.openBranchesInRemote' as Commands,
OpenBranchInRemote: 'gitlens.openBranchInRemote' as Commands, OpenBranchInRemote: 'gitlens.openBranchInRemote' as Commands,

View File

@@ -0,0 +1,115 @@
'use strict';
import { commands, SourceControlResourceState, Uri, window } from 'vscode';
import { Command, Commands } from './common';
import { BuiltInCommands } from '../constants';
import { CommandContext } from '../commands';
import { GitService } from '../gitService';
import { Logger } from '../logger';
import { Messages } from '../messages';
enum Status {
INDEX_MODIFIED,
INDEX_ADDED,
INDEX_DELETED,
INDEX_RENAMED,
INDEX_COPIED,
MODIFIED,
DELETED,
UNTRACKED,
IGNORED,
ADDED_BY_US,
ADDED_BY_THEM,
DELETED_BY_US,
DELETED_BY_THEM,
BOTH_ADDED,
BOTH_DELETED,
BOTH_MODIFIED
}
enum ResourceGroupType {
Merge,
Index,
WorkingTree
}
interface Resource extends SourceControlResourceState {
readonly resourceGroupType: ResourceGroupType;
readonly type: Status;
}
class ExternalDiffFile {
constructor(public uri: Uri, public staged: boolean) {
}
}
export interface ExternalDiffCommandArgs {
files?: ExternalDiffFile[];
}
export class ExternalDiffCommand extends Command {
constructor(private git: GitService) {
super(Commands.ExternalDiff);
}
protected async preExecute(context: CommandContext, args: ExternalDiffCommandArgs = {}): Promise<any> {
if (context.type === 'scm-states') {
args = { ...args };
args.files = context.scmResourceStates.map<ExternalDiffFile>((_: Resource) => new ExternalDiffFile(_.resourceUri, _.resourceGroupType === ResourceGroupType.Index));
return this.execute(args);
} else if (context.type === 'scm-groups') {
const isModified = (status: Status): boolean => status === Status.BOTH_MODIFIED || status === Status.INDEX_MODIFIED || status === Status.MODIFIED;
args = { ...args };
args.files = context.scmResourceGroups[0].resourceStates.filter((_: Resource) => isModified(_.type)).map<ExternalDiffFile>((_: Resource) => new ExternalDiffFile(_.resourceUri, _.resourceGroupType === ResourceGroupType.Index));
return this.execute(args);
}
return this.execute(args);
}
async execute(args: ExternalDiffCommandArgs = {}) {
try {
const diffTool = await this.git.getConfig('diff.tool');
if (!diffTool) {
const result = await window.showWarningMessage(`Unable to open file compare because there is no Git diff tool configured`, 'View Git Docs');
if (!result) return undefined;
return commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://git-scm.com/docs/git-config#git-config-difftool'));
}
const repoPath = await this.git.getRepoPathFromUri(undefined);
if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open changed files`);
if (!args.files) {
const status = await this.git.getStatusForRepo(repoPath);
if (status === undefined) return window.showWarningMessage(`Unable to open changed files`);
args.files = [];
for (const file of status.files) {
if (file.indexStatus === 'M') {
args.files.push(new ExternalDiffFile(file.Uri, true));
}
if (file.workTreeStatus === 'M') {
args.files.push(new ExternalDiffFile(file.Uri, false));
}
}
}
for (const file of args.files) {
this.git.openDiffTool(repoPath, file.uri, file.staged);
}
return undefined;
}
catch (ex) {
Logger.error(ex, 'ExternalDiffCommand');
return window.showErrorMessage(`Unable to open external diff. See output channel for more details`);
}
}
}

View File

@@ -11,7 +11,7 @@ export class ResetSuppressedWarningsCommand extends Command {
} }
async execute() { async execute() {
for (const key of Objects.values<string>(SuppressedKeys)) { for (const key of Objects.values(SuppressedKeys)) {
await this.context.globalState.update(key, false); await this.context.globalState.update(key, false);
} }
} }

View File

@@ -53,6 +53,16 @@ export const CustomRemoteType = {
GitLab: 'GitLab' as CustomRemoteType GitLab: 'GitLab' as CustomRemoteType
}; };
export type GitExplorerFilesLayout =
'auto' |
'list' |
'tree';
export const GitExplorerFilesLayout = {
Auto: 'auto' as GitExplorerFilesLayout,
List: 'list' as GitExplorerFilesLayout,
Tree: 'tree' as GitExplorerFilesLayout
};
export type StatusBarCommand = export type StatusBarCommand =
'gitlens.toggleFileBlame' | 'gitlens.toggleFileBlame' |
'gitlens.showBlameHistory' | 'gitlens.showBlameHistory' |
@@ -132,6 +142,24 @@ export interface ICodeLensLanguageLocation {
customSymbols?: string[]; customSymbols?: string[];
} }
export interface IGitExplorerConfig {
enabled: boolean;
view: GitExplorerView;
files: {
layout: GitExplorerFilesLayout;
compact: boolean;
threshold: number;
};
includeWorkingTree: boolean;
showTrackingBranch: boolean;
commitFormat: string;
commitFileFormat: string;
stashFormat: string;
stashFileFormat: string;
statusFileFormat: string;
// dateFormat: string | null;
}
export interface IRemotesConfig { export interface IRemotesConfig {
type: CustomRemoteType; type: CustomRemoteType;
domain: string; domain: string;
@@ -316,18 +344,7 @@ export interface IConfig {
defaultDateFormat: string | null; defaultDateFormat: string | null;
gitExplorer: { gitExplorer: IGitExplorerConfig;
enabled: boolean;
view: GitExplorerView;
includeWorkingTree: boolean;
showTrackingBranch: boolean;
commitFormat: string;
commitFileFormat: string;
stashFormat: string;
stashFileFormat: string;
statusFileFormat: string;
// dateFormat: string | null;
};
remotes: IRemotesConfig[]; remotes: IRemotesConfig[];

View File

@@ -3,6 +3,7 @@
import { commands, ExtensionContext, extensions, languages, window, workspace } from 'vscode'; import { commands, ExtensionContext, extensions, languages, window, workspace } from 'vscode';
import { AnnotationController } from './annotations/annotationController'; import { AnnotationController } from './annotations/annotationController';
import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands';
import { ExternalDiffCommand } 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, DiffWithCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithRevisionCommand, DiffWithWorkingCommand } from './commands';
@@ -99,6 +100,7 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(commands.registerTextEditorCommand('gitlens.computingFileAnnotations', () => { })); context.subscriptions.push(commands.registerTextEditorCommand('gitlens.computingFileAnnotations', () => { }));
context.subscriptions.push(new CloseUnchangedFilesCommand(git)); context.subscriptions.push(new CloseUnchangedFilesCommand(git));
context.subscriptions.push(new ExternalDiffCommand(git));
context.subscriptions.push(new OpenChangedFilesCommand(git)); context.subscriptions.push(new OpenChangedFilesCommand(git));
context.subscriptions.push(new CopyMessageToClipboardCommand(git)); context.subscriptions.push(new CopyMessageToClipboardCommand(git));
context.subscriptions.push(new CopyShaToClipboardCommand(git)); context.subscriptions.push(new CopyShaToClipboardCommand(git));

View File

@@ -6,7 +6,10 @@ import { GitStatusFile, IGitStatusFile, IGitStatusFileWithCommit } from '../mode
import * as path from 'path'; import * as path from 'path';
export interface IStatusFormatOptions extends IFormatOptions { export interface IStatusFormatOptions extends IFormatOptions {
relativePath?: string;
tokenOptions?: { tokenOptions?: {
directory?: Strings.ITokenOptions;
file?: Strings.ITokenOptions; file?: Strings.ITokenOptions;
filePath?: Strings.ITokenOptions; filePath?: Strings.ITokenOptions;
path?: Strings.ITokenOptions; path?: Strings.ITokenOptions;
@@ -15,18 +18,23 @@ export interface IStatusFormatOptions extends IFormatOptions {
export class StatusFileFormatter extends Formatter<IGitStatusFile, IStatusFormatOptions> { export class StatusFileFormatter extends Formatter<IGitStatusFile, IStatusFormatOptions> {
get directory() {
const directory = GitStatusFile.getFormattedDirectory(this._item, false, this._options.relativePath);
return this._padOrTruncate(directory, this._options.tokenOptions!.file);
}
get file() { get file() {
const file = path.basename(this._item.fileName); const file = path.basename(this._item.fileName);
return this._padOrTruncate(file, this._options.tokenOptions!.file); return this._padOrTruncate(file, this._options.tokenOptions!.file);
} }
get filePath() { get filePath() {
const filePath = GitStatusFile.getFormattedPath(this._item); const filePath = GitStatusFile.getFormattedPath(this._item, undefined, this._options.relativePath);
return this._padOrTruncate(filePath, this._options.tokenOptions!.filePath); return this._padOrTruncate(filePath, this._options.tokenOptions!.filePath);
} }
get path() { get path() {
const directory = GitStatusFile.getFormattedDirectory(this._item, false); const directory = GitStatusFile.getRelativePath(this._item, this._options.relativePath);
return this._padOrTruncate(directory, this._options.tokenOptions!.file); return this._padOrTruncate(directory, this._options.tokenOptions!.file);
} }

View File

@@ -276,6 +276,17 @@ export class Git {
return gitCommand({ cwd: repoPath }, ...params); return gitCommand({ cwd: repoPath }, ...params);
} }
static difftool_fileDiff(repoPath: string, fileName: string, staged: boolean) {
const params = [`difftool`, `--no-prompt`];
if (staged) {
params.push('--staged');
}
params.push('--');
params.push(fileName);
return gitCommand({ cwd: repoPath }, ...params);
}
static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) { static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) {
const params = [...defaultLogParams, `-m`]; const params = [...defaultLogParams, `-m`];
if (maxCount && !reverse) { if (maxCount && !reverse) {
@@ -420,7 +431,7 @@ export class Git {
static status(repoPath: string, porcelainVersion: number = 1): Promise<string> { static status(repoPath: string, porcelainVersion: number = 1): Promise<string> {
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain'; const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
return gitCommand({ cwd: repoPath }, 'status', porcelain, '--branch'); return gitCommand({ cwd: repoPath }, 'status', porcelain, '--branch', '-u');
} }
static status_file(repoPath: string, fileName: string, porcelainVersion: number = 1): Promise<string> { static status_file(repoPath: string, fileName: string, porcelainVersion: number = 1): Promise<string> {

View File

@@ -63,11 +63,14 @@ export class GitUri extends Uri {
return Uri.file(this.sha ? this.path : this.fsPath); return Uri.file(this.sha ? this.path : this.fsPath);
} }
getFormattedPath(separator: string = Strings.pad(GlyphChars.Dot, 2, 2)): string { getFormattedPath(separator: string = Strings.pad(GlyphChars.Dot, 2, 2), relativeTo?: string): string {
let directory = path.dirname(this.fsPath); let directory = path.dirname(this.fsPath);
if (this.repoPath) { if (this.repoPath) {
directory = path.relative(this.repoPath, directory); directory = path.relative(this.repoPath, directory);
} }
if (relativeTo !== undefined) {
directory = path.relative(relativeTo, directory);
}
directory = GitService.normalizePath(directory); directory = GitService.normalizePath(directory);
return (!directory || directory === '.') return (!directory || directory === '.')
@@ -75,8 +78,12 @@ export class GitUri extends Uri {
: `${path.basename(this.fsPath)}${separator}${directory}`; : `${path.basename(this.fsPath)}${separator}${directory}`;
} }
getRelativePath(): string { getRelativePath(relativeTo?: string): string {
return GitService.normalizePath(path.relative(this.repoPath || '', this.fsPath)); let relativePath = path.relative(this.repoPath || '', this.fsPath);
if (relativeTo !== undefined) {
relativePath = path.relative(relativeTo, relativePath);
}
return GitService.normalizePath(relativePath);
} }
static async fromUri(uri: Uri, git: GitService) { static async fromUri(uri: Uri, git: GitService) {
@@ -104,15 +111,19 @@ export class GitUri extends Uri {
return new GitUri(uri, repoPathOrCommit); return new GitUri(uri, repoPathOrCommit);
} }
static getDirectory(fileName: string): string { static getDirectory(fileName: string, relativeTo?: string): string {
const directory: string | undefined = GitService.normalizePath(path.dirname(fileName)); let directory: string | undefined = path.dirname(fileName);
if (relativeTo !== undefined) {
directory = path.relative(relativeTo, directory);
}
directory = GitService.normalizePath(directory);
return (!directory || directory === '.') ? '' : directory; return (!directory || directory === '.') ? '' : directory;
} }
static getFormattedPath(fileNameOrUri: string | Uri, separator: string = Strings.pad(GlyphChars.Dot, 2, 2)): string { static getFormattedPath(fileNameOrUri: string | Uri, separator: string = Strings.pad(GlyphChars.Dot, 2, 2), relativeTo?: string): string {
let fileName: string; let fileName: string;
if (fileNameOrUri instanceof Uri) { if (fileNameOrUri instanceof Uri) {
if (fileNameOrUri instanceof GitUri) return fileNameOrUri.getFormattedPath(separator); if (fileNameOrUri instanceof GitUri) return fileNameOrUri.getFormattedPath(separator, relativeTo);
fileName = fileNameOrUri.fsPath; fileName = fileNameOrUri.fsPath;
} }
@@ -120,11 +131,29 @@ export class GitUri extends Uri {
fileName = fileNameOrUri; fileName = fileNameOrUri;
} }
const directory = GitUri.getDirectory(fileName); const directory = GitUri.getDirectory(fileName, relativeTo);
return !directory return !directory
? path.basename(fileName) ? path.basename(fileName)
: `${path.basename(fileName)}${separator}${directory}`; : `${path.basename(fileName)}${separator}${directory}`;
} }
static getRelativePath(fileNameOrUri: string | Uri, relativeTo?: string, repoPath?: string): string {
let fileName: string;
if (fileNameOrUri instanceof Uri) {
if (fileNameOrUri instanceof GitUri) return fileNameOrUri.getRelativePath(relativeTo);
fileName = fileNameOrUri.fsPath;
}
else {
fileName = fileNameOrUri;
}
let relativePath = path.relative(repoPath || '', fileName);
if (relativeTo !== undefined) {
relativePath = path.relative(relativeTo, relativePath);
}
return GitService.normalizePath(relativePath);
}
} }
export interface IGitCommitInfo { export interface IGitCommitInfo {

View File

@@ -26,6 +26,8 @@ export interface IGitStatusFile {
status: GitStatusFileStatus; status: GitStatusFileStatus;
fileName: string; fileName: string;
originalFileName?: string; originalFileName?: string;
workTreeStatus: GitStatusFileStatus;
indexStatus: GitStatusFileStatus;
} }
export interface IGitStatusFileWithCommit extends IGitStatusFile { export interface IGitStatusFileWithCommit extends IGitStatusFile {
@@ -36,7 +38,7 @@ export class GitStatusFile implements IGitStatusFile {
originalFileName?: string; originalFileName?: string;
constructor(public repoPath: string, public status: GitStatusFileStatus, public fileName: string, public staged: boolean, originalFileName?: string) { constructor(public repoPath: string, public status: GitStatusFileStatus, public workTreeStatus: GitStatusFileStatus, public indexStatus: GitStatusFileStatus, public fileName: string, public staged: boolean, originalFileName?: string) {
this.originalFileName = originalFileName; this.originalFileName = originalFileName;
} }
@@ -56,15 +58,19 @@ export class GitStatusFile implements IGitStatusFile {
return Uri.file(path.resolve(this.repoPath, this.fileName)); return Uri.file(path.resolve(this.repoPath, this.fileName));
} }
static getFormattedDirectory(status: IGitStatusFile, includeOriginal: boolean = false): string { static getFormattedDirectory(status: IGitStatusFile, includeOriginal: boolean = false, relativeTo?: string): string {
const directory = GitUri.getDirectory(status.fileName); const directory = GitUri.getDirectory(status.fileName, relativeTo);
return (includeOriginal && status.status === 'R' && status.originalFileName) return (includeOriginal && status.status === 'R' && status.originalFileName)
? `${directory} ${Strings.pad(GlyphChars.ArrowLeft, 1, 1)} ${status.originalFileName}` ? `${directory} ${Strings.pad(GlyphChars.ArrowLeft, 1, 1)} ${status.originalFileName}`
: directory; : directory;
} }
static getFormattedPath(status: IGitStatusFile, separator: string = Strings.pad(GlyphChars.Dot, 2, 2)): string { static getFormattedPath(status: IGitStatusFile, separator: string = Strings.pad(GlyphChars.Dot, 2, 2), relativeTo?: string): string {
return GitUri.getFormattedPath(status.fileName, separator); return GitUri.getFormattedPath(status.fileName, separator, relativeTo);
}
static getRelativePath(status: IGitStatusFile, relativeTo?: string): string {
return GitUri.getRelativePath(status.fileName, relativeTo);
} }
} }

View File

@@ -6,6 +6,8 @@ interface FileStatusEntry {
status: GitStatusFileStatus; status: GitStatusFileStatus;
fileName: string; fileName: string;
originalFileName: string; originalFileName: string;
workTreeStatus: GitStatusFileStatus;
indexStatus: GitStatusFileStatus;
} }
const aheadStatusV1Regex = /(?:ahead ([0-9]+))/; const aheadStatusV1Regex = /(?:ahead ([0-9]+))/;
@@ -69,7 +71,7 @@ export class GitStatusParser {
else { else {
entry = this._parseFileEntry(rawStatus, fileName); entry = this._parseFileEntry(rawStatus, fileName);
} }
status.files.push(new GitStatusFile(repoPath, entry.status, entry.fileName, entry.staged, entry.originalFileName)); status.files.push(new GitStatusFile(repoPath, entry.status, entry.workTreeStatus, entry.indexStatus, entry.fileName, entry.staged, entry.originalFileName));
} }
} }
} }
@@ -117,7 +119,7 @@ export class GitStatusParser {
} }
if (entry !== undefined) { if (entry !== undefined) {
status.files.push(new GitStatusFile(repoPath, entry.status, entry.fileName, entry.staged, entry.originalFileName)); status.files.push(new GitStatusFile(repoPath, entry.status, entry.workTreeStatus, entry.indexStatus, entry.fileName, entry.staged, entry.originalFileName));
} }
} }
} }
@@ -131,7 +133,9 @@ export class GitStatusParser {
status: (indexStatus || workTreeStatus || '?') as GitStatusFileStatus, status: (indexStatus || workTreeStatus || '?') as GitStatusFileStatus,
fileName: fileName, fileName: fileName,
originalFileName: originalFileName, originalFileName: originalFileName,
staged: !!indexStatus staged: !!indexStatus,
indexStatus: indexStatus,
workTreeStatus: workTreeStatus
} as FileStatusEntry; } as FileStatusEntry;
} }
} }

View File

@@ -1036,6 +1036,12 @@ export class GitService extends Disposable {
return !!result; return !!result;
} }
openDiffTool(repoPath: string, uri: Uri, staged: boolean) {
Logger.log(`openDiffTool('${repoPath}', '${uri}', ${staged})`);
return Git.difftool_fileDiff(repoPath, uri.fsPath, staged);
}
openDirectoryDiff(repoPath: string, sha1: string, sha2?: string) { openDirectoryDiff(repoPath: string, sha1: string, sha2?: string) {
Logger.log(`openDirectoryDiff('${repoPath}', ${sha1}, ${sha2})`); Logger.log(`openDirectoryDiff('${repoPath}', ${sha1}, ${sha2})`);

View File

@@ -1,6 +1,16 @@
'use strict'; 'use strict';
import { Objects } from './object';
export namespace Arrays { export namespace Arrays {
export function countUniques<T>(array: T[], accessor: (item: T) => string): { [key: string]: number } {
const uniqueCounts = Object.create(null);
for (const item of array) {
const value = accessor(item);
uniqueCounts[value] = (uniqueCounts[value] || 0) + 1;
}
return uniqueCounts;
}
export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } { export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } {
return array.reduce((previous, current) => { return array.reduce((previous, current) => {
const value = accessor(current); const value = accessor(current);
@@ -10,6 +20,96 @@ export namespace Arrays {
}, Object.create(null)); }, Object.create(null));
} }
export interface IHierarchicalItem<T> {
name: string;
relativePath: string;
value?: T;
// parent?: IHierarchicalItem<T>;
children: { [key: string]: IHierarchicalItem<T> } | undefined;
descendants: T[] | undefined;
}
export function makeHierarchical<T>(values: T[], splitPath: (i: T) => string[], joinPath: (...paths: string[]) => string, compact: boolean = false): IHierarchicalItem<T> {
const seed = {
name: '',
relativePath: '',
children: Object.create(null),
descendants: []
};
const hierarchy = values.reduce((root: IHierarchicalItem<T>, value) => {
let folder = root;
let relativePath = '';
for (const folderName of splitPath(value)) {
relativePath = joinPath(relativePath, folderName);
if (folder.children === undefined) {
folder.children = Object.create(null);
}
let f = folder.children![folderName];
if (f === undefined) {
folder.children![folderName] = f = {
name: folderName,
relativePath: relativePath,
// parent: folder,
children: undefined,
descendants: undefined
};
}
if (folder.descendants === undefined) {
folder.descendants = [];
}
folder.descendants.push(value);
folder = f;
}
folder.value = value;
return root;
}, seed);
if (compact) return compactHierarchy(hierarchy, joinPath, true);
return hierarchy;
}
export function compactHierarchy<T>(root: IHierarchicalItem<T>, joinPath: (...paths: string[]) => string, isRoot: boolean = true): IHierarchicalItem<T> {
if (root.children === undefined) return root;
const children = [...Objects.values(root.children)];
// // Attempts less nesting but duplicate roots
// if (!isRoot && children.every(c => c.value === undefined)) {
// const parentSiblings = root.parent!.children!;
// if (parentSiblings[root.name] !== undefined) {
// delete parentSiblings[root.name];
// for (const child of children) {
// child.name = joinPath(root.name, child.name);
// parentSiblings[child.name] = child;
// }
// }
// }
for (const child of children) {
compactHierarchy(child, joinPath, false);
}
if (!isRoot && children.length === 1) {
const child = children[0];
if (child.value === undefined) {
root.name = joinPath(root.name, child.name);
root.relativePath = child.relativePath;
root.children = child.children;
}
}
return root;
}
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(_ => {

View File

@@ -6,7 +6,9 @@ export namespace Objects {
return _isEqual(first, second); return _isEqual(first, second);
} }
export function* entries(o: any): IterableIterator<[string, any]> { export function entries<T>(o: { [key: string]: T }): IterableIterator<[string, T]>;
export function entries<T>(o: { [key: number]: T }): IterableIterator<[string, T]>;
export function* entries<T>(o: any): IterableIterator<[string, T]> {
for (const key in o) { for (const key in o) {
yield [key, o[key]]; yield [key, o[key]];
} }
@@ -56,6 +58,8 @@ export namespace Objects {
} }
} }
export function values<T>(o: { [key: string]: T }): IterableIterator<T>;
export function values<T>(o: { [key: number]: T }): IterableIterator<T>;
export function* values<T>(o: any): IterableIterator<T> { export function* values<T>(o: any): IterableIterator<T> {
for (const key in o) { for (const key in o) {
yield o[key]; yield o[key];

View File

@@ -2,7 +2,7 @@
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 { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, StatusFileFormatter } from '../gitService'; import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
import * as path from 'path'; import * as path from 'path';
export enum CommitFileNodeDisplayAs { export enum CommitFileNodeDisplayAs {
@@ -17,6 +17,8 @@ export enum CommitFileNodeDisplayAs {
export class CommitFileNode extends ExplorerNode { export class CommitFileNode extends ExplorerNode {
readonly priority: boolean = false;
readonly repoPath: string;
readonly resourceType: ResourceType = 'gitlens:commit-file'; readonly resourceType: ResourceType = 'gitlens:commit-file';
constructor( constructor(
@@ -28,6 +30,7 @@ export class CommitFileNode extends ExplorerNode {
public readonly branch?: GitBranch 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 }));
this.repoPath = commit.repoPath;
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
@@ -36,7 +39,7 @@ export class CommitFileNode extends ExplorerNode {
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
if (this.commit.type !== 'file') { if (this.commit.type !== 'file') {
const log = await this.git.getLogForFile(this.commit.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 }); const log = await this.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
if (log !== undefined) { if (log !== undefined) {
this.commit = log.commits.get(this.commit.sha) || this.commit; this.commit = log.commits.get(this.commit.sha) || this.commit;
} }
@@ -62,6 +65,14 @@ export class CommitFileNode extends ExplorerNode {
return item; return item;
} }
private _folderName: string | undefined;
get folderName() {
if (this._folderName === undefined) {
this._folderName = path.dirname(this.uri.getRelativePath());
}
return this._folderName;
}
private _label: string | undefined; private _label: string | undefined;
get label() { get label() {
if (this._label === undefined) { if (this._label === undefined) {
@@ -70,11 +81,22 @@ export class CommitFileNode extends ExplorerNode {
truncateMessageAtNewLine: true, truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions) } as ICommitFormatOptions)
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status); : StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(),
this.status,
{ relativePath: this.relativePath } as IStatusFormatOptions);
} }
return this._label; return this._label;
} }
private _relativePath: string | undefined;
get relativePath(): string | undefined {
return this._relativePath;
}
set relativePath(value: string | undefined) {
this._relativePath = value;
this._label = undefined;
}
protected getCommitTemplate() { protected getCommitTemplate() {
return this.git.config.gitExplorer.commitFormat; return this.git.config.gitExplorer.commitFormat;
} }

View File

@@ -1,13 +1,17 @@
'use strict'; 'use strict';
import { Iterables } from '../system'; import { Arrays, 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, CommitFileNodeDisplayAs } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { GitExplorerFilesLayout } from '../configuration';
import { FolderNode, IFileExplorerNode } from './folderNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as path from 'path';
export class CommitNode extends ExplorerNode { export class CommitNode extends ExplorerNode {
readonly repoPath: string;
readonly resourceType: ResourceType = 'gitlens:commit'; readonly resourceType: ResourceType = 'gitlens:commit';
constructor( constructor(
@@ -17,17 +21,32 @@ export class CommitNode extends ExplorerNode {
public readonly branch?: GitBranch public readonly branch?: GitBranch
) { ) {
super(new GitUri(commit.uri, commit)); super(new GitUri(commit.uri, commit));
this.repoPath = commit.repoPath;
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1); const repoPath = this.repoPath;
const log = await this.git.getLogForRepo(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 [];
const children = [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))]; let children: IFileExplorerNode[] = [
...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))
];
if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'),
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact);
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
children = await root.getChildren() as IFileExplorerNode[];
}
else {
children.sort((a, b) => a.label!.localeCompare(b.label!)); children.sort((a, b) => a.label!.localeCompare(b.label!));
}
return children; return children;
} }

View File

@@ -10,6 +10,7 @@ export declare type ResourceType =
'gitlens:commit' | 'gitlens:commit' |
'gitlens:commit-file' | 'gitlens:commit-file' |
'gitlens:file-history' | 'gitlens:file-history' |
'gitlens:folder' |
'gitlens:history' | 'gitlens:history' |
'gitlens:message' | 'gitlens:message' |
'gitlens:pager' | 'gitlens:pager' |

85
src/views/folderNode.ts Normal file
View File

@@ -0,0 +1,85 @@
'use strict';
import { Arrays, Objects } from '../system';
import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { GitExplorerFilesLayout, IGitExplorerConfig } from '../configuration';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitUri } from '../gitService';
export interface IFileExplorerNode extends ExplorerNode {
folderName: string;
label?: string;
priority: boolean;
relativePath?: string;
root?: Arrays.IHierarchicalItem<IFileExplorerNode>;
}
export class FolderNode extends ExplorerNode {
readonly priority: boolean = true;
readonly resourceType: ResourceType = 'gitlens:folder';
constructor(
public readonly repoPath: string,
public folderName: string,
public relativePath: string | undefined,
public readonly root: Arrays.IHierarchicalItem<IFileExplorerNode>,
private readonly config: IGitExplorerConfig
) {
super(new GitUri(Uri.file(repoPath), { repoPath: repoPath, fileName: repoPath }));
}
async getChildren(): Promise<(FolderNode | IFileExplorerNode)[]> {
if (this.root.descendants === undefined || this.root.children === undefined) return [];
let children: (FolderNode | IFileExplorerNode)[];
const nesting = FolderNode.getFileNesting(this.config, this.root.descendants, this.relativePath === undefined);
if (nesting !== GitExplorerFilesLayout.List) {
children = [];
for (const folder of Objects.values(this.root.children)) {
if (folder.value === undefined) {
children.push(new FolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.config));
continue;
}
folder.value.relativePath = this.root.relativePath;
children.push(folder.value);
}
}
else {
this.root.descendants.forEach(n => n.relativePath = this.root.relativePath);
children = this.root.descendants;
}
children.sort((a, b) => {
return ((a instanceof FolderNode) ? -1 : 1) - ((b instanceof FolderNode) ? -1 : 1) ||
(a.priority ? -1 : 1) - (b.priority ? -1 : 1) ||
a.label!.localeCompare(b.label!);
});
return children;
}
async getTreeItem(): Promise<TreeItem> {
// TODO: Change this to expanded once https://github.com/Microsoft/vscode/issues/30918 is fixed
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
return item;
}
get label(): string {
return this.folderName;
}
static getFileNesting<T extends IFileExplorerNode>(config: IGitExplorerConfig, children: T[], isRoot: boolean): GitExplorerFilesLayout {
const nesting = config.files.layout || GitExplorerFilesLayout.Auto;
if (nesting === GitExplorerFilesLayout.Auto) {
if (isRoot || config.files.compact) {
const nestingThreshold = config.files.threshold || 5;
if (children.length <= nestingThreshold) return GitExplorerFilesLayout.List;
}
return GitExplorerFilesLayout.Tree;
}
return nesting;
}
}

View File

@@ -3,7 +3,7 @@ import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } fr
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, StatusFileFormatter } from '../gitService'; import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
import * as path from 'path'; import * as path from 'path';
export class StatusFileCommitsNode extends ExplorerNode { export class StatusFileCommitsNode extends ExplorerNode {
@@ -11,7 +11,7 @@ export class StatusFileCommitsNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status-file-commits'; readonly resourceType: ResourceType = 'gitlens:status-file-commits';
constructor( constructor(
repoPath: string, public readonly repoPath: string,
public readonly status: IGitStatusFile, public readonly status: IGitStatusFile,
public commits: GitLogCommit[], public commits: GitLogCommit[],
protected readonly context: ExtensionContext, protected readonly context: ExtensionContext,
@@ -47,10 +47,20 @@ export class StatusFileCommitsNode extends ExplorerNode {
return item; return item;
} }
private _folderName: string | undefined;
get folderName() {
if (this._folderName === undefined) {
this._folderName = path.dirname(this.uri.getRelativePath());
}
return this._folderName;
}
private _label: string | undefined; private _label: string | undefined;
get label() { get label() {
if (this._label === undefined) { if (this._label === undefined) {
this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat, { ...this.status, commit: this.commit } as IGitStatusFileWithCommit); this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat,
{ ...this.status, commit: this.commit } as IGitStatusFileWithCommit,
{ relativePath: this.relativePath } as IStatusFormatOptions);
} }
return this._label; return this._label;
} }
@@ -59,12 +69,25 @@ export class StatusFileCommitsNode extends ExplorerNode {
return this.commits[0]; return this.commits[0];
} }
get priority(): boolean {
return this.commit.isUncommitted;
}
private _relativePath: string | undefined;
get relativePath(): string | undefined {
return this._relativePath;
}
set relativePath(value: string | undefined) {
this._relativePath = value;
this._label = undefined;
}
getCommand(): Command | undefined { getCommand(): Command | undefined {
return { return {
title: 'Compare File with Previous Revision', title: 'Compare File with Previous Revision',
command: Commands.DiffWithPrevious, command: Commands.DiffWithPrevious,
arguments: [ arguments: [
GitUri.fromFileStatus(this.status, this.uri.repoPath!), GitUri.fromFileStatus(this.status, this.repoPath),
{ {
commit: this.commit, commit: this.commit,
line: 0, line: 0,

View File

@@ -1,12 +1,16 @@
'use strict'; 'use strict';
import { Arrays, Iterables, Objects } from '../system'; import { Arrays, Iterables, Objects } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { GitExplorerFilesLayout } from '../configuration';
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { FolderNode, IFileExplorerNode } from './folderNode';
import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService'; import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService';
import { StatusFileCommitsNode } from './statusFileCommitsNode'; import { StatusFileCommitsNode } from './statusFileCommitsNode';
import * as path from 'path';
export class StatusFilesNode extends ExplorerNode { export class StatusFilesNode extends ExplorerNode {
readonly repoPath: string;
readonly resourceType: ResourceType = 'gitlens:status-files'; readonly resourceType: ResourceType = 'gitlens:status-files';
maxCount: number | undefined = undefined; maxCount: number | undefined = undefined;
@@ -19,14 +23,17 @@ export class StatusFilesNode extends ExplorerNode {
public readonly branch?: GitBranch public readonly branch?: GitBranch
) { ) {
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
this.repoPath = status.repoPath;
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
let statuses: IGitStatusFileWithCommit[] = []; let statuses: IGitStatusFileWithCommit[] = [];
const repoPath = this.repoPath;
let log: GitLog | undefined; let log: GitLog | undefined;
if (this.range !== undefined) { if (this.range !== undefined) {
log = await this.git.getLogForRepo(this.status.repoPath, this.range, this.maxCount); log = await this.git.getLogForRepo(repoPath, this.range, this.maxCount);
if (log !== undefined) { if (log !== undefined) {
statuses = Array.from(Iterables.flatMap(log.commits.values(), c => { statuses = Array.from(Iterables.flatMap(log.commits.values(), c => {
return c.fileStatuses.map(s => { return c.fileStatuses.map(s => {
@@ -38,22 +45,33 @@ export class StatusFilesNode extends ExplorerNode {
if (this.status.files.length !== 0 && this.includeWorkingTree) { if (this.status.files.length !== 0 && this.includeWorkingTree) {
statuses.splice(0, 0, ...this.status.files.map(s => { statuses.splice(0, 0, ...this.status.files.map(s => {
return { ...s, commit: new GitLogCommit('file', this.status.repoPath, GitService.uncommittedSha, s.fileName, 'You', new Date(), '', s.status, [s], s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit; return {
...s,
commit: new GitLogCommit('file', repoPath, GitService.uncommittedSha, s.fileName, 'You', new Date(), '', s.status, [s], s.originalFileName, 'HEAD', s.fileName)
} as IGitStatusFileWithCommit;
})); }));
} }
statuses.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime()); statuses.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
const groups = Arrays.groupBy(statuses, s => s.fileName); const groups = Arrays.groupBy(statuses, s => s.fileName);
const children: (StatusFileCommitsNode | ShowAllNode)[] = [ let children: IFileExplorerNode[] = [
...Iterables.map(Objects.values<IGitStatusFileWithCommit[]>(groups), ...Iterables.map(Objects.values(groups), statuses => new StatusFileCommitsNode(repoPath, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch))
statuses => new StatusFileCommitsNode(this.uri.repoPath!, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch))
]; ];
children.sort((a: StatusFileCommitsNode, b: StatusFileCommitsNode) => (a.commit.isUncommitted ? -1 : 1) - (b.commit.isUncommitted ? -1 : 1) || a.label!.localeCompare(b.label!)); if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'),
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact);
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
children = await root.getChildren() as IFileExplorerNode[];
}
else {
children.sort((a, b) => (a.priority ? -1 : 1) - (b.priority ? -1 : 1) || a.label!.localeCompare(b.label!));
}
if (log !== undefined && log.truncated) { if (log !== undefined && log.truncated) {
children.push(new ShowAllNode('Show All Changes', this, this.context)); (children as (IFileExplorerNode | ShowAllNode)[]).push(new ShowAllNode('Show All Changes', this, this.context));
} }
return children; return children;
} }
@@ -62,7 +80,7 @@ export class StatusFilesNode extends ExplorerNode {
let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0; let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0;
if (this.status.upstream !== undefined) { if (this.status.upstream !== undefined) {
const stats = await this.git.getChangedFilesCount(this.status.repoPath, `${this.status.upstream}...`); const stats = await this.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`);
if (stats !== undefined) { if (stats !== undefined) {
files += stats.files; files += stats.files;
} }
@@ -82,5 +100,4 @@ export class StatusFilesNode extends ExplorerNode {
private get includeWorkingTree(): boolean { private get includeWorkingTree(): boolean {
return this.git.config.gitExplorer.includeWorkingTree; return this.git.config.gitExplorer.includeWorkingTree;
} }
} }