Browse Source

Overhauls commit search & adds it to Git commands

Closes #410 - adds ability to search by multiple criteria
Adds ability to match case, match all, & use regex
main
Eric Amodio 5 years ago
parent
commit
fbcb690135
34 changed files with 948 additions and 413 deletions
  1. +7
    -1
      CHANGELOG.md
  2. +4
    -0
      README.md
  3. +4
    -0
      images/dark/icon-eye-selected.svg
  4. +3
    -0
      images/dark/icon-eye.svg
  5. +4
    -0
      images/dark/icon-match-all-selected.svg
  6. +3
    -0
      images/dark/icon-match-all.svg
  7. +4
    -0
      images/dark/icon-match-case-selected.svg
  8. +3
    -0
      images/dark/icon-match-case.svg
  9. +4
    -0
      images/dark/icon-match-regex-selected.svg
  10. +3
    -0
      images/dark/icon-match-regex.svg
  11. +4
    -0
      images/light/icon-eye-selected.svg
  12. +3
    -0
      images/light/icon-eye.svg
  13. +4
    -0
      images/light/icon-match-all-selected.svg
  14. +3
    -0
      images/light/icon-match-all.svg
  15. +4
    -0
      images/light/icon-match-case-selected.svg
  16. +3
    -0
      images/light/icon-match-case.svg
  17. +4
    -0
      images/light/icon-match-regex-selected.svg
  18. +3
    -0
      images/light/icon-match-regex.svg
  19. +24
    -0
      package.json
  20. +497
    -0
      src/commands/git/search.ts
  21. +81
    -65
      src/commands/gitCommands.ts
  22. +3
    -2
      src/commands/quickCommand.ts
  23. +32
    -210
      src/commands/searchCommits.ts
  24. +8
    -4
      src/commands/showQuickCommitDetails.ts
  25. +6
    -0
      src/config.ts
  26. +132
    -68
      src/git/gitService.ts
  27. +10
    -7
      src/quickpicks/commonQuickPicks.ts
  28. +3
    -0
      src/system.ts
  29. +6
    -2
      src/views/nodes/commitNode.ts
  30. +3
    -2
      src/views/nodes/helpers.ts
  31. +7
    -3
      src/views/nodes/resultsCommitsNode.ts
  32. +25
    -23
      src/views/nodes/searchNode.ts
  33. +13
    -6
      src/views/nodes/searchResultsCommitsNode.ts
  34. +31
    -20
      src/views/searchView.ts

+ 7
- 1
CHANGELOG.md View File

@ -11,6 +11,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds all-new iconography to better match VS Code's new visual style — thanks to John Letey ([@johnletey](https://github.com/johnletey)) and Jon Beaumont-Pike ([@jonbp](https://github.com/jonbp)) for their help!
- Adds an all-new Welcome experience with a simple quick setup of common GitLens features — can be accessed via the _Welcome_ (`gitlens.showWelcomePage`) command
- Adds a new and improved interactive Settings editor experience — can be accessed via the _Open Settings_ (`gitlens.showSettingsPage`) command
- Adds an all-new commit search experience, via _Git Commands_ (`gitlens.gitCommands`) or _Search Commits_ (`gitlens.showCommitSearch`)
- Adds ability to match on more than one search pattern — closes [#410](https://github.com/eamodio/vscode-gitlens/issues/410)
- Adds case-\[in\]sensitive matching support — defaults to the new `gitlens.gitCommands.search.matchCase` setting
- Adds support for regular expression matching — defaults to the new `gitlens.gitCommands.search.matchRegex` setting
- Adds ability to match on all or any patterns when searching commit messages — defaults to the new `gitlens.gitCommands.search.matchAll` setting
- Adds ability to sort branches and tags in quick pick menus and views — closes [#745](https://github.com/eamodio/vscode-gitlens/issues/745)
- Adds a `gitlens.sortBranchesBy` setting to specify how branches are sorted in quick pick menus and views
- Adds a `gitlens.sortTagsBy` setting to specify how tags are sorted in quick pick menus and views
@ -23,11 +28,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Changed
- Improves the _Git Commands_ (`gitlens.gitCommands`) experience
- Dramatically improves the _Git Commands_ (`gitlens.gitCommands`) experience
- Adds a confirmation toggle (look for the checkmark icon in the upper right) to some of the Git commands
- Saves to the new `gitCommands.skipConfirmations` setting to specify which (and when) Git commands will skip the confirmation step
- Adds a new _reset_ Git command to reset current HEAD to a specified commit
- Adds a new _revert_ Git command to revert specific commits
- Adds a new _search_ Git command to search for specific commits
- Adds a new _stash_ Git command with sub-commands for _apply_, _drop_, _pop_, and _push_
- Adds a new _Fetch All & Prune_ option to the _fetch_ Git command
- Adds the last fetched on date to the confirmation step of the _fetch_ Git command (when a single repo is selected)

+ 4
- 0
README.md View File

@ -835,6 +835,10 @@ See also [View Settings](#view-settings- 'Jump to the View settings')
| Name | Description |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `gitlens.gitCommands.closeOnFocusOut` | Specifies whether to dismiss the Git Commands menu when focus is lost (if not, press `ESC` to dismiss) |
| `gitlens.gitCommands.search.matchAll` | Specifies whether to match all or any commit message search patterns |
| `gitlens.gitCommands.search.matchCase` | Specifies whether to match commit search patterns with or without regard to casing |
| `gitlens.gitCommands.search.matchRegex` | Specifies whether to match commit search patterns using regular expressions |
| `gitlens.gitCommands.search.showInView` | Specifies whether to show the results of a commit search in the _Search Commits_ view or directly within the quick pick menu |
| `gitlens.gitCommands.skipConfirmations` | Specifies which (and when) Git commands will skip the confirmation step, using the format: `git-command-name:(menu|command)` |
### Date & Time Settings [#](#date--time-settings- 'Date & Time Settings')

+ 4
- 0
images/dark/icon-eye-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#fff" fill-opacity=".1" stroke="#C5C5C5" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#C5C5C5" fill-rule="evenodd" d="M8 10.75c2.59 0 4.1-1.218 5.233-2.75C12.1 6.468 10.59 5.25 8 5.25S3.9 6.468 2.767 8C3.9 9.532 5.41 10.75 8 10.75zM14.75 8c-1.294 2-3.177 4-6.75 4s-5.456-2-6.75-4C2.544 6 4.427 4 8 4s5.456 2 6.75 4zM8 9.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/dark/icon-eye.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" fill-rule="evenodd" d="M8 10.75c2.59 0 4.1-1.218 5.233-2.75C12.1 6.468 10.59 5.25 8 5.25S3.9 6.468 2.767 8C3.9 9.532 5.41 10.75 8 10.75zM14.75 8c-1.294 2-3.177 4-6.75 4s-5.456-2-6.75-4C2.544 6 4.427 4 8 4s5.456 2 6.75 4zM8 9.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z" clip-rule="evenodd"/>
</svg>

+ 4
- 0
images/dark/icon-match-all-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#fff" fill-opacity=".1" stroke="#C5C5C5" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#C5C5C5" d="M5.811 4.914a1.748 1.748 0 0 0 .186.789c.065.128.148.26.248.395.1.131.22.27.36.418.227-.14.424-.27.59-.394.165-.124.302-.25.41-.377a1.248 1.248 0 0 0 .324-.855 1.01 1.01 0 0 0-.075-.395.909.909 0 0 0-.532-.52 1.143 1.143 0 0 0-.434-.078c-.332 0-.594.09-.787.27-.193.175-.29.424-.29.747zm.782 6.123c.223 0 .43-.022.619-.066a2.676 2.676 0 0 0 .978-.472c.139-.108.27-.223.393-.347l-2.24-2.434c-.2.132-.377.261-.532.389a2.244 2.244 0 0 0-.393.4 1.635 1.635 0 0 0-.237.473 1.912 1.912 0 0 0-.081.586c0 .215.032.415.098.598.07.18.168.335.295.467.127.127.284.227.469.299.185.071.395.107.63.107zM4 9.602c0-.311.039-.588.116-.831a2.27 2.27 0 0 1 .335-.664c.15-.2.332-.385.544-.556a7.35 7.35 0 0 1 .735-.508 13.39 13.39 0 0 1-.353-.443 3.563 3.563 0 0 1-.306-.49 3.282 3.282 0 0 1-.209-.557 2.438 2.438 0 0 1-.08-.64c0-.298.047-.565.144-.8.096-.24.235-.441.417-.605.18-.167.405-.293.67-.376.267-.088.57-.132.91-.132.32 0 .605.044.856.132a1.653 1.653 0 0 1 1.047.98c.093.236.14.503.14.802 0 .255-.049.492-.145.711a2.456 2.456 0 0 1-.388.604c-.162.184-.35.355-.561.515a9.266 9.266 0 0 1-.66.448l2.037 2.225a3.257 3.257 0 0 0 .503-.736c.066-.135.124-.281.174-.436.054-.156.1-.327.139-.515h1.065a5.77 5.77 0 0 1-.209.73 4.48 4.48 0 0 1-.272.622c-.1.195-.214.378-.341.55-.124.167-.26.333-.411.496L11.5 11.88h-1.302l-.972-1.028a7.43 7.43 0 0 1-.562.484c-.185.14-.382.26-.59.359A3.578 3.578 0 0 1 6.593 12a3.58 3.58 0 0 1-1.094-.155 2.271 2.271 0 0 1-.816-.467 2.11 2.11 0 0 1-.51-.753A2.817 2.817 0 0 1 4 9.602z"/>
</svg>

+ 3
- 0
images/dark/icon-match-all.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" d="M5.811 4.914a1.748 1.748 0 0 0 .186.789c.065.128.148.26.248.395.1.131.22.27.36.418.227-.14.424-.27.59-.394.165-.124.302-.25.41-.377a1.248 1.248 0 0 0 .324-.855 1.01 1.01 0 0 0-.075-.395.909.909 0 0 0-.532-.52 1.143 1.143 0 0 0-.434-.078c-.332 0-.594.09-.787.27-.193.175-.29.424-.29.747zm.782 6.123c.223 0 .43-.022.619-.066a2.676 2.676 0 0 0 .978-.472c.139-.108.27-.223.393-.347l-2.24-2.434c-.2.132-.377.261-.532.389a2.244 2.244 0 0 0-.393.4 1.635 1.635 0 0 0-.237.473 1.912 1.912 0 0 0-.081.586c0 .215.032.415.098.598.07.18.168.335.295.467.127.127.284.227.469.299.185.071.395.107.63.107zM4 9.602c0-.311.039-.588.116-.831a2.27 2.27 0 0 1 .335-.664c.15-.2.332-.385.544-.556a7.35 7.35 0 0 1 .735-.508 13.39 13.39 0 0 1-.353-.443 3.563 3.563 0 0 1-.306-.49 3.282 3.282 0 0 1-.209-.557 2.438 2.438 0 0 1-.08-.64c0-.298.047-.565.144-.8.096-.24.235-.441.417-.605.18-.167.405-.293.67-.376.267-.088.57-.132.91-.132.32 0 .605.044.856.132a1.653 1.653 0 0 1 1.047.98c.093.236.14.503.14.802 0 .255-.049.492-.145.711a2.456 2.456 0 0 1-.388.604c-.162.184-.35.355-.561.515a9.266 9.266 0 0 1-.66.448l2.037 2.225a3.257 3.257 0 0 0 .503-.736c.066-.135.124-.281.174-.436.054-.156.1-.327.139-.515h1.065a5.77 5.77 0 0 1-.209.73 4.48 4.48 0 0 1-.272.622c-.1.195-.214.378-.341.55-.124.167-.26.333-.411.496L11.5 11.88h-1.302l-.972-1.028a7.43 7.43 0 0 1-.562.484c-.185.14-.382.26-.59.359A3.578 3.578 0 0 1 6.593 12a3.58 3.58 0 0 1-1.094-.155 2.271 2.271 0 0 1-.816-.467 2.11 2.11 0 0 1-.51-.753A2.817 2.817 0 0 1 4 9.602z"/>
</svg>

+ 4
- 0
images/dark/icon-match-case-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#fff" fill-opacity=".1" stroke="#C5C5C5" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#C5C5C5" fill-rule="evenodd" d="M7.328 9.052l.864 2.35H9.25L6.108 3H5.12L2 11.402h1.062l.812-2.35h3.454zM5.695 4.453l.043.135 1.278 3.574h-2.83l1.268-3.574.043-.135.036-.156.031-.152.02-.126h.023l.023.126.028.152.037.156zm7.335 6.011v.936h.96V7.498c0-.719-.18-1.272-.539-1.661-.359-.389-.889-.583-1.588-.583-.199 0-.401.019-.606.056a4.875 4.875 0 0 0-1.078.326 2.081 2.081 0 0 0-.343.188v.984c.266-.23.566-.411.904-.54a2.927 2.927 0 0 1 1.052-.193c.188 0 .358.028.513.085a.98.98 0 0 1 .396.267c.109.121.193.279.252.472.059.193.088.427.088.7l-1.811.252c-.344.047-.64.126-.888.237a1.947 1.947 0 0 0-.615.419 1.6 1.6 0 0 0-.36.58 2.134 2.134 0 0 0-.117.721c0 .246.042.475.124.688.082.213.203.397.363.551.16.154.36.276.598.366.238.09.513.135.826.135.402 0 .76-.092 1.075-.278.315-.186.572-.454.771-.806h.023zm-2.128-1.743c.176-.064.401-.114.674-.149l1.465-.205v.609c0 .246-.041.475-.123.688a1.727 1.727 0 0 1-.343.557 1.573 1.573 0 0 1-.524.372 1.63 1.63 0 0 1-.668.135c-.187 0-.353-.025-.495-.076a1.03 1.03 0 0 1-.357-.211.896.896 0 0 1-.22-.316 1.005 1.005 0 0 1-.076-.393 1.6 1.6 0 0 1 .055-.44.739.739 0 0 1 .202-.334 1.16 1.16 0 0 1 .41-.237z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/dark/icon-match-case.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" fill-rule="evenodd" d="M7.328 9.052l.864 2.35H9.25L6.108 3H5.12L2 11.402h1.062l.812-2.35h3.454zM5.695 4.453l.043.135 1.278 3.574h-2.83l1.268-3.574.043-.135.036-.156.031-.152.02-.126h.023l.023.126.028.152.037.156zm7.335 6.011v.936h.96V7.498c0-.719-.18-1.272-.539-1.661-.359-.389-.889-.583-1.588-.583-.199 0-.401.019-.606.056a4.875 4.875 0 0 0-1.078.326 2.081 2.081 0 0 0-.343.188v.984c.266-.23.566-.411.904-.54a2.927 2.927 0 0 1 1.052-.193c.188 0 .358.028.513.085a.98.98 0 0 1 .396.267c.109.121.193.279.252.472.059.193.088.427.088.7l-1.811.252c-.344.047-.64.126-.888.237a1.947 1.947 0 0 0-.615.419 1.6 1.6 0 0 0-.36.58 2.134 2.134 0 0 0-.117.721c0 .246.042.475.124.688.082.213.203.397.363.551.16.154.36.276.598.366.238.09.513.135.826.135.402 0 .76-.092 1.075-.278.315-.186.572-.454.771-.806h.023zm-2.128-1.743c.176-.064.401-.114.674-.149l1.465-.205v.609c0 .246-.041.475-.123.688a1.727 1.727 0 0 1-.343.557 1.573 1.573 0 0 1-.524.372 1.63 1.63 0 0 1-.668.135c-.187 0-.353-.025-.495-.076a1.03 1.03 0 0 1-.357-.211.896.896 0 0 1-.22-.316 1.005 1.005 0 0 1-.076-.393 1.6 1.6 0 0 1 .055-.44.739.739 0 0 1 .202-.334 1.16 1.16 0 0 1 .41-.237z" clip-rule="evenodd"/>
</svg>

+ 4
- 0
images/dark/icon-match-regex-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#fff" fill-opacity=".1" stroke="#C5C5C5" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#C5C5C5" fill-rule="evenodd" d="M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/dark/icon-match-regex.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" fill-rule="evenodd" d="M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z" clip-rule="evenodd"/>
</svg>

+ 4
- 0
images/light/icon-eye-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#000" fill-opacity=".1" stroke="#424242" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#424242" fill-rule="evenodd" d="M8 10.75c2.59 0 4.1-1.218 5.233-2.75C12.1 6.468 10.59 5.25 8 5.25S3.9 6.468 2.767 8C3.9 9.532 5.41 10.75 8 10.75zM14.75 8c-1.294 2-3.177 4-6.75 4s-5.456-2-6.75-4C2.544 6 4.427 4 8 4s5.456 2 6.75 4zM8 9.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/light/icon-eye.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#424242" fill-rule="evenodd" d="M8 10.75c2.59 0 4.1-1.218 5.233-2.75C12.1 6.468 10.59 5.25 8 5.25S3.9 6.468 2.767 8C3.9 9.532 5.41 10.75 8 10.75zM14.75 8c-1.294 2-3.177 4-6.75 4s-5.456-2-6.75-4C2.544 6 4.427 4 8 4s5.456 2 6.75 4zM8 9.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z" clip-rule="evenodd"/>
</svg>

+ 4
- 0
images/light/icon-match-all-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#000" fill-opacity=".1" stroke="#424242" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#424242" d="M5.811 4.914a1.748 1.748 0 0 0 .186.789c.065.128.148.26.248.395.1.131.22.27.36.418.227-.14.424-.27.59-.394.165-.124.302-.25.41-.377a1.248 1.248 0 0 0 .324-.855 1.01 1.01 0 0 0-.075-.395.909.909 0 0 0-.532-.52 1.143 1.143 0 0 0-.434-.078c-.332 0-.594.09-.787.27-.193.175-.29.424-.29.747zm.782 6.123c.223 0 .43-.022.619-.066a2.676 2.676 0 0 0 .978-.472c.139-.108.27-.223.393-.347l-2.24-2.434c-.2.132-.377.261-.532.389a2.244 2.244 0 0 0-.393.4 1.635 1.635 0 0 0-.237.473 1.912 1.912 0 0 0-.081.586c0 .215.032.415.098.598.07.18.168.335.295.467.127.127.284.227.469.299.185.071.395.107.63.107zM4 9.602c0-.311.039-.588.116-.831a2.27 2.27 0 0 1 .335-.664c.15-.2.332-.385.544-.556a7.35 7.35 0 0 1 .735-.508 13.39 13.39 0 0 1-.353-.443 3.563 3.563 0 0 1-.306-.49 3.282 3.282 0 0 1-.209-.557 2.438 2.438 0 0 1-.08-.64c0-.298.047-.565.144-.8.096-.24.235-.441.417-.605.18-.167.405-.293.67-.376.267-.088.57-.132.91-.132.32 0 .605.044.856.132a1.653 1.653 0 0 1 1.047.98c.093.236.14.503.14.802 0 .255-.049.492-.145.711a2.456 2.456 0 0 1-.388.604c-.162.184-.35.355-.561.515a9.266 9.266 0 0 1-.66.448l2.037 2.225a3.257 3.257 0 0 0 .503-.736c.066-.135.124-.281.174-.436.054-.156.1-.327.139-.515h1.065a5.77 5.77 0 0 1-.209.73 4.48 4.48 0 0 1-.272.622c-.1.195-.214.378-.341.55-.124.167-.26.333-.411.496L11.5 11.88h-1.302l-.972-1.028a7.43 7.43 0 0 1-.562.484c-.185.14-.382.26-.59.359A3.578 3.578 0 0 1 6.593 12a3.58 3.58 0 0 1-1.094-.155 2.271 2.271 0 0 1-.816-.467 2.11 2.11 0 0 1-.51-.753A2.817 2.817 0 0 1 4 9.602z"/>
</svg>

+ 3
- 0
images/light/icon-match-all.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#424242" d="M5.811 4.914a1.748 1.748 0 0 0 .186.789c.065.128.148.26.248.395.1.131.22.27.36.418.227-.14.424-.27.59-.394.165-.124.302-.25.41-.377a1.248 1.248 0 0 0 .324-.855 1.01 1.01 0 0 0-.075-.395.909.909 0 0 0-.532-.52 1.143 1.143 0 0 0-.434-.078c-.332 0-.594.09-.787.27-.193.175-.29.424-.29.747zm.782 6.123c.223 0 .43-.022.619-.066a2.676 2.676 0 0 0 .978-.472c.139-.108.27-.223.393-.347l-2.24-2.434c-.2.132-.377.261-.532.389a2.244 2.244 0 0 0-.393.4 1.635 1.635 0 0 0-.237.473 1.912 1.912 0 0 0-.081.586c0 .215.032.415.098.598.07.18.168.335.295.467.127.127.284.227.469.299.185.071.395.107.63.107zM4 9.602c0-.311.039-.588.116-.831a2.27 2.27 0 0 1 .335-.664c.15-.2.332-.385.544-.556a7.35 7.35 0 0 1 .735-.508 13.39 13.39 0 0 1-.353-.443 3.563 3.563 0 0 1-.306-.49 3.282 3.282 0 0 1-.209-.557 2.438 2.438 0 0 1-.08-.64c0-.298.047-.565.144-.8.096-.24.235-.441.417-.605.18-.167.405-.293.67-.376.267-.088.57-.132.91-.132.32 0 .605.044.856.132a1.653 1.653 0 0 1 1.047.98c.093.236.14.503.14.802 0 .255-.049.492-.145.711a2.456 2.456 0 0 1-.388.604c-.162.184-.35.355-.561.515a9.266 9.266 0 0 1-.66.448l2.037 2.225a3.257 3.257 0 0 0 .503-.736c.066-.135.124-.281.174-.436.054-.156.1-.327.139-.515h1.065a5.77 5.77 0 0 1-.209.73 4.48 4.48 0 0 1-.272.622c-.1.195-.214.378-.341.55-.124.167-.26.333-.411.496L11.5 11.88h-1.302l-.972-1.028a7.43 7.43 0 0 1-.562.484c-.185.14-.382.26-.59.359A3.578 3.578 0 0 1 6.593 12a3.58 3.58 0 0 1-1.094-.155 2.271 2.271 0 0 1-.816-.467 2.11 2.11 0 0 1-.51-.753A2.817 2.817 0 0 1 4 9.602z"/>
</svg>

+ 4
- 0
images/light/icon-match-case-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#000" fill-opacity=".1" stroke="#424242" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#424242" fill-rule="evenodd" d="M7.328 9.052l.864 2.35H9.25L6.108 3H5.12L2 11.402h1.062l.812-2.35h3.454zM5.695 4.453l.043.135 1.278 3.574h-2.83l1.268-3.574.043-.135.036-.156.031-.152.02-.126h.023l.023.126.028.152.037.156zm7.335 6.011v.936h.96V7.498c0-.719-.18-1.272-.539-1.661-.359-.389-.889-.583-1.588-.583-.199 0-.401.019-.606.056a4.875 4.875 0 0 0-1.078.326 2.081 2.081 0 0 0-.343.188v.984c.266-.23.566-.411.904-.54a2.927 2.927 0 0 1 1.052-.193c.188 0 .358.028.513.085a.98.98 0 0 1 .396.267c.109.121.193.279.252.472.059.193.088.427.088.7l-1.811.252c-.344.047-.64.126-.888.237a1.947 1.947 0 0 0-.615.419 1.6 1.6 0 0 0-.36.58 2.134 2.134 0 0 0-.117.721c0 .246.042.475.124.688.082.213.203.397.363.551.16.154.36.276.598.366.238.09.513.135.826.135.402 0 .76-.092 1.075-.278.315-.186.572-.454.771-.806h.023zm-2.128-1.743c.176-.064.401-.114.674-.149l1.465-.205v.609c0 .246-.041.475-.123.688a1.727 1.727 0 0 1-.343.557 1.573 1.573 0 0 1-.524.372 1.63 1.63 0 0 1-.668.135c-.187 0-.353-.025-.495-.076a1.03 1.03 0 0 1-.357-.211.896.896 0 0 1-.22-.316 1.005 1.005 0 0 1-.076-.393 1.6 1.6 0 0 1 .055-.44.739.739 0 0 1 .202-.334 1.16 1.16 0 0 1 .41-.237z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/light/icon-match-case.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#424242" fill-rule="evenodd" d="M7.328 9.052l.864 2.35H9.25L6.108 3H5.12L2 11.402h1.062l.812-2.35h3.454zM5.695 4.453l.043.135 1.278 3.574h-2.83l1.268-3.574.043-.135.036-.156.031-.152.02-.126h.023l.023.126.028.152.037.156zm7.335 6.011v.936h.96V7.498c0-.719-.18-1.272-.539-1.661-.359-.389-.889-.583-1.588-.583-.199 0-.401.019-.606.056a4.875 4.875 0 0 0-1.078.326 2.081 2.081 0 0 0-.343.188v.984c.266-.23.566-.411.904-.54a2.927 2.927 0 0 1 1.052-.193c.188 0 .358.028.513.085a.98.98 0 0 1 .396.267c.109.121.193.279.252.472.059.193.088.427.088.7l-1.811.252c-.344.047-.64.126-.888.237a1.947 1.947 0 0 0-.615.419 1.6 1.6 0 0 0-.36.58 2.134 2.134 0 0 0-.117.721c0 .246.042.475.124.688.082.213.203.397.363.551.16.154.36.276.598.366.238.09.513.135.826.135.402 0 .76-.092 1.075-.278.315-.186.572-.454.771-.806h.023zm-2.128-1.743c.176-.064.401-.114.674-.149l1.465-.205v.609c0 .246-.041.475-.123.688a1.727 1.727 0 0 1-.343.557 1.573 1.573 0 0 1-.524.372 1.63 1.63 0 0 1-.668.135c-.187 0-.353-.025-.495-.076a1.03 1.03 0 0 1-.357-.211.896.896 0 0 1-.22-.316 1.005 1.005 0 0 1-.076-.393 1.6 1.6 0 0 1 .055-.44.739.739 0 0 1 .202-.334 1.16 1.16 0 0 1 .41-.237z" clip-rule="evenodd"/>
</svg>

+ 4
- 0
images/light/icon-match-regex-selected.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#000" fill-opacity=".1" stroke="#424242" stroke-width=".5" d="M.25.25h15.5v15.5H.25z"/>
<path fill="#424242" fill-rule="evenodd" d="M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/light/icon-match-regex.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#424242" fill-rule="evenodd" d="M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z" clip-rule="evenodd"/>
</svg>

+ 24
- 0
package.json View File

@ -487,6 +487,30 @@
"markdownDescription": "Specifies whether to dismiss the Git Commands menu when focus is lost (if not, press `ESC` to dismiss)",
"scope": "window"
},
"gitlens.gitCommands.search.matchAll": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to match all or any commit message search patterns",
"scope": "window"
},
"gitlens.gitCommands.search.matchCase": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to match commit search patterns with or without regard to casing",
"scope": "window"
},
"gitlens.gitCommands.search.matchRegex": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to match commit search patterns using regular expressions",
"scope": "window"
},
"gitlens.gitCommands.search.showInView": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show the results of a commit search in the _Search Commits_ view or directly within the quick pick menu",
"scope": "window"
},
"gitlens.gitCommands.skipConfirmations": {
"type": "array",
"default": [

+ 497
- 0
src/commands/git/search.ts View File

@ -0,0 +1,497 @@
'use strict';
/* eslint-disable no-loop-func */
import { QuickInputButton } from 'vscode';
import { Container } from '../../container';
import { GitLogCommit, GitService, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { QuickCommandBase, StepAsyncGenerator, StepSelection, StepState } from '../quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Iterables, Mutable, Strings } from '../../system';
import { Logger } from '../../logger';
import {
CommitQuickPickItem,
Directive,
DirectiveQuickPickItem,
QuickPickItemOfT
} from '../../quickpicks/gitQuickPicks';
interface State {
repo: Repository;
search: string;
matchAll: boolean;
matchCase: boolean;
matchRegex: boolean;
showInView: boolean;
}
export interface SearchGitCommandArgs {
readonly command: 'search';
state?: Partial<State>;
confirm?: boolean;
prefillOnly?: boolean;
}
const searchOperators = new Set<string>(['', 'author:', 'change:', 'commit:', 'file:']);
const searchOperatorToTitleMap = new Map<string, string>([
['', 'Search by Message'],
['author:', 'Search by Author or Committer'],
['change:', 'Search by Changes'],
['commit:', 'Search by Commit ID'],
['file:', 'Search by File']
]);
export class SearchGitCommand extends QuickCommandBase<State> {
constructor(args?: SearchGitCommandArgs) {
super('search', 'search', 'Search', {
description: 'aka grep, searches for commits'
});
if (args == null || args.state === undefined) return;
let counter = 0;
if (args.state.repo !== undefined) {
counter++;
}
if (args.state.search !== undefined && !args.prefillOnly) {
counter++;
}
this._initialState = {
counter: counter,
confirm: args.confirm,
...args.state
};
}
get canConfirm(): boolean {
return false;
}
isMatch(name: string) {
return super.isMatch(name) || name === 'grep';
}
protected async *steps(): StepAsyncGenerator {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
let pickedCommit: GitLogCommit | undefined;
const cfg = Container.config.gitCommands.search;
if (state.matchAll === undefined) {
state.matchAll = cfg.matchAll;
}
if (state.matchCase === undefined) {
state.matchCase = cfg.matchCase;
}
if (state.matchRegex === undefined) {
state.matchRegex = cfg.matchRegex;
}
if (state.showInView === undefined) {
state.showInView = cfg.showInView;
}
while (true) {
try {
if (state.repo === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repo = repos[0];
} else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createPickStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, r.id === (active && active.id), {
branch: true,
fetched: true,
status: true
})
)
)
});
const selection: StepSelection<typeof step> = yield step;
if (!this.canPickStepMoveNext(step, state, selection)) {
break;
}
state.repo = selection[0].item;
}
}
if (state.search === undefined || state.counter < 2) {
const items: QuickPickItemOfT<string>[] = [
{
label: `${this.title} by Message`,
description: `pattern ${GlyphChars.Dash} use quotes to search for phrases`,
item: ''
},
{
label: `${this.title} by Author or Committer`,
description: 'author: pattern',
item: 'author:'
},
{
label: `${this.title} by Commit ID`,
description: 'commit: sha',
item: 'commit:'
},
{
label: `${this.title} by Files`,
description: 'file: glob',
item: 'file:'
},
{
label: `${this.title} by Changes`,
description: 'change: pattern',
item: 'change:'
}
];
const titleSuffix = `${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`;
const matchCaseButton: Mutable<QuickInputButton> = {
iconPath: state.matchCase
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-case-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-case-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath('images/dark/icon-match-case.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-match-case.svg') as any
},
tooltip: 'Match Case'
};
const matchAllButton: Mutable<QuickInputButton> = {
iconPath: state.matchAll
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-all-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-all-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath('images/dark/icon-match-all.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-match-all.svg') as any
},
tooltip: 'Match All'
};
const matchRegexButton: Mutable<QuickInputButton> = {
iconPath: state.matchRegex
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-regex-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-regex-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath('images/dark/icon-match-regex.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-match-regex.svg') as any
},
tooltip: 'Match using Regular Expressions'
};
const showInViewButton: Mutable<QuickInputButton> = {
iconPath: state.showInView
? {
dark: Container.context.asAbsolutePath('images/dark/icon-eye-selected.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-eye-selected.svg') as any
}
: {
dark: Container.context.asAbsolutePath('images/dark/icon-eye.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-eye.svg') as any
},
tooltip: 'Show Results in the Search Commits View'
};
const step = this.createPickStep<QuickPickItemOfT<string>>({
title: `${this.title}${titleSuffix}`,
placeholder: 'e.g. "Updates dependencies" author:eamodio',
matchOnDescription: true,
matchOnDetail: true,
additionalButtons: [matchCaseButton, matchAllButton, matchRegexButton, showInViewButton],
items: items,
value: state.search,
onDidAccept: (quickpick): boolean => {
const pick = quickpick.selectedItems[0];
if (!searchOperators.has(pick.item)) return true;
const value = quickpick.value.trim();
if (value.length === 0 || searchOperators.has(value)) {
quickpick.value = pick.item;
} else {
quickpick.value = `${value} ${pick.item}`;
}
void step.onDidChangeValue!(quickpick);
return false;
},
onDidClickButton: (quickpick, button) => {
if (button === matchCaseButton) {
state.matchCase = !state.matchCase;
matchCaseButton.iconPath = state.matchCase
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-case-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-case-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-case.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-case.svg'
) as any
};
return;
}
if (button === matchAllButton) {
state.matchAll = !state.matchAll;
matchAllButton.iconPath = state.matchAll
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-all-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-all-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-all.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-all.svg'
) as any
};
return;
}
if (button === matchRegexButton) {
state.matchRegex = !state.matchRegex;
matchRegexButton.iconPath = state.matchRegex
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-regex-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-regex-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath(
'images/dark/icon-match-regex.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-match-regex.svg'
) as any
};
return;
}
if (button === showInViewButton) {
state.showInView = !state.showInView;
showInViewButton.iconPath = state.showInView
? {
dark: Container.context.asAbsolutePath(
'images/dark/icon-eye-selected.svg'
) as any,
light: Container.context.asAbsolutePath(
'images/light/icon-eye-selected.svg'
) as any
}
: {
dark: Container.context.asAbsolutePath('images/dark/icon-eye.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-eye.svg') as any
};
}
},
onDidChangeValue: (quickpick): boolean => {
const operations = GitService.parseSearchOperations(quickpick.value.trim());
quickpick.title =
operations.size === 0 || operations.size > 1
? `${this.title}${titleSuffix}`
: `${searchOperatorToTitleMap.get(operations.keys().next().value)!}${titleSuffix}`;
if (quickpick.value.length === 0) {
quickpick.items = items;
} else {
quickpick.items = [
{
label: 'Search for commits matching',
description: quickpick.value,
item: quickpick.value
}
];
}
return true;
}
});
const selection: StepSelection<typeof step> = yield step;
if (!this.canPickStepMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
state.search = selection[0].item.trim();
}
const resultsPromise = Container.git.getLogForSearch(state.repo.path, {
pattern: state.search,
matchAll: state.matchAll,
matchCase: state.matchCase,
matchRegex: state.matchRegex
});
if (state.showInView) {
void Container.searchView.search(
state.repo.path,
{
pattern: state.search,
matchAll: state.matchAll,
matchCase: state.matchCase,
matchRegex: state.matchRegex
},
{
label: { label: `commits matching: ${state.search}` }
},
resultsPromise
);
break;
}
const results = await resultsPromise;
const openInViewButton: QuickInputButton = {
iconPath: {
dark: Container.context.asAbsolutePath('images/dark/icon-link.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-link.svg') as any
},
tooltip: 'Open Results in the Search Commits View'
};
const step = this.createPickStep<CommitQuickPickItem>({
title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`,
placeholder:
results === undefined
? `No results for commits matching: ${state.search}`
: `${Strings.pluralize('result', results.count, {
number: results.truncated ? `${results.count}+` : undefined
})} for commits matching: ${state.search}`,
matchOnDescription: true,
matchOnDetail: true,
items:
results === undefined
? [
DirectiveQuickPickItem.create(Directive.Back, true),
DirectiveQuickPickItem.create(Directive.Cancel)
]
: [
...Iterables.map(results.commits.values(), commit =>
CommitQuickPickItem.create(
commit,
commit.ref === (pickedCommit && pickedCommit.ref),
{ compact: true, icon: true }
)
)
],
additionalButtons: [openInViewButton],
onDidClickButton: (quickpick, button) => {
if (button !== openInViewButton) return;
void Container.searchView.search(
state.repo!.path,
{
pattern: state.search!,
matchAll: state.matchAll,
matchCase: state.matchCase,
matchRegex: state.matchRegex
},
{
label: { label: `commits matching: ${state.search}` }
},
results
);
}
});
const selection: StepSelection<typeof step> = yield step;
if (!this.canPickStepMoveNext(step, state, selection)) {
continue;
}
state.counter--;
pickedCommit = selection[0].item;
void Container.searchView.search(
pickedCommit.repoPath,
{ pattern: `commit:${pickedCommit.sha}` },
{
label: { label: `commits matching: commit:${pickedCommit.shortSha}` }
}
);
// const gitCommandArgs: GitCommandsCommandArgs = {
// command: 'search',
// state: { ...state }
// };
// const commandArgs: ShowQuickCommitDetailsCommandArgs = {
// sha: commit.sha,
// commit: commit,
// goBackCommand: new CommandQuickPickItem(
// {
// label: 'Back',
// description: ''
// },
// Commands.GitCommands,
// [gitCommandArgs]
// )
// };
// void commands.executeCommand(Commands.ShowQuickCommitDetails, commit.toGitUri(), commandArgs);
// break;
} catch (ex) {
Logger.error(ex, this.title);
throw ex;
}
}
return undefined;
}
}

+ 81
- 65
src/commands/gitCommands.ts View File

@ -19,6 +19,7 @@ import { PushGitCommand, PushGitCommandArgs } from './git/push';
import { RebaseGitCommand, RebaseGitCommandArgs } from './git/rebase';
import { ResetGitCommand, ResetGitCommandArgs } from './git/reset';
import { RevertGitCommand, RevertGitCommandArgs } from './git/revert';
import { SearchGitCommand, SearchGitCommandArgs } from './git/search';
import { StashGitCommand, StashGitCommandArgs } from './git/stash';
import { SwitchGitCommand, SwitchGitCommandArgs } from './git/switch';
import { Container } from '../container';
@ -35,59 +36,10 @@ export type GitCommandsCommandArgs =
| RebaseGitCommandArgs
| ResetGitCommandArgs
| RevertGitCommandArgs
| SearchGitCommandArgs
| StashGitCommandArgs
| SwitchGitCommandArgs;
class PickCommandStep implements QuickPickStep {
readonly buttons = [];
readonly items: QuickCommandBase[];
readonly matchOnDescription = true;
readonly placeholder = 'Choose a git command';
readonly title = 'GitLens';
constructor(args?: GitCommandsCommandArgs) {
this.items = [
new CherryPickGitCommand(args && args.command === 'cherry-pick' ? args : undefined),
new MergeGitCommand(args && args.command === 'merge' ? args : undefined),
new FetchGitCommand(args && args.command === 'fetch' ? args : undefined),
new PullGitCommand(args && args.command === 'pull' ? args : undefined),
new PushGitCommand(args && args.command === 'push' ? args : undefined),
new RebaseGitCommand(args && args.command === 'rebase' ? args : undefined),
new ResetGitCommand(args && args.command === 'reset' ? args : undefined),
new RevertGitCommand(args && args.command === 'revert' ? args : undefined),
new StashGitCommand(args && args.command === 'stash' ? args : undefined),
new SwitchGitCommand(args && args.command === 'switch' ? args : undefined)
];
}
private _active: QuickCommandBase | undefined;
get command(): QuickCommandBase | undefined {
return this._active;
}
find(commandName: string, fuzzy: boolean = false) {
if (fuzzy) {
const cmd = commandName.toLowerCase();
return this.items.find(c => c.isMatch(cmd));
}
return this.items.find(c => c.key === commandName);
}
setCommand(value: QuickCommandBase | undefined, reason: 'menu' | 'command'): void {
if (this._active !== undefined) {
this._active.picked = false;
}
this._active = value;
if (this._active !== undefined) {
this._active.picked = true;
this._active.pickedVia = reason;
}
}
}
@command()
export class GitCommandsCommand extends Command {
private readonly GitQuickInputButtons = class {
@ -187,8 +139,7 @@ export class GitCommandsCommand extends Command {
return;
}
const step = commandsStep.command && commandsStep.command.value;
if (step !== undefined && isQuickInputStep(step) && step.onDidClickButton !== undefined) {
if (step.onDidClickButton !== undefined) {
step.onDidClickButton(input, e);
input.buttons = this.getButtons(step, commandsStep.command);
}
@ -266,13 +217,17 @@ export class GitCommandsCommand extends Command {
return;
}
const step = commandsStep.command && commandsStep.command.value;
if (step !== undefined && isQuickPickStep(step) && step.onDidClickButton !== undefined) {
if (step.onDidClickButton !== undefined) {
step.onDidClickButton(quickpick, e);
quickpick.buttons = this.getButtons(step, commandsStep.command);
}
}),
quickpick.onDidChangeValue(async e => {
if (step.onDidChangeValue !== undefined) {
const cancel = await step.onDidChangeValue(quickpick);
if (cancel) return;
}
if (!overrideItems) {
if (quickpick.canSelectMany && e === ' ') {
quickpick.value = '';
@ -294,9 +249,6 @@ export class GitCommandsCommand extends Command {
commandsStep.setCommand(command, this._pickedVia);
} else {
const step = commandsStep.command.value;
if (step === undefined || !isQuickPickStep(step)) return;
const cmd = quickpick.value.trim().toLowerCase();
const item = step.items.find(
i => i.label.replace(sanitizeLabel, '').toLowerCase() === cmd
@ -319,10 +271,7 @@ export class GitCommandsCommand extends Command {
e.trim().length !== 0 &&
(overrideItems || quickpick.activeItems.length === 0)
) {
const step = commandsStep.command.value;
if (step === undefined || !isQuickPickStep(step) || step.onValidateValue === undefined) {
return;
}
if (step.onValidateValue === undefined) return;
overrideItems = await step.onValidateValue(quickpick, e.trim(), step.items);
} else {
@ -362,10 +311,7 @@ export class GitCommandsCommand extends Command {
const value = quickpick.value.trim();
if (value.length === 0) return;
const step = commandsStep.command && commandsStep.command.value;
if (step === undefined || !isQuickPickStep(step) || step.onDidAccept === undefined) {
return;
}
if (step.onDidAccept === undefined) return;
quickpick.busy = true;
@ -406,6 +352,20 @@ export class GitCommandsCommand extends Command {
commandsStep.setCommand(command, this._pickedVia);
}
if (!quickpick.canSelectMany) {
if (step.onDidAccept !== undefined) {
quickpick.busy = true;
const next = await step.onDidAccept(quickpick);
quickpick.busy = false;
if (!next) {
return;
}
}
}
resolve(await this.nextStep(quickpick, commandsStep.command!, items as QuickPickItem[]));
})
);
@ -439,6 +399,11 @@ export class GitCommandsCommand extends Command {
}
quickpick.show();
// Call the step's change directly, because the quickpick doesn't seem to properly
if (step.value !== undefined && step.onDidChangeValue !== undefined) {
step.onDidChangeValue(quickpick);
}
});
} finally {
quickpick.dispose();
@ -508,3 +473,54 @@ export class GitCommandsCommand extends Command {
input.buttons = this.getButtons(command.value, command);
}
}
class PickCommandStep implements QuickPickStep {
readonly buttons = [];
readonly items: QuickCommandBase[];
readonly matchOnDescription = true;
readonly placeholder = 'Choose a git command';
readonly title = 'GitLens';
constructor(args?: GitCommandsCommandArgs) {
this.items = [
new CherryPickGitCommand(args && args.command === 'cherry-pick' ? args : undefined),
new MergeGitCommand(args && args.command === 'merge' ? args : undefined),
new FetchGitCommand(args && args.command === 'fetch' ? args : undefined),
new PullGitCommand(args && args.command === 'pull' ? args : undefined),
new PushGitCommand(args && args.command === 'push' ? args : undefined),
new RebaseGitCommand(args && args.command === 'rebase' ? args : undefined),
new ResetGitCommand(args && args.command === 'reset' ? args : undefined),
new RevertGitCommand(args && args.command === 'revert' ? args : undefined),
new SearchGitCommand(args && args.command === 'search' ? args : undefined),
new StashGitCommand(args && args.command === 'stash' ? args : undefined),
new SwitchGitCommand(args && args.command === 'switch' ? args : undefined)
];
}
private _active: QuickCommandBase | undefined;
get command(): QuickCommandBase | undefined {
return this._active;
}
find(commandName: string, fuzzy: boolean = false) {
if (fuzzy) {
const cmd = commandName.toLowerCase();
return this.items.find(c => c.isMatch(cmd));
}
return this.items.find(c => c.key === commandName);
}
setCommand(value: QuickCommandBase | undefined, reason: 'menu' | 'command'): void {
if (this._active !== undefined) {
this._active.picked = false;
}
this._active = value;
if (this._active !== undefined) {
this._active.picked = true;
this._active.pickedVia = reason;
}
}
}

+ 3
- 2
src/commands/quickCommand.ts View File

@ -38,9 +38,10 @@ export interface QuickPickStep {
title?: string;
value?: string;
onDidAccept?(quickpick: QuickPick<T>): Promise<boolean>;
onDidAccept?(quickpick: QuickPick<T>): boolean | Promise<boolean>;
onDidChangeValue?(quickpick: QuickPick<T>): boolean | Promise<boolean>;
onDidClickButton?(quickpick: QuickPick<T>, button: QuickInputButton): void;
onValidateValue?(quickpick: QuickPick<T>, value: string, items: T[]): Promise<boolean>;
onValidateValue?(quickpick: QuickPick<T>, value: string, items: T[]): boolean | Promise<boolean>;
validate?(selection: T[]): boolean;
}

+ 32
- 210
src/commands/searchCommits.ts View File

@ -1,53 +1,26 @@
'use strict';
import { commands, InputBoxOptions, TextEditor, Uri, window } from 'vscode';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { GitRepoSearchBy, GitService } from '../git/gitService';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem, CommitsQuickPick, ShowCommitSearchResultsInViewQuickPickItem } from '../quickpicks';
import { Iterables } from '../system';
import { commands } from 'vscode';
import { SearchResultsCommitsNode } from '../views/nodes';
import {
ActiveEditorCachedCommand,
command,
CommandContext,
Commands,
getCommandUri,
getRepoPathOrPrompt,
isCommandViewContextWithRepo
} from './common';
import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails';
const searchByRegex = /^([@~:#])/;
const symbolToSearchByMap = new Map<string, GitRepoSearchBy>([
['@', GitRepoSearchBy.Author],
['~', GitRepoSearchBy.Changes],
[':', GitRepoSearchBy.Files],
['#', GitRepoSearchBy.Sha]
]);
const searchByToSymbolMap = new Map<GitRepoSearchBy, string>([
[GitRepoSearchBy.Author, '@'],
[GitRepoSearchBy.Changes, '~'],
[GitRepoSearchBy.Files, ':'],
[GitRepoSearchBy.Sha, '#']
]);
import { Container } from '../container';
import { Command, command, CommandContext, Commands, isCommandViewContextWithRepo } from './common';
import { GitCommandsCommandArgs } from '../commands';
export interface SearchCommitsCommandArgs {
search?: string;
searchBy?: GitRepoSearchBy;
prefillOnly?: boolean;
search?: {
pattern?: string;
matchAll?: boolean;
matchCase?: boolean;
matchRegex?: boolean;
};
repoPath?: string;
showInView?: boolean;
goBackCommand?: CommandQuickPickItem;
prefillOnly?: boolean;
showInView?: boolean;
}
@command()
export class SearchCommitsCommand extends ActiveEditorCachedCommand {
private _lastSearch: string | undefined;
export class SearchCommitsCommand extends Command {
constructor() {
super([Commands.SearchCommits, Commands.SearchCommitsInView]);
}
@ -58,191 +31,40 @@ export class SearchCommitsCommand extends ActiveEditorCachedCommand {
args.showInView = true;
if (context.node instanceof SearchResultsCommitsNode) {
args.repoPath = context.node.repoPath;
args.search = context.node.search;
args.searchBy = context.node.searchBy;
args.prefillOnly = true;
}
if (isCommandViewContextWithRepo(context)) {
args.repoPath = context.node.repo.path;
return this.execute(context.editor, context.node.uri, args);
}
} else if (context.command === Commands.SearchCommitsInView) {
args = { ...args };
args.showInView = true;
} else {
// TODO: Add a user setting (default to view?)
}
return this.execute(context.editor, context.uri, args);
return this.execute(args);
}
async execute(editor?: TextEditor, uri?: Uri, args: SearchCommitsCommandArgs = {}) {
uri = getCommandUri(uri, editor);
const repoPath =
args.repoPath ||
(await getRepoPathOrPrompt(
`Search for commits in which repository${GlyphChars.Ellipsis}`,
args.goBackCommand
));
if (!repoPath) return undefined;
args = { ...args };
const originalArgs = { ...args };
if (args.prefillOnly && args.search && args.searchBy) {
args.search = `${searchByToSymbolMap.get(args.searchBy) || ''}${args.search}`;
args.searchBy = undefined;
}
if (!args.search || args.searchBy == null) {
let selection: [number, number] | undefined;
if (!args.search) {
if (args.searchBy != null) {
args.search = searchByToSymbolMap.get(args.searchBy);
selection = [1, 1];
} else {
args.search = this._lastSearch;
}
}
if (args.showInView) {
await Container.searchView.show();
}
const repo = await Container.git.getRepository(repoPath);
const opts: InputBoxOptions = {
value: args.search,
prompt: 'Please enter a search string',
placeHolder: `Search${
repo === undefined ? '' : ` ${repo.formattedName}`
} for commits by message, author (@<pattern>), files (:<path/glob>), commit id (#<sha>), or changes (~<pattern>)`,
valueSelection: selection
};
args.search = await window.showInputBox(opts);
if (args.search === undefined) {
return args.goBackCommand === undefined ? undefined : args.goBackCommand.execute();
}
this._lastSearch = originalArgs.search = args.search;
const match = searchByRegex.exec(args.search);
if (match && match[1]) {
args.searchBy = symbolToSearchByMap.get(match[1]);
args.search = args.search.substring(args.search[1] === ' ' ? 2 : 1);
} else if (GitService.isShaLike(args.search)) {
args.searchBy = GitRepoSearchBy.Sha;
} else {
args.searchBy = GitRepoSearchBy.Message;
}
}
if (args.searchBy === undefined) {
args.searchBy = GitRepoSearchBy.Message;
}
let searchLabel: string | undefined = undefined;
switch (args.searchBy) {
case GitRepoSearchBy.Author:
searchLabel = `commits with an author matching '${args.search}'`;
break;
case GitRepoSearchBy.Changes:
searchLabel = `commits with changes matching '${args.search}'`;
break;
case GitRepoSearchBy.Files:
searchLabel = `commits with files matching '${args.search}'`;
break;
case GitRepoSearchBy.Message:
searchLabel = args.search ? `commits with a message matching '${args.search}'` : 'all commits';
break;
case GitRepoSearchBy.Sha:
searchLabel = `commits with an id matching '${args.search}'`;
break;
async execute(args: SearchCommitsCommandArgs = {}) {
let repo;
if (args.repoPath !== undefined) {
repo = await Container.git.getRepository(args.repoPath);
}
if (args.showInView) {
void Container.searchView.search(repoPath, args.search, args.searchBy, {
label: { label: searchLabel! }
});
return undefined;
}
const progressCancellation = CommitsQuickPick.showProgress(searchLabel!);
try {
const log = await Container.git.getLogForSearch(repoPath, args.search, args.searchBy);
if (progressCancellation.token.isCancellationRequested) return undefined;
let goBackCommand: CommandQuickPickItem | undefined =
args.goBackCommand ||
new CommandQuickPickItem(
{
label: `go back ${GlyphChars.ArrowBack}`,
description: 'to commit search'
},
Commands.SearchCommits,
[uri, originalArgs]
);
let commit;
if (args.searchBy !== GitRepoSearchBy.Sha || log === undefined || log.count !== 1) {
const pick = await CommitsQuickPick.show(log, searchLabel!, progressCancellation, {
goBackCommand: goBackCommand,
showAllCommand:
log !== undefined && log.truncated
? new CommandQuickPickItem(
{
label: '$(sync) Show All Commits',
description: 'this may take a while'
},
Commands.SearchCommits,
[uri, { ...args, maxCount: 0, goBackCommand: goBackCommand }]
)
: undefined,
showInViewCommand:
log !== undefined
? new ShowCommitSearchResultsInViewQuickPickItem(args.search, args.searchBy, log, {
label: searchLabel!
})
: undefined
});
if (pick === undefined) return undefined;
if (pick instanceof CommandQuickPickItem) return pick.execute();
commit = pick.item;
goBackCommand = undefined;
} else {
commit = Iterables.first(log.commits.values());
const gitCommandArgs: GitCommandsCommandArgs = {
command: 'search',
prefillOnly: args.prefillOnly,
state: {
repo: repo,
search: args.search && args.search.pattern,
matchAll: args.search && args.search.matchAll,
matchCase: args.search && args.search.matchCase,
matchRegex: args.search && args.search.matchRegex,
showInView: args.showInView
}
const commandArgs: ShowQuickCommitDetailsCommandArgs = {
sha: commit.sha,
commit: commit,
goBackCommand:
goBackCommand ||
new CommandQuickPickItem(
{
label: `go back ${GlyphChars.ArrowBack}`,
description: `to search for ${searchLabel}`
},
Commands.SearchCommits,
[uri, args]
)
};
return commands.executeCommand(Commands.ShowQuickCommitDetails, commit.toGitUri(), commandArgs);
} catch (ex) {
Logger.error(ex, 'ShowCommitSearchCommand');
return Messages.showGenericErrorMessage('Unable to find commits');
} finally {
progressCancellation.cancel();
}
};
return commands.executeCommand(Commands.GitCommands, gitCommandArgs);
}
}

+ 8
- 4
src/commands/showQuickCommitDetails.ts View File

@ -2,7 +2,7 @@
import { commands, TextEditor, Uri } from 'vscode';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { GitCommit, GitLog, GitLogCommit, GitRepoSearchBy, GitUri } from '../git/gitService';
import { GitCommit, GitLog, GitLogCommit, GitUri } from '../git/gitService';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem, CommitQuickPick, CommitWithFileStatusQuickPickItem } from '../quickpicks';
@ -120,9 +120,13 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
}
if (args.showInView) {
void (await Container.searchView.search(repoPath!, args.commit.sha, GitRepoSearchBy.Sha, {
label: { label: `commits with an id matching '${args.commit.shortSha}'` }
}));
void (await Container.searchView.search(
repoPath!,
{ pattern: `commit: ${args.commit.sha}` },
{
label: { label: `commits matching: commit:${args.commit.shortSha}` }
}
));
return undefined;
}

+ 6
- 0
src/config.ts View File

@ -34,6 +34,12 @@ export interface Config {
defaultGravatarsStyle: GravatarDefaultStyle;
gitCommands: {
closeOnFocusOut: boolean;
search: {
matchAll: boolean;
matchCase: boolean;
matchRegex: boolean;
showInView: boolean;
};
skipConfirmations: string[];
};
heatmap: {

+ 132
- 68
src/git/gitService.ts View File

@ -86,14 +86,9 @@ const RepoSearchWarnings = {
const userConfigRegex = /^user\.(name|email) (.*)$/gm;
const mappedAuthorRegex = /(.+)\s<(.+)>/;
export enum GitRepoSearchBy {
Author = 'author',
Changes = 'changes',
Files = 'files',
Message = 'message',
Sha = 'sha'
}
const searchMessageOperationRegex = /(?=(.*?)\s?(?:(?:author:|commit:|file:|change:)|$))/;
const searchMessageValuesRegex = /(?:^|\b)\s?(?:\s?"([^"]*)(?:"|$)|([^"\s]*))/g;
const searchOperationRegex = /((?:author|commit|file|change):)\s?(?=(.*?)\s?(?:(?:author:|commit:|file:|change:)|$))/g;
const emptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined);
const reflogCommands = ['merge', 'pull'];
@ -1429,68 +1424,86 @@ export class GitService implements Disposable {
@log()
async getLogForSearch(
repoPath: string,
search: string,
searchBy: GitRepoSearchBy,
search: {
pattern: string;
matchAll?: boolean;
matchCase?: boolean;
matchRegex?: boolean;
},
options: { maxCount?: number } = {}
): Promise<GitLog | undefined> {
let maxCount = options.maxCount == null ? Container.config.advanced.maxSearchItems || 0 : options.maxCount;
const similarityThreshold = Container.config.advanced.similarityThreshold;
let searchArgs: string[] | undefined = undefined;
switch (searchBy) {
case GitRepoSearchBy.Author:
searchArgs = [
'-m',
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
'--all',
'--full-history',
'-E',
'-i',
`--author=${search}`
];
break;
case GitRepoSearchBy.Changes:
searchArgs = [
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
'--all',
'--full-history',
'-E',
'-i',
`-G${search}`
];
break;
case GitRepoSearchBy.Files:
searchArgs = [
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
'--all',
'--full-history',
'-E',
'-i',
'--',
`${search}`
];
break;
case GitRepoSearchBy.Message:
searchArgs = [
'-m',
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
'--all',
'--full-history',
'-E',
'-i'
];
if (search) {
searchArgs.push(`--grep=${search}`);
}
break;
case GitRepoSearchBy.Sha:
searchArgs = ['-m', `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, search];
maxCount = 1;
break;
}
search = { matchAll: false, matchCase: false, matchRegex: true, ...search };
try {
const data = await Git.log__search(repoPath, searchArgs, { maxCount: maxCount });
let maxCount = options.maxCount == null ? Container.config.advanced.maxSearchItems || 0 : options.maxCount;
const similarityThreshold = Container.config.advanced.similarityThreshold;
const operations = GitService.parseSearchOperations(search.pattern);
const searchArgs = new Set<string>();
const files: string[] = [];
let op;
let values = operations.get('commit:');
if (values !== undefined) {
searchArgs.add('-m');
searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`);
searchArgs.add(values[0]);
maxCount = 1;
} else {
searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`);
searchArgs.add('--all');
searchArgs.add('--full-history');
searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings');
if (!search.matchCase) {
searchArgs.add('--regexp-ignore-case');
}
for ([op, values] of operations.entries()) {
switch (op) {
case 'author:':
searchArgs.add('-m');
for (const value of values) {
searchArgs.add(`--author=${value}`);
searchArgs.add(`--committer=${value}`);
}
break;
case 'change:':
for (const value of values) {
searchArgs.add(`-G=${value}`);
}
break;
case 'file:':
for (const value of values) {
files.push(value);
}
break;
case '':
searchArgs.add('-m');
if (search.matchAll) {
searchArgs.add('--all-match');
}
for (const value of values) {
searchArgs.add(`--grep=${value}`);
}
break;
}
}
}
const args = [...searchArgs.values(), '--'];
if (files.length !== 0) {
args.push(...files);
}
const data = await Git.log__search(repoPath, args, { maxCount: maxCount });
const log = GitLogParser.parse(
data,
GitCommitType.Log,
@ -1504,9 +1517,8 @@ export class GitService implements Disposable {
);
if (log !== undefined) {
const opts = { ...options };
log.query = (maxCount: number | undefined) =>
this.getLogForSearch(repoPath, search, searchBy, { ...opts, maxCount: maxCount });
this.getLogForSearch(repoPath, search, { maxCount: maxCount });
}
return log;
@ -2808,4 +2820,56 @@ export class GitService implements Disposable {
return Git.shortenSha(ref, options);
}
static parseSearchOperations(search: string): Map<string, string[]> {
const operations = new Map<string, string[]>();
let op;
let value;
let match = searchMessageOperationRegex.exec(search);
if (match != null && match[1] !== '') {
const messageSearch = match[1];
let quoted;
let unquoted;
do {
match = searchMessageValuesRegex.exec(messageSearch);
if (match == null) break;
[, quoted, unquoted] = match;
if (!quoted && !unquoted) {
searchMessageValuesRegex.lastIndex = 0;
break;
}
let values = operations.get('');
if (values === undefined) {
values = [quoted || unquoted];
operations.set('', values);
} else {
values.push(quoted || unquoted);
}
} while (match != null);
}
do {
match = searchOperationRegex.exec(search);
if (match == null) break;
[, op, value] = match;
if (op !== undefined) {
let values = operations.get(op);
if (values === undefined) {
values = [value];
operations.set(op, values);
} else {
values.push(value);
}
}
} while (match != null);
return operations;
}
}

+ 10
- 7
src/quickpicks/commonQuickPicks.ts View File

@ -3,7 +3,7 @@ import { CancellationTokenSource, commands, QuickPickItem, window } from 'vscode
import { Commands } from '../commands';
import { configuration } from '../configuration';
import { Container } from '../container';
import { GitLog, GitLogCommit, GitRepoSearchBy, GitUri } from '../git/gitService';
import { GitLog, GitLogCommit, GitUri } from '../git/gitService';
import { KeyMapping, Keys } from '../keyboard';
import { ReferencesQuickPick, ReferencesQuickPickItem } from './referencesQuickPick';
@ -109,16 +109,19 @@ export class ShowCommitInViewQuickPickItem extends CommandQuickPickItem {
}
async execute(): Promise<{} | undefined> {
return void (await Container.searchView.search(this.commit.repoPath, this.commit.sha, GitRepoSearchBy.Sha, {
label: { label: `commits with an id matching '${this.commit.shortSha}'` }
}));
return void (await Container.searchView.search(
this.commit.repoPath,
{ pattern: `commit:${this.commit.sha}` },
{
label: { label: `commits matching: commit:${this.commit.shortSha}` }
}
));
}
}
export class ShowCommitSearchResultsInViewQuickPickItem extends CommandQuickPickItem {
constructor(
public readonly search: string,
public readonly searchBy: GitRepoSearchBy,
public readonly search: { pattern: string; matchAll?: boolean; matchCase?: boolean; matchRegex?: boolean },
public readonly results: GitLog,
public readonly resultsLabel: string | { label: string; resultsType?: { singular: string; plural: string } },
item: QuickPickItem = {
@ -130,7 +133,7 @@ export class ShowCommitSearchResultsInViewQuickPickItem extends CommandQuickPick
}
execute(): Promise<{} | undefined> {
Container.searchView.showSearchResults(this.results.repoPath, this.search, this.searchBy, this.results, {
Container.searchView.showSearchResults(this.results.repoPath, this.search, this.results, {
label: this.resultsLabel
});
return Promise.resolve(undefined);

+ 3
- 0
src/system.ts View File

@ -1,5 +1,8 @@
'use strict';
export type PartialDeep<T> = T extends object ? { [K in keyof T]?: PartialDeep<T[K]> } : T;
export type PickPartialDeep<T, K extends keyof T> = Omit<Partial<T>, K> & { [P in K]?: Partial<T[P]> };
export type Mutable<T> = { -readonly [P in keyof T]-?: T[P] };
export type PickMutable<T, K extends keyof T> = Omit<T, K> & { -readonly [P in K]: T[P] };

+ 6
- 2
src/views/nodes/commitNode.ts View File

@ -18,7 +18,8 @@ export class CommitNode extends ViewRefNode {
parent: ViewNode,
public readonly commit: GitLogCommit,
public readonly branch?: GitBranch,
private readonly getBranchAndTagTips?: (sha: string) => string | undefined
private readonly getBranchAndTagTips?: (sha: string) => string | undefined,
private readonly _options: { expand?: boolean } = {}
) {
super(commit.toGitUri(), view, parent);
}
@ -62,7 +63,10 @@ export class CommitNode extends ViewRefNode {
truncateMessageAtNewLine: true
});
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
const item = new TreeItem(
label,
this._options.expand ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed
);
item.contextValue = ResourceType.Commit;
if (this.branch !== undefined && this.branch.current) {
item.contextValue += '+current';

+ 3
- 2
src/views/nodes/helpers.ts View File

@ -14,9 +14,10 @@ const markers: [number, string][] = [
export function* insertDateMarkers<T extends ViewNode & { commit: GitLogCommit }>(
iterable: Iterable<T>,
parent: ViewNode,
skip?: number
skip?: number,
{ show }: { show: boolean } = { show: true }
): Iterable<ViewNode> {
if (!parent.view.config.showRelativeDateMarkers) {
if (!parent.view.config.showRelativeDateMarkers || !show) {
return yield* iterable;
}

+ 7
- 3
src/views/nodes/resultsCommitsNode.ts View File

@ -44,14 +44,18 @@ export class ResultsCommitsNode extends ViewNode implements Pagea
const { log } = await this.getCommitsQueryResults();
if (log === undefined) return [];
const options = { expand: this._options.expand && log.count === 1 };
const getBranchAndTagTips = await Container.git.getBranchesAndTagsTipsFn(this.uri.repoPath);
const children = [
...insertDateMarkers(
Iterables.map(
log.commits.values(),
c => new CommitNode(this.view, this, c, undefined, getBranchAndTagTips)
c => new CommitNode(this.view, this, c, undefined, getBranchAndTagTips, options)
),
this
this,
undefined,
{ show: log.count > 1 }
)
];
@ -78,7 +82,7 @@ export class ResultsCommitsNode extends ViewNode implements Pagea
state =
log == null || log.count === 0
? TreeItemCollapsibleState.None
: this._options.expand
: this._options.expand || log.count === 1
? TreeItemCollapsibleState.Expanded
: TreeItemCollapsibleState.Collapsed;
} catch (ex) {

+ 25
- 23
src/views/nodes/searchNode.ts View File

@ -2,7 +2,6 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { SearchCommitsCommandArgs } from '../../commands';
import { GlyphChars } from '../../constants';
import { GitRepoSearchBy } from '../../git/gitService';
import { debug, gate, Iterables, log, Promises } from '../../system';
import { View } from '../viewBase';
import { CommandMessageNode, MessageNode } from './common';
@ -22,9 +21,12 @@ export class SearchNode extends ViewNode {
command: 'gitlens.showCommitSearch'
};
const getCommandArgs = (searchBy: GitRepoSearchBy): SearchCommitsCommandArgs => {
const getCommandArgs = (
search: '' | 'author:' | 'change:' | 'commit:' | 'file:'
): SearchCommitsCommandArgs => {
return {
searchBy: searchBy
search: { pattern: search },
prefillOnly: true
};
};
@ -34,55 +36,55 @@ export class SearchNode extends ViewNode {
this,
{
...command,
arguments: [this, getCommandArgs(GitRepoSearchBy.Message)]
arguments: [this, getCommandArgs('')]
},
'Search commits by message',
'message-pattern',
'Click to search commits by message'
'Search for commits with messages',
'pattern',
`Click to search for commits with matching messages ${GlyphChars.Dash} use quotes to search for phrases`
),
new CommandMessageNode(
this.view,
this,
{
...command,
arguments: [this, getCommandArgs(GitRepoSearchBy.Author)]
arguments: [this, getCommandArgs('author:')]
},
`${GlyphChars.Space.repeat(4)} or, by author`,
'@ author-pattern',
'Click to search commits by author'
`${GlyphChars.Space.repeat(4)} or, authors or committers`,
'author: pattern',
'Click to search for commits with matching authors or committers'
),
new CommandMessageNode(
this.view,
this,
{
...command,
arguments: [this, getCommandArgs(GitRepoSearchBy.Sha)]
arguments: [this, getCommandArgs('commit:')]
},
`${GlyphChars.Space.repeat(4)} or, by commit id`,
'# sha',
'Click to search commits by commit id'
`${GlyphChars.Space.repeat(4)} or, commit id`,
'commit: sha',
'Click to search for commits with matching commit id'
),
new CommandMessageNode(
this.view,
this,
{
...command,
arguments: [this, getCommandArgs(GitRepoSearchBy.Files)]
arguments: [this, getCommandArgs('file:')]
},
`${GlyphChars.Space.repeat(4)} or, by files`,
': file-path/glob',
'Click to search commits by files'
`${GlyphChars.Space.repeat(4)} or, files`,
'file: glob',
'Click to search for commits with matching files'
),
new CommandMessageNode(
this.view,
this,
{
...command,
arguments: [this, getCommandArgs(GitRepoSearchBy.Changes)]
arguments: [this, getCommandArgs('change:')]
},
`${GlyphChars.Space.repeat(4)} or, by changes`,
'~ pattern',
'Click to search commits by changes'
`${GlyphChars.Space.repeat(4)} or, changes`,
'change: pattern',
'Click to search for commits with matching changes'
)
];
}

+ 13
- 6
src/views/nodes/searchResultsCommitsNode.ts View File

@ -2,7 +2,6 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { SearchCommitsCommandArgs } from '../../commands';
import { Commands } from '../../commands/common';
import { GitRepoSearchBy } from '../../git/gitService';
import { ViewWithFiles } from '../viewBase';
import { CommitsQueryResults, ResultsCommitsNode } from './resultsCommitsNode';
import { ResourceType, ViewNode } from './viewNode';
@ -16,8 +15,12 @@ export class SearchResultsCommitsNode extends ResultsCommitsNode {
view: ViewWithFiles,
parent: ViewNode,
repoPath: string,
public readonly search: string,
public readonly searchBy: GitRepoSearchBy,
public readonly search: {
pattern: string;
matchAll?: boolean;
matchCase?: boolean;
matchRegex?: boolean;
},
label: string,
commitsQuery: (maxCount: number | undefined) => Promise<CommitsQueryResults>
) {
@ -30,7 +33,11 @@ export class SearchResultsCommitsNode extends ResultsCommitsNode {
}
get id(): string {
return `gitlens:repository(${this.repoPath}):search(${this.searchBy}:${this.search}):commits|${this._instanceId}`;
return `gitlens:repository(${this.repoPath}):search(${this.search && this.search.pattern}|${
this.search && this.search.matchAll ? 'A' : ''
}${this.search && this.search.matchCase ? 'C' : ''}${
this.search && this.search.matchRegex ? 'R' : ''
}):commits|${this._instanceId}`;
}
get type(): ResourceType {
@ -43,8 +50,8 @@ export class SearchResultsCommitsNode extends ResultsCommitsNode {
if (item.collapsibleState === TreeItemCollapsibleState.None) {
const args: SearchCommitsCommandArgs = {
search: this.search,
searchBy: this.searchBy,
prefillOnly: true
prefillOnly: true,
showInView: true
};
item.command = {
title: 'Search Commits',

+ 31
- 20
src/views/searchView.ts View File

@ -3,7 +3,7 @@ import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, SearchViewConfig, ViewFilesLayout, ViewsConfig } from '../configuration';
import { CommandContext, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
import { GitLog, GitRepoSearchBy } from '../git/gitService';
import { GitLog } from '../git/gitService';
import { Functions, Strings } from '../system';
import { nodeSupportsConditionalDismissal, SearchNode, SearchResultsCommitsNode, ViewNode } from './nodes';
import { ViewBase } from './viewBase';
@ -105,25 +105,31 @@ export class SearchView extends ViewBase {
async search(
repoPath: string,
search: string,
searchBy: GitRepoSearchBy,
options: {
maxCount?: number;
search: {
pattern: string;
matchAll?: boolean;
matchCase?: boolean;
matchRegex?: boolean;
},
{
label,
...options
}: {
label:
| string
| {
label: string;
resultsType?: { singular: string; plural: string };
};
}
maxCount?: number;
},
results?: Promise<GitLog | undefined> | GitLog
) {
await this.show();
const searchQueryFn = this.getSearchQueryFn(
Container.git.getLogForSearch(repoPath, search, searchBy, {
maxCount: options.maxCount
}),
options
results || Container.git.getLogForSearch(repoPath, search, options),
{ label: label }
);
return this.addResults(
@ -132,8 +138,7 @@ export class SearchView extends ViewBase {
this._root!,
repoPath,
search,
searchBy,
`results for ${typeof options.label === 'string' ? options.label : options.label.label}`,
`${typeof label === 'string' ? label : label.label}`,
searchQueryFn
)
);
@ -141,27 +146,33 @@ export class SearchView extends ViewBase {
showSearchResults(
repoPath: string,
search: string,
searchBy: GitRepoSearchBy,
search: {
pattern: string;
matchAll?: boolean;
matchCase?: boolean;
matchRegex?: boolean;
},
results: GitLog,
options: {
{
label,
...options
}: {
label:
| string
| {
label: string;
resultsType?: { singular: string; plural: string };
};
maxCount?: number;
}
) {
const label = this.getSearchLabel(options.label, results);
const searchQueryFn = Functions.cachedOnce(this.getSearchQueryFn(results, options), {
label = this.getSearchLabel(label, results);
const searchQueryFn = Functions.cachedOnce(this.getSearchQueryFn(results, { label: label, ...options }), {
label: label,
log: results
});
return this.addResults(
new SearchResultsCommitsNode(this, this._root!, repoPath, search, searchBy, label, searchQueryFn)
);
return this.addResults(new SearchResultsCommitsNode(this, this._root!, repoPath, search, label, searchQueryFn));
}
private addResults(results: ViewNode) {

Loading…
Cancel
Save