Преглед на файлове

Updates configuration usage for multi-root

Uses new ConfigurationChangeEvent arg
main
Eric Amodio преди 7 години
родител
ревизия
fe40f488dd
променени са 38 файла, в които са добавени 1086 реда и са изтрити 680 реда
  1. +173
    -88
      package.json
  2. +57
    -48
      src/annotations/annotationController.ts
  3. +4
    -4
      src/annotations/annotationProvider.ts
  4. +1
    -1
      src/annotations/blameAnnotationProvider.ts
  5. +1
    -1
      src/annotations/recentChangesAnnotationProvider.ts
  6. +35
    -36
      src/codeLensController.ts
  7. +243
    -28
      src/configuration.ts
  8. +29
    -27
      src/currentLineController.ts
  9. +3
    -3
      src/extension.ts
  10. +11
    -10
      src/git/git.ts
  11. +34
    -33
      src/git/gitContextTracker.ts
  12. +3
    -6
      src/git/models/remote.ts
  13. +62
    -2
      src/git/models/repository.ts
  14. +3
    -2
      src/git/parsers/remoteParser.ts
  15. +14
    -11
      src/git/remotes/custom.ts
  16. +15
    -35
      src/git/remotes/factory.ts
  17. +63
    -48
      src/gitCodeLensProvider.ts
  18. +10
    -10
      src/gitRevisionCodeLensProvider.ts
  19. +62
    -59
      src/gitService.ts
  20. +40
    -33
      src/logger.ts
  21. +17
    -0
      src/system/function.ts
  22. +10
    -10
      src/views/branchHistoryNode.ts
  23. +10
    -9
      src/views/branchesNode.ts
  24. +10
    -10
      src/views/commitFileNode.ts
  25. +12
    -12
      src/views/commitNode.ts
  26. +1
    -1
      src/views/fileHistoryNode.ts
  27. +4
    -3
      src/views/folderNode.ts
  28. +82
    -75
      src/views/gitExplorer.ts
  29. +7
    -6
      src/views/remoteNode.ts
  30. +9
    -8
      src/views/remotesNode.ts
  31. +3
    -3
      src/views/repositoryNode.ts
  32. +7
    -8
      src/views/stashFileNode.ts
  33. +8
    -8
      src/views/stashNode.ts
  34. +9
    -8
      src/views/stashesNode.ts
  35. +8
    -8
      src/views/statusFileCommitsNode.ts
  36. +13
    -13
      src/views/statusFilesNode.ts
  37. +3
    -3
      src/views/statusNode.ts
  38. +10
    -10
      src/views/statusUpstreamNode.ts

+ 173
- 88
package.json Целия файл

@ -7,7 +7,7 @@
},
"publisher": "eamodio",
"engines": {
"vscode": "^1.17.2"
"vscode": "^1.18.0"
},
"license": "SEE LICENSE IN LICENSE",
"displayName": "Git Lens \u2014 git blame annotations, code lens, and more",
@ -23,14 +23,15 @@
"Other"
],
"keywords": [
"git",
"gitlens",
"git",
"blame",
"log",
"annotation",
"diff",
"lens",
"history"
"history",
"multi-root ready"
],
"galleryBanner": {
"color": "#56098c",
@ -55,12 +56,14 @@
"gitlens.debug": {
"type": "boolean",
"default": false,
"description": "Specifies debug mode"
"description": "Specifies debug mode",
"scope": "window"
},
"gitlens.insiders": {
"type": "boolean",
"default": false,
"description": "Specifies whether or not to enable new experimental features (expect there to be issues)"
"description": "Specifies whether or not to enable new experimental features (expect there to be issues)",
"scope": "window"
},
"gitlens.outputLevel": {
"type": "string",
@ -70,27 +73,32 @@
"errors",
"verbose"
],
"description": "Specifies how much (if any) output will be sent to the GitLens output channel"
"description": "Specifies how much (if any) output will be sent to the GitLens output channel",
"scope": "window"
},
"gitlens.annotations.file.gutter.format": {
"type": "string",
"default": "${message|40?} ${ago|14-}",
"description": "Specifies the format of the gutter blame annotations\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.annotations.file.gutter.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting"
"description": "Specifies the format of the gutter blame annotations\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.annotations.file.gutter.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window"
},
"gitlens.annotations.file.gutter.dateFormat": {
"type": "string",
"default": null,
"description": "Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats"
"description": "Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats",
"scope": "window"
},
"gitlens.annotations.file.gutter.compact": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations"
"description": "Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations",
"scope": "window"
},
"gitlens.annotations.file.gutter.heatmap.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a heatmap indicator in the gutter blame annotations"
"description": "Specifies whether or not to provide a heatmap indicator in the gutter blame annotations",
"scope": "window"
},
"gitlens.annotations.file.gutter.heatmap.location": {
"type": "string",
@ -99,87 +107,104 @@
"left",
"right"
],
"description": "Specifies where the heatmap indicators will be shown in the gutter blame annotations\n `left` - adds a heatmap indicator on the left edge of the gutter blame annotations\n `right` - adds a heatmap indicator on the right edge of the gutter blame annotations"
"description": "Specifies where the heatmap indicators will be shown in the gutter blame annotations\n `left` - adds a heatmap indicator on the left edge of the gutter blame annotations\n `right` - adds a heatmap indicator on the right edge of the gutter blame annotations",
"scope": "window"
},
"gitlens.annotations.file.gutter.hover.details": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations"
"description": "Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations",
"scope": "window"
},
"gitlens.annotations.file.gutter.hover.changes": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation over the gutter blame annotations"
"description": "Specifies whether or not to provide a changes (diff) hover annotation over the gutter blame annotations",
"scope": "window"
},
"gitlens.annotations.file.gutter.hover.wholeLine": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to trigger hover annotations over the whole line"
"description": "Specifies whether or not to trigger hover annotations over the whole line",
"scope": "window"
},
"gitlens.annotations.file.hover.details": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a commit details hover annotation over each line"
"description": "Specifies whether or not to provide a commit details hover annotation over each line",
"scope": "window"
},
"gitlens.annotations.file.hover.changes": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation over each line"
"description": "Specifies whether or not to provide a changes (diff) hover annotation over each line",
"scope": "window"
},
"gitlens.annotations.file.hover.heatmap.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide heatmap indicators on the left edge of each line"
"description": "Specifies whether or not to provide heatmap indicators on the left edge of each line",
"scope": "window"
},
"gitlens.annotations.file.recentChanges.hover.details": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a commit details hover annotation"
"description": "Specifies whether or not to provide a commit details hover annotation",
"scope": "window"
},
"gitlens.annotations.file.recentChanges.hover.changes": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation"
"description": "Specifies whether or not to provide a changes (diff) hover annotation",
"scope": "window"
},
"gitlens.annotations.line.hover.details": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a commit details hover annotation for the current line"
"description": "Specifies whether or not to provide a commit details hover annotation for the current line",
"scope": "window"
},
"gitlens.annotations.line.hover.changes": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation for the current line"
"description": "Specifies whether or not to provide a changes (diff) hover annotation for the current line",
"scope": "window"
},
"gitlens.annotations.line.trailing.format": {
"type": "string",
"default": "${authorAgo} \u2022 ${message}",
"description": "Specifies the format of the trailing blame annotations\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.annotations.line.trailing.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting"
"description": "Specifies the format of the trailing blame annotations\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.annotations.line.trailing.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window"
},
"gitlens.annotations.line.trailing.dateFormat": {
"type": "string",
"default": null,
"description": "Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats"
"description": "Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats",
"scope": "window"
},
"gitlens.annotations.line.trailing.hover.details": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations"
"description": "Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations",
"scope": "window"
},
"gitlens.annotations.line.trailing.hover.changes": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations"
"description": "Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations",
"scope": "window"
},
"gitlens.annotations.line.trailing.hover.wholeLine": {
"type": "boolean",
"default": false,
"description": "Specifies whether or not to trigger hover annotations over the whole line"
"description": "Specifies whether or not to trigger hover annotations over the whole line",
"scope": "window"
},
"gitlens.blame.ignoreWhitespace": {
"type": "boolean",
"default": false,
"description": "Specifies whether or not to ignore whitespace when comparing revisions during blame operations"
"description": "Specifies whether or not to ignore whitespace when comparing revisions during blame operations",
"scope": "resource"
},
"gitlens.blame.file.annotationType": {
"type": "string",
@ -188,12 +213,14 @@
"gutter",
"hover"
],
"description": "Specifies the type of blame annotations that will be shown for the current file\n `gutter` - adds an annotation to the beginning of each line\n `hover` - shows annotations when hovering over each line"
"description": "Specifies the type of blame annotations that will be shown for the current file\n `gutter` - adds an annotation to the beginning of each line\n `hover` - shows annotations when hovering over each line",
"scope": "window"
},
"gitlens.blame.file.lineHighlight.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to highlight lines associated with the current line"
"description": "Specifies whether or not to highlight lines associated with the current line",
"scope": "window"
},
"gitlens.blame.file.lineHighlight.locations": {
"type": "array",
@ -213,12 +240,14 @@
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"description": "Specifies where the associated line highlights will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)"
"description": "Specifies where the associated line highlights will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)",
"scope": "window"
},
"gitlens.blame.line.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide a blame annotation for the current line, by default\nUse the `gitlens.toggleLineBlame` command to toggle the annotations on and off for the current session"
"description": "Specifies whether or not to provide a blame annotation for the current line, by default\nUse the `gitlens.toggleLineBlame` command to toggle the annotations on and off for the current session",
"scope": "window"
},
"gitlens.blame.line.annotationType": {
"type": "string",
@ -227,7 +256,8 @@
"trailing",
"hover"
],
"description": "Specifies the type of blame annotations that will be shown for the current line\n `trailing` - adds an annotation to the end of the current line\n `hover` - shows annotations when hovering over the current line"
"description": "Specifies the type of blame annotations that will be shown for the current line\n `trailing` - adds an annotation to the end of the current line\n `hover` - shows annotations when hovering over the current line",
"scope": "window"
},
"gitlens.recentChanges.file.lineHighlight.locations": {
"type": "array",
@ -247,17 +277,20 @@
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"description": "Specifies where the highlights of the recently changed lines will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)"
"description": "Specifies where the highlights of the recently changed lines will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)",
"scope": "window"
},
"gitlens.codeLens.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide any Git code lens, by default\nUse the `gitlens.toggleCodeLens` command to toggle the Git code lens on and off for the current session"
"description": "Specifies whether or not to provide any Git code lens, by default\nUse the `gitlens.toggleCodeLens` command to toggle the Git code lens on and off for the current session",
"scope": "window"
},
"gitlens.codeLens.recentChange.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block"
"description": "Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block",
"scope": "window"
},
"gitlens.codeLens.recentChange.command": {
"type": "string",
@ -270,12 +303,14 @@
"gitlens.showQuickFileHistory",
"gitlens.showQuickRepoHistory"
],
"description": "Specifies the command to be executed when the `recent change` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick"
"description": "Specifies the command to be executed when the `recent change` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick",
"scope": "window"
},
"gitlens.codeLens.authors.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)"
"description": "Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)",
"scope": "window"
},
"gitlens.codeLens.authors.command": {
"type": "string",
@ -288,7 +323,8 @@
"gitlens.showQuickFileHistory",
"gitlens.showQuickRepoHistory"
],
"description": "Specifies the command to be executed when the `authors` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick"
"description": "Specifies the command to be executed when the `authors` code lens is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current committed file with the previous commit\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick",
"scope": "window"
},
"gitlens.codeLens.locations": {
"type": "array",
@ -307,7 +343,8 @@
"minItems": 1,
"maxItems": 4,
"uniqueItems": true,
"description": "Specifies where Git code lens will be shown in the document\n `document` - adds code lens at the top of the document\n `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc)\n `blocks` - adds code lens at the start of block-like symbols (functions, methods, etc) lines"
"description": "Specifies where Git code lens will be shown in the document\n `document` - adds code lens at the top of the document\n `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc)\n `blocks` - adds code lens at the start of block-like symbols (functions, methods, etc) lines",
"scope": "resource"
},
"gitlens.codeLens.customLocationSymbols": {
"type": "array",
@ -315,7 +352,8 @@
"type": "string"
},
"uniqueItems": true,
"description": "Specifies a set of document symbols where Git code lens will or will not be shown in the document\nPrefix with `!` to not show Git code lens for the symbol\nMust be a member of `SymbolKind`"
"description": "Specifies a set of document symbols where Git code lens will or will not be shown in the document\nPrefix with `!` to not show Git code lens for the symbol\nMust be a member of `SymbolKind`",
"scope": "resource"
},
"gitlens.codeLens.perLanguageLocations": {
"type": "array",
@ -405,37 +443,44 @@
}
},
"uniqueItems": true,
"description": "Specifies where Git code lens will be shown in the document for the specified languages"
"description": "Specifies where Git code lens will be shown in the document for the specified languages",
"scope": "resource"
},
"gitlens.codeLens.debug": {
"type": "boolean",
"default": false,
"description": "Specifies whether or not to show debug information in code lens"
"description": "Specifies whether or not to show debug information in code lens",
"scope": "window"
},
"gitlens.defaultDateFormat": {
"type": "string",
"default": null,
"description": "Specifies how all absolute dates will be formatted by default\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats"
"description": "Specifies how all absolute dates will be formatted by default\nSee https://momentjs.com/docs/#/displaying/format/ for valid formats",
"scope": "window"
},
"gitlens.gitExplorer.autoRefresh": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to automatically refresh the `GitLens` custom view when the repository or the file system changes"
"description": "Specifies whether or not to automatically refresh the `GitLens` custom view when the repository or the file system changes",
"scope": "window"
},
"gitlens.gitExplorer.commitFormat": {
"type": "string",
"default": "${message} \u00a0\u2022\u00a0 ${authorAgo} \u00a0 (${id})",
"description": "Specifies the format of committed changes in the `GitLens` custom view\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting"
"description": "Specifies the format of committed changes in the `GitLens` custom view\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window"
},
"gitlens.gitExplorer.commitFileFormat": {
"type": "string",
"default": "${filePath}",
"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"
"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",
"scope": "window"
},
"gitlens.gitExplorer.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to show the `GitLens` custom view"
"description": "Specifies whether or not to show the `GitLens` custom view",
"scope": "window"
},
"gitlens.gitExplorer.files.layout": {
"type": "string",
@ -445,42 +490,50 @@
"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"
"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",
"scope": "window"
},
"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`"
"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`",
"scope": "window"
},
"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`"
"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`",
"scope": "window"
},
"gitlens.gitExplorer.includeWorkingTree": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view"
"description": "Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view",
"scope": "window"
},
"gitlens.gitExplorer.showTrackingBranch": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view"
"description": "Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view",
"scope": "window"
},
"gitlens.gitExplorer.stashFormat": {
"type": "string",
"default": "${message}",
"description": "Specifies the format of stashed changes in the `GitLens` custom view\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting"
"description": "Specifies the format of stashed changes in the `GitLens` custom view\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window"
},
"gitlens.gitExplorer.stashFileFormat": {
"type": "string",
"default": "${filePath}",
"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"
"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",
"scope": "window"
},
"gitlens.gitExplorer.statusFileFormat": {
"type": "string",
"default": "${working}${filePath}",
"description": "Specifies the format of the status of a working or committed file in the `GitLens` custom view\nAvailable tokens\n ${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"
"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",
"scope": "window"
},
"gitlens.gitExplorer.view": {
"type": "string",
@ -490,7 +543,8 @@
"history",
"repository"
],
"description": "Specifies the starting view (mode) of the `GitLens` custom view\n `auto` - shows the last selected view, defaults to `repository`\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer"
"description": "Specifies the starting view (mode) of the `GitLens` custom view\n `auto` - shows the last selected view, defaults to `repository`\n `history` - shows the commit history of the active file\n `repository` - shows a repository explorer",
"scope": "window"
},
"gitlens.remotes": {
"type": "array",
@ -577,12 +631,14 @@
}
},
"uniqueItems": true,
"description": "Specifies user-defined remote (code-hosting) services or custom domains for built-in remote services"
"description": "Specifies user-defined remote (code-hosting) services or custom domains for built-in remote services",
"scope": "resource"
},
"gitlens.statusBar.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to provide blame information on the status bar"
"description": "Specifies whether or not to provide blame information on the status bar",
"scope": "window"
},
"gitlens.statusBar.alignment": {
"type": "string",
@ -591,7 +647,8 @@
"left",
"right"
],
"description": "Specifies the blame alignment in the status bar\n `left` - align to the left\n `right` - align to the right"
"description": "Specifies the blame alignment in the status bar\n `left` - align to the left\n `right` - align to the right",
"scope": "window"
},
"gitlens.statusBar.command": {
"type": "string",
@ -606,127 +663,152 @@
"gitlens.showQuickFileHistory",
"gitlens.showQuickRepoHistory"
],
"description": "Specifies the command to be executed when the blame status bar item is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current line commit with the previous\n `gitlens.diffWithWorking` - compares the current line commit with the working tree\n `gitlens.toggleCodeLens` - toggles Git code lens\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick"
"description": "Specifies the command to be executed when the blame status bar item is clicked\n `gitlens.toggleFileBlame` - toggles file blame annotations\n `gitlens.diffWithPrevious` - compares the current line commit with the previous\n `gitlens.diffWithWorking` - compares the current line commit with the working tree\n `gitlens.toggleCodeLens` - toggles Git code lens\n `gitlens.showQuickCommitDetails` - shows a commit details quick pick\n `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick\n `gitlens.showQuickFileHistory` - shows a file history quick pick\n `gitlens.showQuickRepoHistory` - shows a branch history quick pick",
"scope": "window"
},
"gitlens.statusBar.format": {
"type": "string",
"default": "${authorAgo}",
"description": "Specifies the format of the status bar blame information\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting"
"description": "Specifies the format of the status bar blame information\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)\n ${authorAgo} - commit author, relative commit date\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window"
},
"gitlens.statusBar.dateFormat": {
"type": "string",
"default": null,
"description": "Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats"
"description": "Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats",
"scope": "window"
},
"gitlens.strings.codeLens.unsavedChanges.recentChangeAndAuthors": {
"type": "string",
"default": "Cannot determine recent change or authors (unsaved changes)",
"description": "Specifies the string to be shown in place of both the `recent change` and `authors` code lens when there are unsaved changes"
"description": "Specifies the string to be shown in place of both the `recent change` and `authors` code lens when there are unsaved changes",
"scope": "window"
},
"gitlens.strings.codeLens.unsavedChanges.recentChangeOnly": {
"type": "string",
"default": "Cannot determine recent change (unsaved changes)",
"description": "Specifies the string to be shown in place of the `recent change` code lens when there are unsaved changes"
"description": "Specifies the string to be shown in place of the `recent change` code lens when there are unsaved changes",
"scope": "window"
},
"gitlens.strings.codeLens.unsavedChanges.authorsOnly": {
"type": "string",
"default": "Cannot determine authors (unsaved changes)",
"description": "Specifies the string to be shown in place of the `authors` code lens when there are unsaved changes"
"description": "Specifies the string to be shown in place of the `authors` code lens when there are unsaved changes",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.separateLines": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not gutter blame annotations will be separated by a small gap"
"description": "Specifies whether or not gutter blame annotations will be separated by a small gap",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.dark.backgroundColor": {
"type": "string",
"default": "rgba(255, 255, 255, 0.075)",
"description": "Specifies the dark theme background color of the gutter blame annotations. Must be a valid css color"
"description": "Specifies the dark theme background color of the gutter blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.light.backgroundColor": {
"type": "string",
"default": "rgba(0, 0, 0, 0.05)",
"description": "Specifies the light theme background color of the gutter blame annotations. Must be a valid css color"
"description": "Specifies the light theme background color of the gutter blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.dark.foregroundColor": {
"type": "string",
"default": "rgb(190, 190, 190)",
"description": "Specifies the dark theme foreground color of the gutter blame annotations. Must be a valid css color"
"description": "Specifies the dark theme foreground color of the gutter blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.light.foregroundColor": {
"type": "string",
"default": "rgb(116, 116, 116)",
"description": "Specifies the light theme foreground color of the gutter blame annotations. Must be a valid css color"
"description": "Specifies the light theme foreground color of the gutter blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.dark.uncommittedForegroundColor": {
"type": "string",
"default": "rgba(0, 188, 242, 0.6)",
"description": "Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color"
"description": "Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.file.gutter.light.uncommittedForegroundColor": {
"type": "string",
"default": "rgba(0, 188, 242, 0.6)",
"description": "Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color"
"description": "Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.line.trailing.dark.backgroundColor": {
"type": "string",
"default": null,
"description": "Specifies the dark theme background color of the trailing blame annotation. Must be a valid css color"
"description": "Specifies the dark theme background color of the trailing blame annotation. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.line.trailing.light.backgroundColor": {
"type": "string",
"default": null,
"description": "Specifies the light theme background color of the trailing blame annotation. Must be a valid css color"
"description": "Specifies the light theme background color of the trailing blame annotation. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.line.trailing.dark.foregroundColor": {
"type": "string",
"default": "rgba(153, 153, 153, 0.35)",
"description": "Specifies the dark theme foreground color of the trailing blame annotation. Must be a valid css color"
"description": "Specifies the dark theme foreground color of the trailing blame annotation. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.annotations.line.trailing.light.foregroundColor": {
"type": "string",
"default": "rgba(153, 153, 153, 0.35)",
"description": "Specifies the light theme foreground color of the trailing blame annotation. Must be a valid css color"
"description": "Specifies the light theme foreground color of the trailing blame annotation. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.lineHighlight.dark.backgroundColor": {
"type": "string",
"default": "rgba(0, 188, 242, 0.2)",
"description": "Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color"
"description": "Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.lineHighlight.light.backgroundColor": {
"type": "string",
"default": "rgba(0, 188, 242, 0.2)",
"description": "Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color"
"description": "Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.lineHighlight.dark.overviewRulerColor": {
"type": "string",
"default": "rgba(0, 188, 242, 0.6)",
"description": "Specifies the dark theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color"
"description": "Specifies the dark theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.theme.lineHighlight.light.overviewRulerColor": {
"type": "string",
"default": "rgba(0, 188, 242, 0.6)",
"description": "Specifies the light theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color"
"description": "Specifies the light theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color",
"scope": "window"
},
"gitlens.advanced.caching.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether git output will be cached"
"description": "Specifies whether git output will be cached",
"scope": "window"
},
"gitlens.advanced.caching.maxLines": {
"type": "number",
"default": 0,
"description": "Specifies the threshold for caching larger documents"
"description": "Specifies the threshold for caching larger documents",
"scope": "window"
},
"gitlens.advanced.git": {
"type": "string",
"default": null,
"description": "Specifies the git path to use"
"description": "Specifies the git path to use",
"scope": "window"
},
"gitlens.advanced.maxQuickHistory": {
"type": "number",
"default": 200,
"description": "Specifies the maximum number of QuickPick history entries to show"
"description": "Specifies the maximum number of QuickPick history entries to show",
"scope": "window"
},
"gitlens.advanced.menus": {
"type": "object",
@ -759,7 +841,6 @@
"remote": true
}
},
"description": "Specifies which commands will be added to which menus",
"properties": {
"editorContext": {
"type": "object",
@ -884,17 +965,21 @@
}
}
}
}
},
"description": "Specifies which commands will be added to which menus",
"scope": "window"
},
"gitlens.advanced.quickPick.closeOnFocusOut": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to close the QuickPick menu when focus is lost"
"description": "Specifies whether or not to close the QuickPick menu when focus is lost",
"scope": "window"
},
"gitlens.advanced.telemetry.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to enable GitLens telemetry (even if enabled still abides by the overall `telemetry.enableTelemetry` setting"
"description": "Specifies whether or not to enable GitLens telemetry (even if enabled still abides by the overall `telemetry.enableTelemetry` setting",
"scope": "window"
}
}
},

+ 57
- 48
src/annotations/annotationController.ts Целия файл

@ -1,9 +1,9 @@
'use strict';
import { Functions, Iterables, Objects } from '../system';
import { DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { Functions, Iterables } from '../system';
import { ConfigurationChangeEvent, DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider';
import { TextDocumentComparer } from '../comparers';
import { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration';
import { configuration, IConfig, LineHighlightLocations } from '../configuration';
import { CommandContext, isTextEditor, setCommandContext } from '../constants';
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
@ -53,7 +53,6 @@ export class AnnotationController extends Disposable {
private _annotationsDisposable: Disposable | undefined;
private _annotationProviders: Map<TextEditorCorrelationKey, AnnotationProviderBase> = new Map();
private _config: IConfig;
private _disposable: Disposable;
private _keyboardScope: KeyboardScope | undefined = undefined;
@ -64,11 +63,10 @@ export class AnnotationController extends Disposable {
) {
super(() => this.dispose());
this.onConfigurationChanged();
this._disposable = Disposable.from(
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)
configuration.onDidChange(this.onConfigurationChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
@ -81,46 +79,47 @@ export class AnnotationController extends Disposable {
this._disposable && this._disposable.dispose();
}
private onConfigurationChanged() {
let changed = false;
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
const cfgBlameHighlight = cfg.blame.file.lineHighlight;
const cfgChangesHighlight = cfg.recentChanges.file.lineHighlight;
const cfgTheme = cfg.theme.lineHighlight;
private onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (!Objects.areEquivalent(cfgBlameHighlight, this._config && this._config.blame.file.lineHighlight) ||
!Objects.areEquivalent(cfgChangesHighlight, this._config && this._config.recentChanges.file.lineHighlight) ||
!Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) {
changed = true;
let cfg: IConfig | undefined;
if (initializing ||
configuration.changed(e, configuration.name('blame')('file')('lineHighlight').value) ||
configuration.changed(e, configuration.name('theme')('lineHighlight').value)) {
Decorations.blameHighlight && Decorations.blameHighlight.dispose();
if (cfgBlameHighlight.enabled) {
if (cfg === undefined) {
cfg = configuration.get<IConfig>();
}
const cfgHighlight = cfg.blame.file.lineHighlight;
const cfgTheme = cfg.theme.lineHighlight;
if (cfgHighlight.enabled) {
Decorations.blameHighlight = window.createTextEditorDecorationType({
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
dark: {
backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || configuration.defaults.theme.lineHighlight.dark.backgroundColor
: undefined,
gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter)
gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/dark/highlight-gutter.svg')
: undefined,
overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || configuration.defaults.theme.lineHighlight.dark.overviewRulerColor
: undefined
},
light: {
backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || configuration.defaults.theme.lineHighlight.light.backgroundColor
: undefined,
gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter)
gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/light/highlight-gutter.svg')
: undefined,
overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || configuration.defaults.theme.lineHighlight.light.overviewRulerColor
: undefined
}
});
@ -128,48 +127,58 @@ export class AnnotationController extends Disposable {
else {
Decorations.blameHighlight = undefined;
}
}
if (initializing ||
configuration.changed(e, configuration.name('recentChanges')('file')('lineHighlight').value) ||
configuration.changed(e, configuration.name('theme')('lineHighlight').value)) {
Decorations.recentChangesHighlight && Decorations.recentChangesHighlight.dispose();
if (cfg === undefined) {
cfg = configuration.get<IConfig>();
}
const cfgHighlight = cfg.recentChanges.file.lineHighlight;
const cfgTheme = cfg.theme.lineHighlight;
Decorations.recentChangesHighlight = window.createTextEditorDecorationType({
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
dark: {
backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || configuration.defaults.theme.lineHighlight.dark.backgroundColor
: undefined,
gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter)
gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/dark/highlight-gutter.svg')
: undefined,
overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || configuration.defaults.theme.lineHighlight.dark.overviewRulerColor
: undefined
},
light: {
backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
backgroundColor: cfgHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || configuration.defaults.theme.lineHighlight.light.backgroundColor
: undefined,
gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter)
gutterIconPath: cfgHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/light/highlight-gutter.svg')
: undefined,
overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
overviewRulerColor: cfgHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || configuration.defaults.theme.lineHighlight.light.overviewRulerColor
: undefined
}
});
}
if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) ||
!Objects.areEquivalent(cfg.recentChanges.file, this._config && this._config.recentChanges.file) ||
!Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) ||
!Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) {
changed = true;
}
if (initializing) return;
this._config = cfg;
if (configuration.changed(e, configuration.name('blame')('file').value) ||
configuration.changed(e, configuration.name('recentChanges')('file').value) ||
configuration.changed(e, configuration.name('annotations')('file').value) ||
configuration.changed(e, configuration.name('theme')('annotations')('file').value)) {
if (cfg === undefined) {
cfg = configuration.get<IConfig>();
}
if (changed) {
// Since the configuration has changed -- reset any visible annotations
for (const provider of this._annotationProviders.values()) {
if (provider === undefined) continue;
@ -178,7 +187,7 @@ export class AnnotationController extends Disposable {
provider.reset(Decorations.recentChangesAnnotation, Decorations.recentChangesHighlight);
}
else {
if (provider.annotationType === this._config.blame.file.annotationType) {
if (provider.annotationType === cfg.blame.file.annotationType) {
provider.reset(Decorations.blameAnnotation, Decorations.blameHighlight);
}
else {

+ 4
- 4
src/annotations/annotationProvider.ts Целия файл

@ -1,8 +1,8 @@
'use strict';
import { DecorationOptions, Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace } from 'vscode';
import { DecorationOptions, Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window } from 'vscode';
import { FileAnnotationType } from '../annotations/annotationController';
import { TextDocumentComparer } from '../comparers';
import { ExtensionKey, IConfig } from '../configuration';
import { configuration, IConfig } from '../configuration';
export type TextEditorCorrelationKey = string;
@ -31,7 +31,7 @@ export abstract class AnnotationProviderBase extends Disposable {
this.correlationKey = AnnotationProviderBase.getCorrelationKey(this.editor);
this.document = this.editor.document;
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
this._config = configuration.get<IConfig>();
this._disposable = Disposable.from(
window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this)
@ -78,7 +78,7 @@ export abstract class AnnotationProviderBase extends Disposable {
async reset(decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined) {
await this.clear();
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
this._config = configuration.get<IConfig>();
this.decoration = decoration;
this.highlightDecoration = highlightDecoration;

+ 1
- 1
src/annotations/blameAnnotationProvider.ts Целия файл

@ -103,7 +103,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
}
}
const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType);
const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, await this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType);
return new Hover(message, document.validateRange(new Range(position.line, 0, position.line, endOfLineIndex)));
}

+ 1
- 1
src/annotations/recentChangesAnnotationProvider.ts Целия файл

@ -48,7 +48,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
if (cfg.hover.details) {
this._decorations.push({
hoverMessage: Annotations.getHoverMessage(commit, dateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType),
hoverMessage: Annotations.getHoverMessage(commit, dateFormat, await this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType),
range: range
} as DecorationOptions);
}

+ 35
- 36
src/codeLensController.ts Целия файл

@ -1,18 +1,17 @@
'use strict';
import { Objects } from './system';
import { Disposable, ExtensionContext, languages, TextEditor, workspace } from 'vscode';
import { IConfig } from './configuration';
import { CommandContext, ExtensionKey, setCommandContext } from './constants';
import { ConfigurationChangeEvent, Disposable, ExtensionContext, languages, TextEditor } from 'vscode';
import { configuration, ICodeLensConfig } from './configuration';
import { CommandContext, setCommandContext } from './constants';
import { GitCodeLensProvider } from './gitCodeLensProvider';
import { BlameabilityChangeEvent, BlameabilityChangeReason, GitContextTracker, GitService } from './gitService';
import { Logger } from './logger';
export class CodeLensController extends Disposable {
private _codeLensProvider: GitCodeLensProvider | undefined;
private _codeLensProviderDisposable: Disposable | undefined;
private _config: IConfig;
private _canToggle: boolean;
private _disposable: Disposable | undefined;
private _provider: GitCodeLensProvider | undefined;
private _providerDisposable: Disposable | undefined;
constructor(
private readonly context: ExtensionContext,
@ -21,71 +20,71 @@ export class CodeLensController extends Disposable {
) {
super(() => this.dispose());
this.onConfigurationChanged();
this._disposable = Disposable.from(
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this),
this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._providerDisposable && this._providerDisposable.dispose();
this._disposable && this._disposable.dispose();
this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose();
this._codeLensProviderDisposable = undefined;
this._codeLensProvider = undefined;
}
private onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
private onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (!Objects.areEquivalent(cfg.codeLens, this._config && this._config.codeLens)) {
if (this._config !== undefined) {
const section = configuration.name('codeLens').value;
if (initializing || configuration.changed(e, section, null)) {
if (!initializing) {
Logger.log('CodeLens config changed; resetting CodeLens provider');
}
if (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) {
if (this._codeLensProvider) {
this._codeLensProvider.reset();
const cfg = configuration.get<ICodeLensConfig>(section);
if (cfg.enabled && (cfg.recentChange.enabled || cfg.authors.enabled)) {
if (this._provider !== undefined) {
this._provider.reset();
}
else {
this._codeLensProvider = new GitCodeLensProvider(this.context, this.git);
this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._codeLensProvider);
this._provider = new GitCodeLensProvider(this.context, this.git);
this._providerDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._provider);
}
}
else {
this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose();
this._codeLensProviderDisposable = undefined;
this._codeLensProvider = undefined;
if (this._providerDisposable !== undefined) {
this._providerDisposable.dispose();
this._providerDisposable = undefined;
}
this._provider = undefined;
}
setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled);
this._canToggle = cfg.recentChange.enabled || cfg.authors.enabled;
setCommandContext(CommandContext.CanToggleCodeLens, this._canToggle);
}
this._config = cfg;
}
private onBlameabilityChanged(e: BlameabilityChangeEvent) {
if (this._codeLensProvider === undefined) return;
if (this._provider === undefined) return;
// Don't reset if this was an editor change, because code lens will naturally be re-rendered
if (e.blameable && e.reason !== BlameabilityChangeReason.EditorChanged) {
Logger.log('Blameability changed; resetting CodeLens provider');
this._codeLensProvider.reset();
this._provider.reset();
}
}
toggleCodeLens(editor: TextEditor) {
if (!this._config.codeLens.recentChange.enabled && !this._config.codeLens.authors.enabled) return;
if (!this._canToggle) return;
Logger.log(`toggleCodeLens()`);
if (this._codeLensProviderDisposable) {
this._codeLensProviderDisposable.dispose();
this._codeLensProviderDisposable = undefined;
if (this._providerDisposable !== undefined) {
this._providerDisposable.dispose();
this._providerDisposable = undefined;
return;
}
this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this.git));
this._providerDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this.git));
}
}

+ 243
- 28
src/configuration.ts Целия файл

@ -1,10 +1,13 @@
'use strict';
import { ConfigurationChangeEvent, Event, EventEmitter, ExtensionContext, Uri, workspace } from 'vscode';
import { FileAnnotationType } from './annotations/annotationController';
import { ExtensionKey } from './constants';
import { LineAnnotationType } from './currentLineController';
import { GitExplorerView } from './views/gitExplorer';
import { OutputLevel } from './logger';
import { Functions } from './system';
export { ExtensionKey } from './constants';
export { ExtensionKey };
export enum CodeLensCommand {
DiffWithPrevious = 'gitlens.diffWithPrevious',
@ -95,6 +98,22 @@ export interface IAdvancedConfig {
};
}
export interface ICodeLensConfig {
enabled: boolean;
recentChange: {
enabled: boolean;
command: CodeLensCommand;
};
authors: {
enabled: boolean;
command: CodeLensCommand;
};
locations: CodeLensLocations[];
customLocationSymbols: string[];
perLanguageLocations: ICodeLensLanguageLocation[];
debug: boolean;
}
export interface ICodeLensLanguageLocation {
language: string | undefined;
locations: CodeLensLocations[];
@ -124,17 +143,19 @@ export interface IRemotesConfig {
type: CustomRemoteType;
domain: string;
name?: string;
urls?: {
repository: string;
branches: string;
branch: string;
commit: string;
file: string;
fileInBranch: string;
fileInCommit: string;
fileLine: string;
fileRange: string;
};
urls?: IRemotesUrlsConfig;
}
export interface IRemotesUrlsConfig {
repository: string;
branches: string;
branch: string;
commit: string;
file: string;
fileInBranch: string;
fileInCommit: string;
fileLine: string;
fileRange: string;
}
export interface IThemeConfig {
@ -300,21 +321,7 @@ export interface IConfig {
}
};
codeLens: {
enabled: boolean;
recentChange: {
enabled: boolean;
command: CodeLensCommand;
};
authors: {
enabled: boolean;
command: CodeLensCommand;
};
locations: CodeLensLocations[];
customLocationSymbols: string[];
perLanguageLocations: ICodeLensLanguageLocation[];
debug: boolean;
};
codeLens: ICodeLensConfig;
defaultDateFormat: string | null;
@ -347,4 +354,212 @@ export interface IConfig {
outputLevel: OutputLevel;
advanced: IAdvancedConfig;
}
}
const emptyConfig: IConfig = {
annotations: {
file: {
gutter: {
format: '',
dateFormat: null,
compact: false,
heatmap: {
enabled: false,
location: 'left'
},
hover: {
details: false,
changes: false,
wholeLine: false
}
},
hover: {
details: false,
changes: false,
heatmap: {
enabled: false
}
},
recentChanges: {
hover: {
details: false,
changes: false
}
}
},
line: {
hover: {
details: false,
changes: false
},
trailing: {
format: '',
dateFormat: null,
hover: {
details: false,
changes: false,
wholeLine: false
}
}
}
},
blame: {
ignoreWhitespace: false,
file: {
annotationType: 'gutter' as FileAnnotationType,
lineHighlight: {
enabled: false,
locations: []
}
},
line: {
enabled: false,
annotationType: 'trailing' as LineAnnotationType
}
},
recentChanges: {
file: {
lineHighlight: {
locations: []
}
}
},
codeLens: {
enabled: false,
recentChange: {
enabled: false,
command: CodeLensCommand.DiffWithPrevious
},
authors: {
enabled: false,
command: CodeLensCommand.DiffWithPrevious
},
locations: [],
customLocationSymbols: [],
perLanguageLocations: [],
debug: false
},
defaultDateFormat: null,
gitExplorer: {
enabled: false,
autoRefresh: false,
view: GitExplorerView.Auto,
files: {
layout: GitExplorerFilesLayout.Auto,
compact: false,
threshold: 0
},
includeWorkingTree: false,
showTrackingBranch: false,
commitFormat: '',
commitFileFormat: '',
stashFormat: '',
stashFileFormat: '',
statusFileFormat: ''
// dateFormat: string | null;
},
remotes: [],
statusBar: {
enabled: false,
alignment: 'left',
command: StatusBarCommand.DiffWithPrevious,
format: '',
dateFormat: null
},
strings: {
codeLens: {
unsavedChanges: {
recentChangeAndAuthors: '',
recentChangeOnly: '',
authorsOnly: ''
}
}
},
theme: themeDefaults,
debug: false,
insiders: false,
outputLevel: 'verbose' as OutputLevel,
advanced: {
caching: {
enabled: false,
maxLines: 0
},
git: '',
maxQuickHistory: 0,
menus: {
explorerContext: {
fileDiff: false,
history: false,
remote: false
},
editorContext: {
blame: false,
copy: false,
details: false,
fileDiff: false,
history: false,
lineDiff: false,
remote: false
},
editorTitle: {
blame: false,
fileDiff: false,
history: false,
status: false
},
editorTitleContext: {
blame: false,
fileDiff: false,
history: false,
remote: false
}
},
quickPick: {
closeOnFocusOut: false
},
telemetry: {
enabled: false
}
}
};
export class Configuration {
static configure(context: ExtensionContext) {
context.subscriptions.push(workspace.onDidChangeConfiguration(configuration.onConfigurationChanged, configuration));
}
private _onDidChange = new EventEmitter<ConfigurationChangeEvent>();
get onDidChange(): Event<ConfigurationChangeEvent> {
return this._onDidChange.event;
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (!e.affectsConfiguration(ExtensionKey, null!)) return;
this._onDidChange.fire(e);
}
readonly defaults = { theme: themeDefaults };
readonly initializingChangeEvent: ConfigurationChangeEvent = {
affectsConfiguration: (section: string, resource?: Uri) => false
};
get<T>(section?: string, resource?: Uri | null) {
return workspace.getConfiguration(section === undefined ? undefined : ExtensionKey, resource!).get<T>(section === undefined ? ExtensionKey : section)!;
}
changed(e: ConfigurationChangeEvent, section: string, resource?: Uri | null) {
return e.affectsConfiguration(`${ExtensionKey}.${section}`, resource!);
}
initializing(e: ConfigurationChangeEvent) {
return e === this.initializingChangeEvent;
}
name<K extends keyof IConfig>(name: K) {
return Functions.propOf(emptyConfig, name);
}
}
export const configuration = new Configuration();

+ 29
- 27
src/currentLineController.ts Целия файл

@ -1,12 +1,12 @@
'use strict';
import { Functions, Objects } from './system';
import { CancellationToken, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { Functions } from './system';
import { CancellationToken, ConfigurationChangeEvent, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window } from 'vscode';
import { AnnotationController, FileAnnotationType } from './annotations/annotationController';
import { Annotations, endOfLineIndex } from './annotations/annotations';
import { Commands } from './commands';
import { TextEditorComparer } from './comparers';
import { IConfig, StatusBarCommand } from './configuration';
import { DocumentSchemes, ExtensionKey, isTextEditor } from './constants';
import { configuration, IConfig, StatusBarCommand } from './configuration';
import { DocumentSchemes, isTextEditor } from './constants';
import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from './gitService';
// import { Logger } from './logger';
@ -26,7 +26,7 @@ export enum LineAnnotationType {
export class CurrentLineController extends Disposable {
private _blameable: boolean;
private _blameLineAnnotationState: { enabled: boolean, annotationType: LineAnnotationType, reason: 'user' | 'debugging' } | undefined = undefined;
private _blameLineAnnotationState: { enabled: boolean, annotationType: LineAnnotationType, reason: 'user' | 'debugging' } | undefined;
private _config: IConfig;
private _currentLine: { line: number, commit?: GitCommit, logCommit?: GitLogCommit } = { line: -1 };
private _debugSessionEndDisposable: Disposable | undefined;
@ -49,13 +49,12 @@ export class CurrentLineController extends Disposable {
this._updateBlameDebounced = Functions.debounce(this.updateBlame, 250);
this.onConfigurationChanged();
this._disposable = Disposable.from(
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this),
annotationController.onDidToggleAnnotations(this.onFileAnnotationsToggled, this),
debug.onDidStartDebugSession(this.onDebugSessionStarted, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
@ -68,27 +67,28 @@ export class CurrentLineController extends Disposable {
this._disposable && this._disposable.dispose();
}
private onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
private onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
const cfg = configuration.get<IConfig>();
let changed = false;
let clear = false;
if (!Objects.areEquivalent(cfg.blame.line, this._config && this._config.blame.line)) {
if (initializing || configuration.changed(e, configuration.name('blame')('line').value)) {
changed = true;
this._blameLineAnnotationState = undefined;
clear = (this._config !== undefined);
}
if (!Objects.areEquivalent(cfg.annotations.line.trailing, this._config && this._config.annotations.line.trailing) ||
!Objects.areEquivalent(cfg.annotations.line.hover, this._config && this._config.annotations.line.hover) ||
!Objects.areEquivalent(cfg.theme.annotations.line.trailing, this._config && this._config.theme.annotations.line.trailing)) {
if (initializing ||
configuration.changed(e, configuration.name('annotations')('line')('trailing').value) ||
configuration.changed(e, configuration.name('annotations')('line')('hover').value) ||
configuration.changed(e, configuration.name('theme')('annotations')('line')('trailing').value)) {
changed = true;
clear = (this._config !== undefined);
}
if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) {
if (initializing || configuration.changed(e, configuration.name('statusBar').value)) {
changed = true;
if (cfg.statusBar.enabled) {
const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left;
if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) {
@ -99,7 +99,7 @@ export class CurrentLineController extends Disposable {
this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0);
this._statusBarItem.command = cfg.statusBar.command;
}
else if (!cfg.statusBar.enabled && this._statusBarItem) {
else if (this._statusBarItem !== undefined) {
this._statusBarItem.dispose();
this._statusBarItem = undefined;
}
@ -109,19 +109,18 @@ export class CurrentLineController extends Disposable {
if (!changed) return;
if (clear) {
this.clearAnnotations(this._editor);
}
const trackCurrentLine = cfg.statusBar.enabled ||
cfg.blame.line.enabled ||
(this._blameLineAnnotationState !== undefined && this._blameLineAnnotationState.enabled);
const trackCurrentLine = cfg.statusBar.enabled || cfg.blame.line.enabled || (this._blameLineAnnotationState !== undefined && this._blameLineAnnotationState.enabled);
if (trackCurrentLine && !this._trackCurrentLineDisposable) {
this._trackCurrentLineDisposable = Disposable.from(
if (trackCurrentLine) {
this._trackCurrentLineDisposable = this._trackCurrentLineDisposable || Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this),
window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this),
this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this)
);
}
else if (!trackCurrentLine && this._trackCurrentLineDisposable) {
else if (this._trackCurrentLineDisposable !== undefined) {
this._trackCurrentLineDisposable.dispose();
this._trackCurrentLineDisposable = undefined;
}
@ -251,6 +250,9 @@ export class CurrentLineController extends Disposable {
async refresh(editor?: TextEditor) {
this._currentLine.line = -1;
if (editor === undefined && this._editor !== undefined) return;
this.clearAnnotations(this._editor);
if (editor === undefined || !this.isEditorBlameable(editor)) {
@ -425,7 +427,7 @@ export class CurrentLineController extends Disposable {
}
}
const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType);
const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, await this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType);
return new Hover(message, range);
}

+ 3
- 3
src/extension.ts Целия файл

@ -2,7 +2,7 @@
// import { Objects } from './system';
import { commands, ExtensionContext, extensions, languages, window, workspace } from 'vscode';
import { AnnotationController } from './annotations/annotationController';
import { CodeLensLocations, IConfig, LineHighlightLocations } from './configuration';
import { CodeLensLocations, Configuration, IConfig, LineHighlightLocations } from './configuration';
import { ApplicationInsightsKey, CommandContext, ExtensionKey, GlobalState, QualifiedExtensionId, setCommandContext } from './constants';
import { CodeLensController } from './codeLensController';
import { configureCommands } from './commands';
@ -10,7 +10,7 @@ import { CurrentLineController, LineAnnotationType } from './currentLineControll
import { GitContentProvider } from './gitContentProvider';
import { GitExplorer } from './views/gitExplorer';
import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider';
import { GitContextTracker, GitService, RemoteProviderFactory } from './gitService';
import { GitContextTracker, GitService } from './gitService';
import { Keyboard } from './keyboard';
import { Logger } from './logger';
import { Messages, SuppressedKeys } from './messages';
@ -18,10 +18,10 @@ import { Telemetry } from './telemetry';
// this method is called when your extension is activated
export async function activate(context: ExtensionContext) {
Configuration.configure(context);
Logger.configure(context);
Messages.configure(context);
Telemetry.configure(ApplicationInsightsKey);
RemoteProviderFactory.configure(context);
const gitlens = extensions.getExtension(QualifiedExtensionId)!;
const gitlensVersion = gitlens.packageJSON.version;

+ 11
- 10
src/git/git.ts Целия файл

@ -26,11 +26,6 @@ const defaultBlameParams = [`blame`, `--root`, `--incremental`];
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor %an%nauthor-date %at%nparents %P%nsummary %B%nfilename ?`];
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?`];
let defaultEncoding = 'utf8';
export function setDefaultEncoding(encoding: string) {
defaultEncoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
}
const GitWarnings = [
/Not a git repository/,
/is outside repository/,
@ -123,6 +118,12 @@ export class Git {
return git;
}
static getEncoding(encoding: string | undefined) {
return (encoding !== undefined && iconv.encodingExists(encoding))
? encoding
: 'utf8';
}
static async getGitPath(gitPath?: string): Promise<IGit> {
git = await findGitPath(gitPath);
Logger.log(`Git found: ${git.version} @ ${git.path === 'git' ? 'PATH' : git.path}`);
@ -130,7 +131,7 @@ export class Git {
}
static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) {
const data = await Git.show(repoPath, fileName, branchOrSha, 'binary');
const data = await Git.show(repoPath, fileName, branchOrSha, { encoding: 'binary' });
if (data === undefined) return undefined;
const suffix = Strings.truncate(Strings.sanitizeForFS(Git.isSha(branchOrSha) ? Git.shortenSha(branchOrSha) : branchOrSha), 50, '');
@ -243,7 +244,7 @@ export class Git {
}
}
static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string, encoding?: string) {
static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string, options: { encoding?: string } = {}) {
const params = [`diff`, `--diff-filter=M`, `-M`, `--no-ext-diff`];
if (sha1) {
params.push(sha1);
@ -252,7 +253,7 @@ export class Git {
params.push(sha2);
}
return gitCommand({ cwd: repoPath, encoding: encoding || defaultEncoding }, ...params, '--', fileName);
return gitCommand({ cwd: repoPath, encoding: options.encoding || 'utf8' }, ...params, '--', fileName);
}
static diff_nameStatus(repoPath: string, sha1?: string, sha2?: string, options: { filter?: string } = {}) {
@ -420,11 +421,11 @@ export class Git {
}
}
static async show(repoPath: string | undefined, fileName: string, branchOrSha: string, encoding?: string) {
static async show(repoPath: string | undefined, fileName: string, branchOrSha: string, options: { encoding?: string } = {}) {
const [file, root] = Git.splitPath(fileName, repoPath);
if (Git.isUncommitted(branchOrSha)) throw new Error(`sha=${branchOrSha} is uncommitted`);
const opts = { cwd: root, encoding: encoding || defaultEncoding, willHandleErrors: true } as GitCommandOptions;
const opts = { cwd: root, encoding: options.encoding || 'utf8', willHandleErrors: true } as GitCommandOptions;
const args = `${branchOrSha}:./${file}`;
try {
const data = await gitCommand(opts, 'show', args);

+ 34
- 33
src/git/gitContextTracker.ts Целия файл

@ -1,7 +1,8 @@
'use strict';
import { Functions } from '../system';
import { Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode';
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode';
import { TextDocumentComparer } from '../comparers';
import { configuration } from '../configuration';
import { CommandContext, isTextEditor, setCommandContext } from '../constants';
import { GitChangeEvent, GitChangeReason, GitService, GitUri, Repository, RepositoryChangeEvent } from '../gitService';
// import { Logger } from '../logger';
@ -40,45 +41,49 @@ export class GitContextTracker extends Disposable {
return this._onDidChangeBlameability.event;
}
private _context: Context = { state: { dirty: false } };
private _disposable: Disposable | undefined;
private _gitEnabled: boolean;
private readonly _context: Context = { state: { dirty: false } };
private readonly _disposable: Disposable;
private _listenersDisposable: Disposable | undefined;
constructor(
private readonly git: GitService
) {
super(() => this.dispose());
this.onConfigurationChanged();
this._disposable = Disposable.from(
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._listenersDisposable && this._listenersDisposable.dispose();
this._disposable && this._disposable.dispose();
}
private onConfigurationChanged() {
const gitEnabled = workspace.getConfiguration('git').get<boolean>('enabled', true);
if (this._gitEnabled !== gitEnabled) {
this._gitEnabled = gitEnabled;
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (!configuration.initializing(e) && !e.affectsConfiguration('git.enabled', null!)) return;
if (this._disposable !== undefined) {
this._disposable.dispose();
this._disposable = undefined;
}
const enabled = workspace.getConfiguration('git', null!).get<boolean>('enabled', true);
if (this._listenersDisposable !== undefined) {
this._listenersDisposable.dispose();
this._listenersDisposable = undefined;
}
setCommandContext(CommandContext.Enabled, gitEnabled);
setCommandContext(CommandContext.Enabled, enabled);
if (gitEnabled) {
this._disposable = Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this),
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this),
workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this),
this.git.onDidBlameFail(this.onBlameFailed, this),
this.git.onDidChange(this.onGitChanged, this)
);
if (enabled) {
this._listenersDisposable = Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this),
workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this),
this.git.onDidBlameFail(this.onBlameFailed, this),
this.git.onDidChange(this.onGitChanged, this)
);
this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true);
}
this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true);
}
else {
this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, false);
}
}
@ -98,9 +103,9 @@ export class GitContextTracker extends Disposable {
}
private onGitChanged(e: GitChangeEvent) {
if (e.reason === GitChangeReason.RemoteCache || e.reason === GitChangeReason.Repositories) {
this.updateRemotes();
}
if (e.reason !== GitChangeReason.Repositories) return;
this.updateRemotes();
}
private onRepoChanged(e: RepositoryChangeEvent) {
@ -186,9 +191,7 @@ export class GitContextTracker extends Disposable {
private async updateRemotes() {
let hasRemotes = false;
if (this._context.repo !== undefined) {
const remotes = await this.git.getRemotes(this._context.repo.path);
hasRemotes = remotes.length !== 0;
hasRemotes = await this._context.repo.hasRemotes();
setCommandContext(CommandContext.ActiveHasRemotes, hasRemotes);
}
else {
@ -200,9 +203,7 @@ export class GitContextTracker extends Disposable {
for (const repo of repositories) {
if (repo === this._context.repo) continue;
const remotes = await this.git.getRemotes(repo.path);
hasRemotes = remotes.length !== 0;
hasRemotes = await repo.hasRemotes();
if (hasRemotes) break;
}
}

+ 3
- 6
src/git/models/remote.ts Целия файл

@ -1,5 +1,5 @@
'use strict';
import { RemoteProvider, RemoteProviderFactory } from '../remotes/factory';
import { RemoteProvider } from '../remotes/factory';
export enum GitRemoteType {
Fetch = 'fetch',
@ -8,15 +8,12 @@ export enum GitRemoteType {
export class GitRemote {
provider?: RemoteProvider;
constructor(
public readonly repoPath: string,
public readonly name: string,
public readonly domain: string,
public readonly path: string,
public readonly provider: RemoteProvider | undefined,
public readonly types: { type: GitRemoteType, url: string }[]
) {
this.provider = RemoteProviderFactory.getRemoteProvider(this.domain, this.path);
}
) { }
}

+ 62
- 2
src/git/models/repository.ts Целия файл

@ -1,9 +1,14 @@
'use strict';
import { Functions } from '../../system';
import { Disposable, Event, EventEmitter, RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode';
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode';
import { configuration, IRemotesConfig } from '../../configuration';
import { GitBranch, GitDiffShortStat, GitRemote, GitStash, GitStatus } from '../git';
import { GitService, GitUri } from '../../gitService';
import { RemoteProviderFactory, RemoteProviderMap } from '../remotes/factory';
export enum RepositoryChange {
// FileSystem = 'file-system',
RemotesCache = 'remotes-cache',
Repository = 'repository',
Stashes = 'stashes'
}
@ -55,6 +60,7 @@ export class Repository extends Disposable {
readonly index: number;
readonly name: string;
readonly normalizedPath: string;
readonly storage: Map<string, any> = new Map();
private readonly _disposable: Disposable;
@ -63,11 +69,14 @@ export class Repository extends Disposable {
private _fsWatchCounter = 0;
private _fsWatcherDisposable: Disposable | undefined;
private _pendingChanges: { repo?: RepositoryChangeEvent, fs?: RepositoryFileSystemChangeEvent } = { };
private _providerMap: RemoteProviderMap;
private _remotes: GitRemote[] | undefined;
private _suspended: boolean;
constructor(
private readonly folder: WorkspaceFolder,
public readonly path: string,
private readonly git: GitService,
private readonly onAnyRepositoryChanged: () => void,
suspended: boolean
) {
@ -75,6 +84,8 @@ export class Repository extends Disposable {
this.index = folder.index;
this.name = folder.name;
this.normalizedPath = (this.path.endsWith('/') ? this.path : `${this.path}/`).toLowerCase();
this._suspended = suspended;
const watcher = workspace.createFileSystemWatcher(new RelativePattern(folder, '**/.git/{index,HEAD,refs/stash,refs/heads/**,refs/remotes/**}'));
@ -82,8 +93,10 @@ export class Repository extends Disposable {
watcher,
watcher.onDidChange(this.onRepositoryChanged, this),
watcher.onDidCreate(this.onRepositoryChanged, this),
watcher.onDidDelete(this.onRepositoryChanged, this)
watcher.onDidDelete(this.onRepositoryChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
@ -99,6 +112,20 @@ export class Repository extends Disposable {
this._disposable && this._disposable.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
const section = configuration.name('remotes').value;
if (initializing || configuration.changed(e, section, this.folder.uri)) {
this._providerMap = RemoteProviderFactory.createMap(configuration.get<IRemotesConfig[] | null | undefined>(section, this.folder.uri));
if (!initializing) {
this._remotes = undefined;
this.fireChange(RepositoryChange.RemotesCache);
}
}
}
private onFileSystemChanged(uri: Uri) {
// Ignore .git changes
if (/\.git/.test(uri.fsPath)) return;
@ -177,6 +204,39 @@ export class Repository extends Disposable {
return this.folder === workspace.getWorkspaceFolder(uri);
}
async getBranch(): Promise<GitBranch | undefined> {
return this.git.getBranch(this.path);
}
async getBranches(): Promise<GitBranch[]> {
return this.git.getBranches(this.path);
}
async getChangedFilesCount(sha?: string): Promise<GitDiffShortStat | undefined> {
return this.git.getChangedFilesCount(this.path, sha);
}
async getRemotes(): Promise<GitRemote[]> {
if (this._remotes === undefined) {
this._remotes = await this.git.getRemotesCore(this.path, this._providerMap);
}
return this._remotes;
}
async getStashList(): Promise<GitStash | undefined> {
return this.git.getStashList(this.path);
}
async getStatus(): Promise<GitStatus | undefined> {
return this.git.getStatusForRepo(this.path);
}
async hasRemotes(): Promise<boolean> {
const remotes = await this.getRemotes();
return remotes !== undefined && remotes.length > 0;
}
resume() {
if (!this._suspended) return;

+ 3
- 2
src/git/parsers/remoteParser.ts Целия файл

@ -1,13 +1,14 @@
'use strict';
import { GitRemote } from './../git';
import { GitRemoteType } from '../models/remote';
import { RemoteProvider } from '../remotes/factory';
const remoteRegex = /^(.*)\t(.*)\s\((.*)\)$/gm;
const urlRegex = /^(?:git:\/\/(.*?)\/|https:\/\/(.*?)\/|http:\/\/(.*?)\/|git@(.*):|ssh:\/\/(?:.*@)?(.*?)(?::.*?)?\/)(.*)$/;
export class GitRemoteParser {
static parse(data: string, repoPath: string): GitRemote[] {
static parse(data: string, repoPath: string, providerFactory: (domain: string, path: string) => RemoteProvider | undefined): GitRemote[] {
if (!data) return [];
const remotes: GitRemote[] = [];
@ -25,7 +26,7 @@ export class GitRemoteParser {
const uniqueness = `${domain}/${path}`;
let remote: GitRemote | undefined = groups[uniqueness];
if (remote === undefined) {
remote = new GitRemote(repoPath, match[1], domain, path, [{ url: url, type: match[3] as GitRemoteType }]);
remote = new GitRemote(repoPath, match[1], domain, path, providerFactory(domain, path), [{ url: url, type: match[3] as GitRemoteType }]);
remotes.push(remote);
groups[uniqueness] = remote;
}

+ 14
- 11
src/git/remotes/custom.ts Целия файл

@ -1,17 +1,20 @@
'use strict';
import { Strings } from '../../system';
import { Range } from 'vscode';
import { IRemotesConfig } from '../../configuration';
import { IRemotesConfig, IRemotesUrlsConfig } from '../../configuration';
import { RemoteProvider } from './provider';
export class CustomService extends RemoteProvider {
private readonly urls: IRemotesUrlsConfig;
constructor(
domain: string,
path: string,
private readonly config: IRemotesConfig
config: IRemotesConfig
) {
super(domain, path, config.name, true);
this.urls = config.urls!;
}
get name() {
@ -19,34 +22,34 @@ export class CustomService extends RemoteProvider {
}
protected getUrlForRepository(): string {
return Strings.interpolate(this.config.urls!.repository, { repo: this.path });
return Strings.interpolate(this.urls.repository, { repo: this.path });
}
protected getUrlForBranches(): string {
return Strings.interpolate(this.config.urls!.branches, { repo: this.path });
return Strings.interpolate(this.urls.branches, { repo: this.path });
}
protected getUrlForBranch(branch: string): string {
return Strings.interpolate(this.config.urls!.branch, { repo: this.path, branch: branch });
return Strings.interpolate(this.urls.branch, { repo: this.path, branch: branch });
}
protected getUrlForCommit(sha: string): string {
return Strings.interpolate(this.config.urls!.commit, { repo: this.path, id: sha });
return Strings.interpolate(this.urls.commit, { repo: this.path, id: sha });
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = Strings.interpolate(this.config.urls!.fileLine, { line: range.start.line });
line = Strings.interpolate(this.urls.fileLine, { line: range.start.line });
}
else {
line = Strings.interpolate(this.config.urls!.fileRange, { start: range.start.line, end: range.end.line });
line = Strings.interpolate(this.urls.fileRange, { start: range.start.line, end: range.end.line });
}
}
if (sha) return Strings.interpolate(this.config.urls!.fileInCommit, { repo: this.path, id: sha, file: fileName, line: line });
if (branch) return Strings.interpolate(this.config.urls!.fileInBranch, { repo: this.path, branch: branch, file: fileName, line: line });
return Strings.interpolate(this.config.urls!.file, { repo: this.path, file: fileName, line: line });
if (sha) return Strings.interpolate(this.urls.fileInCommit, { repo: this.path, id: sha, file: fileName, line: line });
if (branch) return Strings.interpolate(this.urls.fileInBranch, { repo: this.path, branch: branch, file: fileName, line: line });
return Strings.interpolate(this.urls.file, { repo: this.path, file: fileName, line: line });
}
}

+ 15
- 35
src/git/remotes/factory.ts Целия файл

@ -1,10 +1,7 @@
'use strict';
import { Objects } from '../../system';
import { Event, EventEmitter, ExtensionContext, workspace } from 'vscode';
import { BitbucketService } from './bitbucket';
import { BitbucketServerService } from './bitbucket-server';
import { CustomRemoteType, IConfig, IRemotesConfig } from '../../configuration';
import { ExtensionKey } from '../../constants';
import { CustomRemoteType, IRemotesConfig } from '../../configuration';
import { CustomService } from './custom';
import { GitHubService } from './github';
import { GitLabService } from './gitlab';
@ -21,29 +18,22 @@ const defaultProviderMap = new Map Rem
['visualstudio.com', (domain: string, path: string) => new VisualStudioService(domain, path)]
]);
export class RemoteProviderFactory {
private static _providerMap: Map<string, (domain: string, path: string) => RemoteProvider>;
private static _remotesCfg: IRemotesConfig[];
export type RemoteProviderMap = Map<string, (domain: string, path: string) => RemoteProvider>;
private static _onDidChange = new EventEmitter<void>();
public static get onDidChange(): Event<void> {
return this._onDidChange.event;
}
export class RemoteProviderFactory {
static configure(context: ExtensionContext) {
context.subscriptions.push(workspace.onDidChangeConfiguration(() => this.onConfigurationChanged()));
this.onConfigurationChanged(true);
static factory(providerMap: RemoteProviderMap): (domain: string, path: string) => RemoteProvider | undefined {
return (domain: string, path: string) => this.create(providerMap, domain, path);
}
static getRemoteProvider(domain: string, path: string): RemoteProvider | undefined {
static create(providerMap: RemoteProviderMap, domain: string, path: string): RemoteProvider | undefined {
try {
let key = domain.toLowerCase();
if (key.endsWith('visualstudio.com')) {
key = 'visualstudio.com';
}
const creator = this._providerMap.get(key);
const creator = providerMap.get(key);
if (creator === undefined) return undefined;
return creator(domain, path);
@ -54,27 +44,17 @@ export class RemoteProviderFactory {
}
}
private static onConfigurationChanged(silent: boolean = false) {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey);
if (cfg === undefined) return;
if (!Objects.areEquivalent(cfg.remotes, this._remotesCfg)) {
this._providerMap = new Map(defaultProviderMap);
this._remotesCfg = cfg.remotes;
if (this._remotesCfg != null && this._remotesCfg.length > 0) {
for (const remoteCfg of this._remotesCfg) {
const provider = this.getCustomProvider(remoteCfg);
if (provider === undefined) continue;
this._providerMap.set(remoteCfg.domain.toLowerCase(), provider);
}
static createMap(cfg: IRemotesConfig[] | null | undefined): RemoteProviderMap {
const providerMap = new Map(defaultProviderMap);
if (cfg != null && cfg.length > 0) {
for (const rc of cfg) {
const provider = this.getCustomProvider(rc);
if (provider === undefined) continue;
if (!silent) {
this._onDidChange.fire();
}
providerMap.set(rc.domain.toLowerCase(), provider);
}
}
return providerMap;
}
private static getCustomProvider(cfg: IRemotesConfig) {

+ 63
- 48
src/gitCodeLensProvider.ts Целия файл

@ -1,9 +1,9 @@
'use strict';
import { Functions, Iterables } from './system';
import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode';
import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands';
import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants';
import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration';
import { CodeLensCommand, CodeLensLocations, configuration, ICodeLensConfig, ICodeLensLanguageLocation } from './configuration';
import { BuiltInCommands, DocumentSchemes } from './constants';
import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService';
import { Logger } from './logger';
@ -16,9 +16,10 @@ export class GitRecentChangeCodeLens extends CodeLens {
public readonly blameRange: Range,
public readonly isFullRange: boolean,
range: Range,
public dirty: boolean
public readonly desiredCommand: CodeLensCommand,
command?: Command | undefined
) {
super(range);
super(range, command);
}
getBlame(): GitBlameLines | undefined {
@ -34,7 +35,8 @@ export class GitAuthorsCodeLens extends CodeLens {
private readonly blame: () => GitBlameLines | undefined,
public readonly blameRange: Range,
public readonly isFullRange: boolean,
range: Range
range: Range,
public readonly desiredCommand: CodeLensCommand
) {
super(range);
}
@ -53,31 +55,29 @@ export class GitCodeLensProvider implements CodeLensProvider {
static selector: DocumentSelector = { scheme: DocumentSchemes.File };
private _config: IConfig;
private _debug: boolean;
constructor(
context: ExtensionContext,
private readonly git: GitService
) {
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
}
) { }
reset() {
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
Logger.log('Triggering a reset of the git CodeLens provider');
this._onDidChangeCodeLenses.fire();
}
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
const dirty = document.isDirty;
let languageLocations = this._config.codeLens.perLanguageLocations.find(ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId);
const cfg = configuration.get<ICodeLensConfig>(configuration.name('codeLens').value, document.uri);
this._debug = cfg.debug;
let languageLocations = cfg.perLanguageLocations && cfg.perLanguageLocations.find(ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId);
if (languageLocations == null) {
languageLocations = {
language: undefined,
locations: this._config.codeLens.locations,
customSymbols: this._config.codeLens.customLocationSymbols
locations: cfg.locations,
customSymbols: cfg.customLocationSymbols
} as ICodeLensLanguageLocation;
}
@ -118,9 +118,12 @@ export class GitCodeLensProvider implements CodeLensProvider {
const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 1000000, 1000000, 1000000)));
// Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately
const dirtyCommand = dirty ? { title: this.getDirtyTitle(cfg) } as Command : undefined;
if (symbols !== undefined) {
Logger.log('GitCodeLensProvider.provideCodeLenses:', `${symbols.length} symbol(s) found`);
symbols.forEach(sym => this.provideCodeLens(lenses, document, dirty, sym, languageLocations!, documentRangeFn, blame, gitUri));
symbols.forEach(sym => this.provideCodeLens(lenses, document, sym, languageLocations!, documentRangeFn, blame, gitUri, cfg, dirty, dirtyCommand));
}
if ((languageLocations.locations.includes(CodeLensLocations.Document) || languageLocations.customSymbols.includes('file')) && !languageLocations.customSymbols.includes('!file')) {
@ -129,17 +132,34 @@ export class GitCodeLensProvider implements CodeLensProvider {
const blameRange = documentRangeFn();
let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined;
if (dirty || this._config.codeLens.recentChange.enabled) {
if (dirty || cfg.recentChange.enabled) {
if (!dirty) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange));
}
lenses.push(new GitRecentChangeCodeLens(SymbolKind.File, gitUri, blameForRangeFn, blameRange, true, new Range(0, 0, 0, blameRange.start.character), dirty));
lenses.push(new GitRecentChangeCodeLens(
SymbolKind.File,
gitUri,
blameForRangeFn,
blameRange,
true,
new Range(0, 0, 0, blameRange.start.character),
cfg.recentChange.command,
dirtyCommand
));
}
if (!dirty && this._config.codeLens.authors.enabled) {
if (!dirty && cfg.authors.enabled) {
if (blameForRangeFn === undefined) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange));
}
lenses.push(new GitAuthorsCodeLens(SymbolKind.File, gitUri, blameForRangeFn, blameRange, true, new Range(0, 1, 0, blameRange.start.character)));
lenses.push(new GitAuthorsCodeLens(
SymbolKind.File,
gitUri,
blameForRangeFn,
blameRange,
true,
new Range(0, 1, 0, blameRange.start.character),
cfg.authors.command
));
}
}
}
@ -206,7 +226,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
return valid ? range || symbol.location.range : undefined;
}
private provideCodeLens(lenses: CodeLens[], document: TextDocument, dirty: boolean, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range, blame: GitBlame | undefined, gitUri: GitUri | undefined): void {
private provideCodeLens(lenses: CodeLens[], document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range, blame: GitBlame | undefined, gitUri: GitUri | undefined, cfg: ICodeLensConfig, dirty: boolean, dirtyCommand: Command | undefined): void {
const blameRange = this.validateSymbolAndGetBlameRange(symbol, languageLocation, documentRangeFn);
if (blameRange === undefined) return;
@ -218,15 +238,15 @@ export class GitCodeLensProvider implements CodeLensProvider {
let startChar = 0;
let blameForRangeFn: (() => GitBlameLines | undefined) | undefined;
if (dirty || this._config.codeLens.recentChange.enabled) {
if (dirty || cfg.recentChange.enabled) {
if (!dirty) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange));
}
lenses.push(new GitRecentChangeCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), dirty));
lenses.push(new GitRecentChangeCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), cfg.recentChange.command, dirtyCommand));
startChar++;
}
if (this._config.codeLens.authors.enabled) {
if (cfg.authors.enabled) {
let multiline = !blameRange.isSingleLine;
// HACK for Omnisharp, since it doesn't return full ranges
if (!multiline && document.languageId === 'csharp') {
@ -251,7 +271,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (blameForRangeFn === undefined) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange));
}
lenses.push(new GitAuthorsCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar))));
lenses.push(new GitAuthorsCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), cfg.authors.command));
}
}
}
@ -263,33 +283,16 @@ export class GitCodeLensProvider implements CodeLensProvider {
}
private resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens {
// Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately
let title: string;
if (lens.dirty) {
if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) {
title = this._config.strings.codeLens.unsavedChanges.recentChangeAndAuthors;
}
else if (this._config.codeLens.recentChange.enabled) {
title = this._config.strings.codeLens.unsavedChanges.recentChangeOnly;
}
else {
title = this._config.strings.codeLens.unsavedChanges.authorsOnly;
}
lens.command = { title: title } as Command;
return lens;
}
const blame = lens.getBlame();
if (blame === undefined) return lens;
const recentCommit = Iterables.first(blame.commits.values());
title = `${recentCommit.author}, ${recentCommit.fromNow()}`;
if (this._config.codeLens.debug) {
let title = `${recentCommit.author}, ${recentCommit.fromNow()}`;
if (this._debug) {
title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`;
}
switch (this._config.codeLens.recentChange.command) {
switch (lens.desiredCommand) {
case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
@ -306,11 +309,11 @@ export class GitCodeLensProvider implements CodeLensProvider {
const count = blame.authors.size;
let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`;
if (this._config.codeLens.debug) {
if (this._debug) {
title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join(Iterables.map(blame.authors.values(), a => a.name), ', ')})]`;
}
switch (this._configlass="p">.codeLens.authors.command) {
switch (lens.desiredCommand) {
case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
@ -400,4 +403,16 @@ export class GitCodeLensProvider implements CodeLensProvider {
};
return lens;
}
private getDirtyTitle(cfg: ICodeLensConfig) {
if (cfg.recentChange.enabled && cfg.authors.enabled) {
return configuration.get<string>(configuration.name('strings')('codeLens')('unsavedChanges')('recentChangeAndAuthors').value);
}
else if (cfg.recentChange.enabled) {
return configuration.get<string>(configuration.name('strings')('codeLens')('unsavedChanges')('recentChangeOnly').value);
}
else {
return configuration.get<string>(configuration.name('strings')('codeLens')('unsavedChanges')('authorsOnly').value);
}
}
}

+ 10
- 10
src/gitRevisionCodeLensProvider.ts Целия файл

@ -1,5 +1,4 @@
'use strict';
// import { Iterables } from './system';
import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs } from './commands';
import { DocumentSchemes } from './constants';
@ -8,9 +7,8 @@ import { GitCommit, GitService, GitUri } from './gitService';
export class GitDiffWithWorkingCodeLens extends CodeLens {
constructor(
git: GitService,
public fileName: string,
public commit: GitCommit,
public readonly fileName: string,
public readonly commit: GitCommit,
range: Range
) {
super(range);
@ -20,9 +18,8 @@ export class GitDiffWithWorkingCodeLens extends CodeLens {
export class GitDiffWithPreviousCodeLens extends CodeLens {
constructor(
git: GitService,
public fileName: string,
public commit: GitCommit,
public readonly fileName: string,
public readonly commit: GitCommit,
range: Range
) {
super(range);
@ -33,7 +30,10 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider {
static selector: DocumentSelector = { scheme: DocumentSchemes.GitLensGit };
constructor(context: ExtensionContext, private git: GitService) { }
constructor(
context: ExtensionContext,
private readonly git: GitService
) { }
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
const data = GitService.fromGitContentUri(document.uri);
@ -45,9 +45,9 @@ export class GitRevisionCodeLensProvider implements CodeLensProvider {
if (commit === undefined) return lenses;
if (commit.previousSha) {
lenses.push(new GitDiffWithPreviousCodeLens(this.git, commit.previousUri.fsPath, commit, new Range(0, 0, 0, 1)));
lenses.push(new GitDiffWithPreviousCodeLens(commit.previousUri.fsPath, commit, new Range(0, 0, 0, 1)));
}
lenses.push(new GitDiffWithWorkingCodeLens(this.git, commit.uri.fsPath, commit, new Range(0, 1, 0, 2)));
lenses.push(new GitDiffWithWorkingCodeLens(commit.uri.fsPath, commit, new Range(0, 1, 0, 2)));
return lenses;
}

+ 62
- 59
src/gitService.ts Целия файл

@ -1,10 +1,10 @@
'use strict';
import { Functions, Iterables, Objects } from './system';
import { Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFoldersChangeEvent } from 'vscode';
import { IConfig } from './configuration';
import { CommandContext, DocumentSchemes, ExtensionKey, setCommandContext } from './constants';
import { RemoteProviderFactory } from './git/remotes/factory';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, Repository, setDefaultEncoding } from './git/git';
import { Functions, Iterables } from './system';
import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFoldersChangeEvent } from 'vscode';
import { configuration, IConfig, IRemotesConfig } from './configuration';
import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, Repository } from './git/git';
import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri';
import { Logger } from './logger';
import * as fs from 'fs';
@ -71,7 +71,6 @@ export enum GitRepoSearchBy {
export enum GitChangeReason {
GitCache = 'git-cache',
RemoteCache = 'remote-cache',
Repositories = 'repositories'
}
@ -101,7 +100,6 @@ export class GitService extends Disposable {
private _disposable: Disposable | undefined;
private _documentKeyMap: Map<TextDocument, string>;
private _gitCache: Map<string, GitCacheEntry>;
private _remotesCache: Map<string, GitRemote[]>;
private _repositories: Map<string, Repository | undefined>;
private _repositoriesPromise: Promise<void> | undefined;
private _suspended: boolean = false;
@ -113,20 +111,17 @@ export class GitService extends Disposable {
this._documentKeyMap = new Map();
this._gitCache = new Map();
this._remotesCache = new Map();
this._repositories = new Map();
this._trackedCache = new Map();
this._versionedUriCache = new Map();
this.onConfigurationChanged();
this._repositoriesPromise = this.onWorkspaceFoldersChanged();
this._disposable = Disposable.from(
window.onDidChangeWindowState(this.onWindowStateChanged, this),
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this),
workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this),
RemoteProviderFactory.onDidChange(this.onRemoteProvidersChanged, this)
configuration.onDidChange(this.onConfigurationChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
this._repositoriesPromise = this.onWorkspaceFoldersChanged();
}
dispose() {
@ -139,7 +134,6 @@ export class GitService extends Disposable {
this._documentKeyMap.clear();
this._gitCache.clear();
this._remotesCache.clear();
this._trackedCache.clear();
this._versionedUriCache.clear();
}
@ -160,13 +154,12 @@ export class GitService extends Disposable {
this._trackedCache.clear();
}
private onConfigurationChanged() {
const encoding = workspace.getConfiguration('files').get<string>('encoding', 'utf8');
setDefaultEncoding(encoding);
private onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
const cfg = configuration.get<IConfig>();
if (!Objects.areEquivalent(cfg.advanced, this.config && this.config.advanced)) {
if (initializing || configuration.changed(e, configuration.name('advanced')('caching')('enabled').value)) {
if (cfg.advanced.caching.enabled) {
this._cacheDisposable && this._cacheDisposable.dispose();
@ -184,26 +177,15 @@ export class GitService extends Disposable {
}
}
// Only count the change if we aren't just starting up
const ignoreWhitespace = this.config === undefined
? cfg.blame.ignoreWhitespace
: this.config.blame.ignoreWhitespace;
this.config = cfg;
if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) {
// Only count the change if we aren't initializing
if (!initializing && configuration.changed(e, configuration.name('blame')('ignoreWhitespace').value, null)) {
this._gitCache.clear();
this.fireChange(GitChangeReason.GitCache);
}
}
private onRemoteProvidersChanged() {
this._remotesCache.clear();
this.fireChange(GitChangeReason.RemoteCache);
}
private onTextDocumentChanged(e: TextDocumentChangeEvent) {
let key = this._documentKeyMap.get(e.document);
if (key === undefined) {
@ -260,8 +242,13 @@ export class GitService extends Disposable {
this._repositories.set(fsPath, undefined);
}
else {
this._repositories.set(fsPath, new Repository(f, rp, this.onAnyRepositoryChanged.bind(this), this._suspended));
this._repositories.set(fsPath, new Repository(f, rp, this, this.onAnyRepositoryChanged.bind(this), this._suspended));
}
// const repoPaths = await this.searchForRepositories(f);
// if (repoPaths.length !== 0) {
// debugger;
// }
}
for (const f of e.removed) {
@ -283,6 +270,11 @@ export class GitService extends Disposable {
}
}
// private async searchForRepositories(folder: WorkspaceFolder): Promise<string[]> {
// const uris = await workspace.findFiles(new RelativePattern(folder, '**/.git/HEAD'));
// return Arrays.filterMapAsync(uris, uri => this.getRepoPathCore(path.dirname(uri.fsPath), true));
// }
private fireChange(reason: GitChangeReason) {
this._onDidChange.fire({ reason: reason });
}
@ -631,7 +623,7 @@ export class GitService extends Disposable {
Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`);
}
const promise = this.getDiffForFileCore(uri.repoPath, uri.fsPath, sha1, sha2, entry, key);
const promise = this.getDiffForFileCore(uri.repoPath, uri.fsPath, sha1, sha2, { encoding: GitService.getEncoding(uri) }, entry, key);
if (entry) {
Logger.log(`Add log cache for '${entry.key}:${key}'`);
@ -644,11 +636,11 @@ export class GitService extends Disposable {
return promise;
}
private async getDiffForFileCore(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, entry: GitCacheEntry | undefined, key: string): Promise<GitDiff | undefined> {
private async getDiffForFileCore(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, options: { encoding?: string }, entry: GitCacheEntry | undefined, key: string): Promise<GitDiff | undefined> {
const [file, root] = Git.splitPath(fileName, repoPath, false);
try {
const data = await Git.diff(root, file, sha1, sha2);
const data = await Git.diff(root, file, sha1, sha2, options);
const diff = GitDiffParser.parse(data);
return diff;
}
@ -879,35 +871,35 @@ export class GitService extends Disposable {
}
}
hasRemotes(repoPath: string | undefined): boolean {
async hasRemotes(repoPath: string | undefined): Promise<boolean> {
if (repoPath === undefined) return false;
const remotes = this._remotesCache.get(this.normalizeRepoPath(repoPath));
return remotes !== undefined && remotes.length > 0;
}
const repository = await this.getRepository(repoPath);
if (repository === undefined) return false;
private normalizeRepoPath(repoPath: string) {
return (repoPath.endsWith('/') ? repoPath : `${repoPath}/`).toLowerCase();
return repository.hasRemotes();
}
async getRemotes(repoPath: string | undefined): Promise<GitRemote[]> {
if (!repoPath) return [];
if (repoPath === undefined) return [];
Logger.log(`getRemotes('${repoPath}')`);
const normalizedRepoPath = this.normalizeRepoPath(repoPath);
const repository = await this.getRepository(repoPath);
if (repository !== undefined) return repository.getRemotes();
return this.getRemotesCore(repoPath);
}
let remotes = this._remotesCache.get(normalizedRepoPath);
if (remotes !== undefined) return remotes;
async getRemotesCore(repoPath: string | undefined, providerMap?: RemoteProviderMap): Promise<GitRemote[]> {
if (repoPath === undefined) return [];
const data = await Git.remote(repoPath);
remotes = GitRemoteParser.parse(data, repoPath);
Logger.log(`getRemotesCore('${repoPath}')`);
if (remotes !== undefined) {
this._remotesCache.set(normalizedRepoPath, remotes);
}
providerMap = providerMap || RemoteProviderFactory.createMap(configuration.get<IRemotesConfig[] | null | undefined>(configuration.name('remotes').value, null));
return remotes;
const data = await Git.remote(repoPath);
return GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providerMap));
}
async getRepoPath(filePath: string): Promise<string | undefined>;
@ -964,9 +956,10 @@ export class GitService extends Disposable {
}
async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> {
Logger.log(`getStashList('${repoPath}')`);
if (repoPath === undefined) return undefined;
Logger.log(`getStashList('${repoPath}')`);
const data = await Git.stash_list(repoPath);
const stash = GitStashParser.parse(data, repoPath);
return stash;
@ -985,9 +978,10 @@ export class GitService extends Disposable {
}
async getStatusForRepo(repoPath: string | undefined): Promise<GitStatus | undefined> {
Logger.log(`getStatusForRepo('${repoPath}')`);
if (repoPath === undefined) return undefined;
Logger.log(`getStatusForRepo('${repoPath}')`);
const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
const data = await Git.status(repoPath, porcelainVersion);
@ -1010,7 +1004,7 @@ export class GitService extends Disposable {
getVersionedFileText(repoPath: string, fileName: string, sha: string) {
Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`);
return Git.show(repoPath, fileName, sha);
return Git.show(repoPath, fileName, sha, { encoding: GitService.getEncoding(repoPath, fileName) });
}
hasGitUriForFile(editor: TextEditor): boolean {
@ -1126,6 +1120,15 @@ export class GitService extends Disposable {
return Git.stash_push(repoPath, pathspecs, message);
}
static getEncoding(repoPath: string, fileName: string): string;
static getEncoding(uri: Uri): string;
static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string {
const uri = (typeof repoPathOrUri === 'string')
? Uri.file(path.join(repoPathOrUri, fileName!))
: repoPathOrUri;
return Git.getEncoding(workspace.getConfiguration('files', uri).get<string>('encoding'));
}
static getGitPath(gitPath?: string): Promise<IGit> {
return Git.getGitPath(gitPath);
}
@ -1191,14 +1194,14 @@ export class GitService extends Disposable {
}
const parsed = path.parse(fileName!);
return Uri.parse(`${DocumentSchemes.GitLensGit}:${parsed.dir}${parsed.name}:${shortSha}${parsed.ext}?${JSON.stringify(data)}`);
return Uri.parse(`${DocumentSchemes.GitLensGit}:${path.join(parsed.dir, parsed.name)}:${shortSha}${parsed.ext}?${JSON.stringify(data)}`);
}
private static toGitUriData<T extends IGitUriData>(commit: IGitUriData, originalFileName?: string): T {
const fileName = Git.normalizePath(path.resolve(commit.repoPath, commit.fileName));
const fileName = Git.normalizePath(path.relative(commit.repoPath, commit.fileName));
const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha } as T;
if (originalFileName) {
data.originalFileName = Git.normalizePath(path.resolve(commit.repoPath, originalFileName));
data.originalFileName = Git.normalizePath(path.relative(commit.repoPath, originalFileName));
}
return data;
}

+ 40
- 33
src/logger.ts Целия файл

@ -1,7 +1,7 @@
'use strict';
import { ExtensionContext, OutputChannel, window, workspace } from 'vscode';
import { IConfig } from './configuration';
import { ExtensionKey, ExtensionOutputChannelName } from './constants';
import { ConfigurationChangeEvent, ExtensionContext, OutputChannel, window } from 'vscode';
import { configuration } from './configuration';
import { ExtensionOutputChannelName } from './constants';
import { Telemetry } from './telemetry';
const ConsolePrefix = `[${ExtensionOutputChannelName}]`;
@ -12,63 +12,70 @@ export enum OutputLevel {
Verbose = 'verbose'
}
let debug = false;
let level: OutputLevel = OutputLevel.Silent;
let output: OutputChannel;
function onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey);
if (cfg === undefined) return;
export class Logger {
if (cfg.debug !== debug || cfg.outputLevel !== level) {
debug = cfg.debug;
level = cfg.outputLevel;
static debug = false;
static level: OutputLevel = OutputLevel.Silent;
static output: OutputChannel | undefined;
if (level === OutputLevel.Silent) {
output && output.dispose();
}
else {
output = output || window.createOutputChannel(ExtensionOutputChannelName);
}
static configure(context: ExtensionContext) {
context.subscriptions.push(configuration.onDidChange(this.onConfigurationChanged, this));
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
}
export class Logger {
private static onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
static configure(context: ExtensionContext) {
context.subscriptions.push(workspace.onDidChangeConfiguration(onConfigurationChanged));
onConfigurationChanged();
let section = configuration.name('debug').value;
if (initializing || configuration.changed(e, section)) {
this.debug = configuration.get<boolean>(section);
}
section = configuration.name('outputLevel').value;
if (initializing || configuration.changed(e, section)) {
this.level = configuration.get<OutputLevel>(section);
if (this.level === OutputLevel.Silent) {
if (this.output !== undefined) {
this.output.dispose();
this.output = undefined;
}
}
else {
this.output = this.output || window.createOutputChannel(ExtensionOutputChannelName);
}
}
}
static log(message?: any, ...params: any[]): void {
if (debug) {
if (this.debug) {
console.log(this.timestamp, ConsolePrefix, message, ...params);
}
if (level === OutputLevel.Verbose) {
output.appendLine((debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' '));
if (this.output !== undefined && this.level === OutputLevel.Verbose) {
this.output.appendLine((this.debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' '));
}
}
static error(ex: Error, classOrMethod?: string, ...params: any[]): void {
if (debug) {
if (this.debug) {
console.error(this.timestamp, ConsolePrefix, classOrMethod, ex, ...params);
}
if (level !== OutputLevel.Silent) {
output.appendLine((debug ? [this.timestamp, classOrMethod, ex, ...params] : [classOrMethod, ex, ...params]).join(' '));
if (this.output !== undefined && this.level !== OutputLevel.Silent) {
this.output.appendLine((this.debug ? [this.timestamp, classOrMethod, ex, ...params] : [classOrMethod, ex, ...params]).join(' '));
}
Telemetry.trackException(ex);
}
static warn(message?: any, ...params: any[]): void {
if (debug) {
if (this.debug) {
console.warn(this.timestamp, ConsolePrefix, message, ...params);
}
if (level !== OutputLevel.Silent) {
output.appendLine((debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' '));
if (this.output !== undefined && this.level !== OutputLevel.Silent) {
this.output.appendLine((this.debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' '));
}
}

+ 17
- 0
src/system/function.ts Целия файл

@ -7,11 +7,28 @@ export interface IDeferred {
flush(...args: any[]): void;
}
interface IPropOfValue {
(): any;
value: string | undefined;
}
export namespace Functions {
export function debounce<T extends Function>(fn: T, wait?: number, options?: { leading?: boolean, maxWait?: number, trailing?: boolean }): T & IDeferred {
return _debounce(fn, wait, options);
}
export function propOf<T, K extends keyof T>(o: T, key: K) {
const propOfCore = <T, K extends keyof T>(o: T, key: K) => {
const value: string = (propOfCore as IPropOfValue).value === undefined
? key
: `${(propOfCore as IPropOfValue).value}.${key}`;
(propOfCore as IPropOfValue).value = value;
const fn = <Y extends keyof T[K]>(k: Y) => propOfCore(o[key], k);
return Object.assign(fn, { value: value });
};
return propOfCore(o, key);
}
export function once<T extends Function>(fn: T): T {
return _once(fn);
}

+ 10
- 10
src/views/branchHistoryNode.ts Целия файл

@ -1,10 +1,11 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode';
import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitBranch, GitService, GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { GitBranch, GitUri } from '../gitService';
export class BranchHistoryNode extends ExplorerNode {
@ -15,34 +16,33 @@ export class BranchHistoryNode extends ExplorerNode {
constructor(
public readonly branch: GitBranch,
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount);
const log = await this.explorer.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount);
if (log === undefined) return [];
const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git, this.branch))];
const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer, this.branch))];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.context));
children.push(new ShowAllNode('Show All Commits', this, this.explorer.context));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
let name = this.branch.getName();
if (!this.branch.remote && this.branch.tracking !== undefined && this.git.config.gitExplorer.showTrackingBranch) {
if (!this.branch.remote && this.branch.tracking !== undefined && this.explorer.config.showTrackingBranch) {
name += ` ${GlyphChars.Space}${GlyphChars.ArrowLeftRight}${GlyphChars.Space} ${this.branch.tracking}`;
}
const item = new TreeItem(`${this.branch!.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.branch.tracking ? `${this.resourceType}:remote` : this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'),
light: this.context.asAbsolutePath('images/light/icon-branch.svg')
dark: this.explorer.context.asAbsolutePath('images/dark/icon-branch.svg'),
light: this.explorer.context.asAbsolutePath('images/light/icon-branch.svg')
};
return item;

+ 10
- 9
src/views/branchesNode.ts Целия файл

@ -1,9 +1,10 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchHistoryNode } from './branchHistoryNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
export class BranchesNode extends ExplorerNode {
@ -11,31 +12,31 @@ export class BranchesNode extends ExplorerNode {
constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly repo: Repository,
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.git.getBranches(this.uri.repoPath!);
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name));
return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))];
return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.explorer))];
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Expanded);
const remotes = await this.git.getRemotes(this.uri.repoPath!);
const remotes = await this.repo.getRemotes();
item.contextValue = (remotes !== undefined && remotes.length > 0)
? `${this.resourceType}:remote`
: this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-branch.svg'),
light: this.context.asAbsolutePath('images/light/icon-branch.svg')
dark: this.explorer.context.asAbsolutePath('images/dark/icon-branch.svg'),
light: this.explorer.context.asAbsolutePath('images/light/icon-branch.svg')
};
return item;

+ 10
- 10
src/views/commitFileNode.ts Целия файл

@ -1,8 +1,9 @@
'use strict';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Command, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitCommitType, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitCommitType, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
import * as path from 'path';
export enum CommitFileNodeDisplayAs {
@ -24,8 +25,7 @@ export class CommitFileNode extends ExplorerNode {
constructor(
public readonly status: IGitStatusFile,
public commit: GitCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
protected readonly explorer: GitExplorer,
private displayAs: CommitFileNodeDisplayAs = CommitFileNodeDisplayAs.Commit,
public readonly branch?: GitBranch
) {
@ -39,7 +39,7 @@ export class CommitFileNode extends ExplorerNode {
async getTreeItem(): Promise<TreeItem> {
if (this.commit.type !== GitCommitType.File) {
const log = await this.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
const log = await this.explorer.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
if (log !== undefined) {
this.commit = log.commits.get(this.commit.sha) || this.commit;
}
@ -53,8 +53,8 @@ export class CommitFileNode extends ExplorerNode {
: getGitStatusIcon(this.status.status);
item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon))
dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.explorer.context.asAbsolutePath(path.join('images', 'light', icon))
};
item.command = this.getCommand();
@ -79,7 +79,7 @@ export class CommitFileNode extends ExplorerNode {
this._label = (this.displayAs & CommitFileNodeDisplayAs.CommitLabel)
? CommitFormatter.fromTemplate(this.getCommitTemplate(), this.commit, {
truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat
dataFormat: this.explorer.git.config.defaultDateFormat
} as ICommitFormatOptions)
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(),
this.status,
@ -98,11 +98,11 @@ export class CommitFileNode extends ExplorerNode {
}
protected getCommitTemplate() {
return this.git.config.gitExplorer.commitFormat;
return this.explorer.config.commitFormat;
}
protected getCommitFileTemplate() {
return this.git.config.gitExplorer.commitFileFormat;
return this.explorer.config.commitFileFormat;
}
getCommand(): Command | undefined {

+ 12
- 12
src/views/commitNode.ts Целия файл

@ -1,11 +1,12 @@
'use strict';
import { Arrays, Iterables } from '../system';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Command, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { GitExplorerFilesLayout } from '../configuration';
import { FolderNode, IFileExplorerNode } from './folderNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as path from 'path';
@ -16,8 +17,7 @@ export class CommitNode extends ExplorerNode {
constructor(
public readonly commit: GitLogCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
private readonly explorer: GitExplorer,
public readonly branch?: GitBranch
) {
super(new GitUri(commit.uri, commit));
@ -27,21 +27,21 @@ export class CommitNode extends ExplorerNode {
async getChildren(): Promise<ExplorerNode[]> {
const repoPath = this.repoPath;
const log = await this.git.getLogForRepo(repoPath, this.commit.sha, 1);
const log = await this.explorer.git.getLogForRepo(repoPath, this.commit.sha, 1);
if (log === undefined) return [];
const commit = Iterables.first(log.commits.values());
if (commit === undefined) return [];
let children: IFileExplorerNode[] = [
...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))
...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.explorer, CommitFileNodeDisplayAs.File, this.branch))
];
if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
if (this.explorer.config.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);
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.explorer.config.files.compact);
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.explorer);
children = await root.getChildren() as IFileExplorerNode[];
}
else {
@ -51,15 +51,15 @@ export class CommitNode extends ExplorerNode {
}
getTreeItem(): TreeItem {
const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.commitFormat, this.commit, {
const item = new TreeItem(CommitFormatter.fromTemplate(this.explorer.config.commitFormat, this.commit, {
truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat
dataFormat: this.explorer.git.config.defaultDateFormat
} as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'),
light: this.context.asAbsolutePath('images/light/icon-commit.svg')
dark: this.explorer.context.asAbsolutePath('images/dark/icon-commit.svg'),
light: this.explorer.context.asAbsolutePath('images/light/icon-commit.svg')
};
return item;

+ 1
- 1
src/views/fileHistoryNode.ts Целия файл

@ -23,7 +23,7 @@ export class FileHistoryNode extends ExplorerNode {
const log = await this.explorer.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha);
if (log === undefined) return [new MessageNode('No file history')];
return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer.context, this.explorer.git, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))];
return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))];
}
getTreeItem(): TreeItem {

+ 4
- 3
src/views/folderNode.ts Целия файл

@ -3,6 +3,7 @@ import { Arrays, Objects } from '../system';
import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { GitExplorerFilesLayout, IGitExplorerConfig } from '../configuration';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitUri } from '../gitService';
export interface IFileExplorerNode extends ExplorerNode {
@ -23,7 +24,7 @@ export class FolderNode extends ExplorerNode {
public readonly folderName: string,
public readonly relativePath: string | undefined,
public readonly root: Arrays.IHierarchicalItem<IFileExplorerNode>,
private readonly config: IGitExplorerConfig
private readonly explorer: GitExplorer
) {
super(new GitUri(Uri.file(repoPath), { repoPath: repoPath, fileName: repoPath }));
}
@ -33,12 +34,12 @@ export class FolderNode extends ExplorerNode {
let children: (FolderNode | IFileExplorerNode)[];
const nesting = FolderNode.getFileNesting(this.config, this.root.descendants, this.relativePath === undefined);
const nesting = FolderNode.getFileNesting(this.explorer.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));
children.push(new FolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.explorer));
continue;
}

+ 82
- 75
src/views/gitExplorer.ts Целия файл

@ -1,9 +1,9 @@
'use strict';
import { Arrays, Functions, Objects } from '../system';
import { commands, Disposable, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode';
import { Arrays, Functions } from '../system';
import { commands, ConfigurationChangeEvent, Disposable, Event, EventEmitter, ExtensionContext, TextDocumentShowOptions, TextEditor, TreeDataProvider, TreeItem, Uri, window, workspace } from 'vscode';
import { Commands, DiffWithCommandArgs, DiffWithCommandArgsRevision, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs } from '../commands';
import { UriComparer } from '../comparers';
import { ExtensionKey, GitExplorerFilesLayout, IConfig } from '../configuration';
import { configuration, ExtensionKey, GitExplorerFilesLayout, IGitExplorerConfig } from '../configuration';
import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants';
import { BranchHistoryNode, CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoriesNode, RepositoryNode, StashNode } from './explorerNodes';
import { GitChangeEvent, GitChangeReason, GitService, GitUri } from '../gitService';
@ -38,7 +38,7 @@ export interface RefreshNodeCommandArgs {
export class GitExplorer implements TreeDataProvider<ExplorerNode> {
private _config: IConfig;
private _config: IGitExplorerConfig;
private _root?: ExplorerNode;
private _view: GitExplorerView | undefined;
@ -56,8 +56,8 @@ export class GitExplorer implements TreeDataProvider {
public readonly context: ExtensionContext,
public readonly git: GitService
) {
commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOn', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, true), this);
commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, false), this);
commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOn', () => this.setAutoRefresh(configuration.get<boolean>(configuration.name('gitExplorer')('autoRefresh').value), true), this);
commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(configuration.get<boolean>(configuration.name('gitExplorer')('autoRefresh').value), false), this);
commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToAuto', () => this.setFilesLayout(GitExplorerFilesLayout.Auto), this);
commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToList', () => this.setFilesLayout(GitExplorerFilesLayout.List), this);
commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToTree', () => this.setFilesLayout(GitExplorerFilesLayout.Tree), this);
@ -76,27 +76,85 @@ export class GitExplorer implements TreeDataProvider {
commands.registerCommand('gitlens.gitExplorer.openChangedFileRevisions', this.openChangedFileRevisions, this);
commands.registerCommand('gitlens.gitExplorer.applyChanges', this.applyChanges, this);
const editorChangedFn = Functions.debounce(this.onActiveEditorChanged, 500);
context.subscriptions.push(window.onDidChangeActiveTextEditor(editorChangedFn, this));
context.subscriptions.push(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this),
configuration.onDidChange(this.onConfigurationChanged, this)
);
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
const visibleEditorsChangedFn = Functions.debounce(this.onVisibleEditorsChanged, 500);
context.subscriptions.push(window.onDidChangeVisibleTextEditors(visibleEditorsChangedFn, this));
private async onActiveEditorChanged(editor: TextEditor | undefined) {
if (this._view !== GitExplorerView.History) return;
context.subscriptions.push(workspace.onDidChangeConfiguration(this.onConfigurationChanged, this));
const root = await this.getRootNode(editor);
if (!this.setRoot(root)) return;
this.onConfigurationChanged();
this.refresh(RefreshReason.ActiveEditorChanged, root);
}
get autoRefresh() {
return this._config.gitExplorer.autoRefresh && this.context.workspaceState.get<boolean>(WorkspaceState.GitExplorerAutoRefresh, true);
private async onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
const section = configuration.name('gitExplorer');
if (!initializing && !configuration.changed(e, section.value)) return;
const cfg = configuration.get<IGitExplorerConfig>(section.value);
if (initializing || configuration.changed(e, section('autoRefresh').value)) {
this.setAutoRefresh(cfg.autoRefresh);
}
if (initializing || configuration.changed(e, section('files')('layout').value)) {
setCommandContext(CommandContext.GitExplorerFilesLayout, cfg.files.layout);
}
let view = cfg.view;
if (view === GitExplorerView.Auto) {
view = this.context.workspaceState.get<GitExplorerView>(WorkspaceState.GitExplorerView, GitExplorerView.Repository);
}
if (initializing) {
this._view = view;
setCommandContext(CommandContext.GitExplorerView, this._view);
this.setRoot(await this.getRootNode(window.activeTextEditor));
}
else {
this.reset(view);
}
this._config = cfg;
}
private onGitChanged(e: GitChangeEvent) {
if (this._root === undefined || this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return;
this.clearRoot();
Logger.log(`GitExplorer[view=${this._view}].onGitChanged(${e.reason})`);
this.refresh(RefreshReason.RepoChanged);
}
private onVisibleEditorsChanged(editors: TextEditor[]) {
if (this._root === undefined || this._view !== GitExplorerView.History) return;
// If we have no visible editors, or no trackable visible editors reset the view
if (editors.length === 0 || !editors.some(e => e.document && this.git.isTrackable(e.document.uri))) {
this.clearRoot();
this.refresh(RefreshReason.VisibleEditorsChanged);
}
}
get config() {
return this._config.gitExplorer;
get autoRefresh() {
return configuration.get<boolean>(configuration.name('gitExplorer')('autoRefresh').value) &&
this.context.workspaceState.get<boolean>(WorkspaceState.GitExplorerAutoRefresh, true);
}
async getTreeItem(node: ExplorerNode): Promise<TreeItem> {
return node.getTreeItem();
get config(): IGitExplorerConfig {
return this._config;
}
private _loading: Promise<void> | undefined;
@ -116,6 +174,10 @@ export class GitExplorer implements TreeDataProvider {
return node.getChildren();
}
async getTreeItem(node: ExplorerNode): Promise<TreeItem> {
return node.getTreeItem();
}
private async getRootNode(editor?: TextEditor): Promise<ExplorerNode | undefined> {
switch (this._view) {
case GitExplorerView.History: {
@ -156,61 +218,6 @@ export class GitExplorer implements TreeDataProvider {
return new HistoryNode(uri, repo, this);
}
private async onActiveEditorChanged(editor: TextEditor | undefined) {
if (this._view !== GitExplorerView.History) return;
const root = await this.getRootNode(editor);
if (!this.setRoot(root)) return;
this.refresh(RefreshReason.ActiveEditorChanged, root);
}
private onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
const changed = !Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer);
if (cfg.gitExplorer.autoRefresh !== (this._config && this._config.gitExplorer.autoRefresh)) {
this.setAutoRefresh(cfg.gitExplorer.autoRefresh);
}
if (cfg.gitExplorer.files.layout !== (this._config && this._config.gitExplorer.files.layout)) {
setCommandContext(CommandContext.GitExplorerFilesLayout, cfg.gitExplorer.files.layout);
}
this._config = cfg;
if (changed) {
let view = cfg.gitExplorer.view;
if (view === GitExplorerView.Auto) {
view = this.context.workspaceState.get<GitExplorerView>(WorkspaceState.GitExplorerView, GitExplorerView.Repository);
}
this.reset(view);
}
}
private onGitChanged(e: GitChangeEvent) {
if (this._root === undefined || this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return;
this.clearRoot();
Logger.log(`GitExplorer[view=${this._view}].onGitChanged(${e.reason})`);
this.refresh(RefreshReason.RepoChanged);
}
private onVisibleEditorsChanged(editors: TextEditor[]) {
if (this._root === undefined || this._view !== GitExplorerView.History) return;
// If we have no visible editors, or no trackable visible editors reset the view
if (editors.length === 0 || !editors.some(e => e.document && this.git.isTrackable(e.document.uri))) {
this.clearRoot();
this.refresh(RefreshReason.VisibleEditorsChanged);
}
}
async refresh(reason: RefreshReason | undefined, root?: ExplorerNode) {
if (reason === undefined) {
reason = RefreshReason.Command;
@ -270,7 +277,7 @@ export class GitExplorer implements TreeDataProvider {
setView(view: GitExplorerView) {
if (this._view === view) return;
if (this._config.gitExplorer.view === GitExplorerView.Auto) {
if (configuration.get<GitExplorerView>(configuration.name('gitExplorer')('view').value) === GitExplorerView.Auto) {
this.context.workspaceState.update(WorkspaceState.GitExplorerView, view);
}
@ -407,6 +414,6 @@ export class GitExplorer implements TreeDataProvider {
}
private async setFilesLayout(layout: GitExplorerFilesLayout) {
await workspace.getConfiguration(ExtensionKey).update('gitExplorer.files.layout', layout, true);
return workspace.getConfiguration(ExtensionKey).update(configuration.name('gitExplorer')('files')('layout').value, layout, true);
}
}

+ 7
- 6
src/views/remoteNode.ts Целия файл

@ -1,10 +1,11 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchHistoryNode } from './branchHistoryNode';
import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitRemote, GitRemoteType, GitService, GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { GitRemote, GitRemoteType, GitUri, Repository } from '../gitService';
export class RemoteNode extends ExplorerNode {
@ -13,18 +14,18 @@ export class RemoteNode extends ExplorerNode {
constructor(
public readonly remote: GitRemote,
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly repo: Repository,
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.git.getBranches(this.uri.repoPath!);
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => a.name.localeCompare(b.name));
return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))];
return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.explorer))];
}
getTreeItem(): TreeItem {

+ 9
- 8
src/views/remotesNode.ts Целия файл

@ -1,8 +1,9 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
import { RemoteNode } from './remoteNode';
export class RemotesNode extends ExplorerNode {
@ -11,18 +12,18 @@ export class RemotesNode extends ExplorerNode {
constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly repo: Repository,
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const remotes = await this.git.getRemotes(this.uri.repoPath!);
const remotes = await this.repo.getRemotes();
if (remotes === undefined || remotes.length === 0) return [new MessageNode('No remotes configured')];
remotes.sort((a, b) => a.name.localeCompare(b.name));
return [...Iterables.map(remotes, r => new RemoteNode(r, this.uri, this.context, this.git))];
return [...Iterables.map(remotes, r => new RemoteNode(r, this.uri, this.repo, this.explorer))];
}
getTreeItem(): TreeItem {
@ -30,8 +31,8 @@ export class RemotesNode extends ExplorerNode {
item.contextValue = this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-remote.svg'),
light: this.context.asAbsolutePath('images/light/icon-remote.svg')
dark: this.explorer.context.asAbsolutePath('images/dark/icon-remote.svg'),
light: this.explorer.context.asAbsolutePath('images/light/icon-remote.svg')
};
return item;

+ 3
- 3
src/views/repositoryNode.ts Целия файл

@ -28,9 +28,9 @@ export class RepositoryNode extends ExplorerNode {
this.children = [
new StatusNode(this.uri, this.repo, this, this.explorer),
new BranchesNode(this.uri, this.explorerpan>.context, this.explorer.git),
new RemotesNode(this.uri, this.explorerpan>.context, this.explorer.git),
new StashesNode(this.uri, this.explorerpan>.context, this.explorer.git)
new BranchesNode(this.uri, this.repo, this.explorer),
new RemotesNode(this.uri, this.repo, this.explorer),
new StashesNode(this.uri, this.repo, this.explorer)
];
return this.children;
}

+ 7
- 8
src/views/stashFileNode.ts Целия файл

@ -1,8 +1,8 @@
'use strict';
import { ExtensionContext } from 'vscode';
import { ResourceType } from './explorerNode';
import { GitLogCommit, GitService, IGitStatusFile } from '../gitService';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitLogCommit, IGitStatusFile } from '../gitService';
export class StashFileNode extends CommitFileNode {
@ -11,17 +11,16 @@ export class StashFileNode extends CommitFileNode {
constructor(
status: IGitStatusFile,
commit: GitLogCommit,
context: ExtensionContext,
git: GitService
explorer: GitExplorer
) {
super(status, commit, context, git, CommitFileNodeDisplayAs.File);
super(status, commit, explorer, CommitFileNodeDisplayAs.File);
}
protected getCommitTemplate() {
return this.git.config.gitExplorer.stashFormat;
return this.explorer.config.stashFormat;
}
protected getCommitFileTemplate() {
return this.git.config.gitExplorer.stashFileFormat;
return this.explorer.config.stashFileFormat;
}
}

+ 8
- 8
src/views/stashNode.ts Целия файл

@ -1,8 +1,9 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, GitService, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { CommitFormatter, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService';
import { StashFileNode } from './stashFileNode';
export class StashNode extends ExplorerNode {
@ -11,8 +12,7 @@ export class StashNode extends ExplorerNode {
constructor(
public readonly commit: GitStashCommit,
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly explorer: GitExplorer
) {
super(new GitUri(commit.uri, commit));
}
@ -21,7 +21,7 @@ export class StashNode extends ExplorerNode {
const statuses = (this.commit as GitStashCommit).fileStatuses;
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
const log = await this.git.getLogForRepo(this.commit.repoPath, `${(this.commit as GitStashCommit).stashName}^3`, 1);
const log = await this.explorer.git.getLogForRepo(this.commit.repoPath, `${(this.commit as GitStashCommit).stashName}^3`, 1);
if (log !== undefined) {
const commit = Iterables.first(log.commits.values());
if (commit !== undefined && commit.fileStatuses.length !== 0) {
@ -31,15 +31,15 @@ export class StashNode extends ExplorerNode {
}
}
const children = statuses.map(s => new StashFileNode(s, this.commit.toFileCommit(s), this.context, this.git));
const children = statuses.map(s => new StashFileNode(s, this.commit.toFileCommit(s), this.explorer));
children.sort((a, b) => a.label!.localeCompare(b.label!));
return children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.stashFormat, this.commit, {
const item = new TreeItem(CommitFormatter.fromTemplate(this.explorer.config.stashFormat, this.commit, {
truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat
dataFormat: this.explorer.git.config.defaultDateFormat
} as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
return item;

+ 9
- 8
src/views/stashesNode.ts Целия файл

@ -1,8 +1,9 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
import { StashNode } from './stashNode';
export class StashesNode extends ExplorerNode {
@ -11,17 +12,17 @@ export class StashesNode extends ExplorerNode {
constructor(
uri: GitUri,
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly repo: Repository,
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const stash = await this.git.getStashList(this.uri.repoPath!);
const stash = await this.repo.getStashList();
if (stash === undefined) return [new MessageNode('No stashed changes')];
return [...Iterables.map(stash.commits.values(), c => new StashNode(c, this.context, this.git))];
return [...Iterables.map(stash.commits.values(), c => new StashNode(c, this.explorer))];
}
getTreeItem(): TreeItem {
@ -29,8 +30,8 @@ export class StashesNode extends ExplorerNode {
item.contextValue = this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-stash.svg'),
light: this.context.asAbsolutePath('images/light/icon-stash.svg')
dark: this.explorer.context.asAbsolutePath('images/dark/icon-stash.svg'),
light: this.explorer.context.asAbsolutePath('images/light/icon-stash.svg')
};
return item;

+ 8
- 8
src/views/statusFileCommitsNode.ts Целия файл

@ -1,9 +1,10 @@
'use strict';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Command, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { getGitStatusIcon, GitBranch, GitLogCommit, GitUri, IGitStatusFile, IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
import * as path from 'path';
export class StatusFileCommitsNode extends ExplorerNode {
@ -14,15 +15,14 @@ export class StatusFileCommitsNode extends ExplorerNode {
public readonly repoPath: string,
public readonly status: IGitStatusFile,
public readonly commits: GitLogCommit[],
protected readonly context: ExtensionContext,
protected readonly git: GitService,
private readonly explorer: GitExplorer,
public readonly branch?: GitBranch
) {
super(new GitUri(Uri.file(path.resolve(repoPath, status.fileName)), { repoPath: repoPath, fileName: status.fileName, sha: 'HEAD' }));
}
async getChildren(): Promise<ExplorerNode[]> {
return this.commits.map(c => new CommitFileNode(this.status, c, this.context, this.git, CommitFileNodeDisplayAs.Commit, this.branch));
return this.commits.map(c => new CommitFileNode(this.status, c, this.explorer, CommitFileNodeDisplayAs.Commit, this.branch));
}
async getTreeItem(): Promise<TreeItem> {
@ -31,8 +31,8 @@ export class StatusFileCommitsNode extends ExplorerNode {
const icon = getGitStatusIcon(this.status.status);
item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon))
dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.explorer.context.asAbsolutePath(path.join('images', 'light', icon))
};
if (this.commits.length === 1 && this.commits[0].isUncommitted) {
@ -58,7 +58,7 @@ export class StatusFileCommitsNode extends ExplorerNode {
private _label: string | undefined;
get label() {
if (this._label === undefined) {
this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat,
this._label = StatusFileFormatter.fromTemplate(this.explorer.config.statusFileFormat,
{ ...this.status, commit: this.commit } as IGitStatusFileWithCommit,
{ relativePath: this.relativePath } as IStatusFormatOptions);
}

+ 13
- 13
src/views/statusFilesNode.ts Целия файл

@ -1,9 +1,10 @@
'use strict';
import { Arrays, Iterables, Objects } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { GitExplorerFilesLayout } from '../configuration';
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { FolderNode, IFileExplorerNode } from './folderNode';
import { GitExplorer } from './gitExplorer';
import { GitBranch, GitCommitType, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService';
import { StatusFileCommitsNode } from './statusFileCommitsNode';
import * as path from 'path';
@ -18,8 +19,7 @@ export class StatusFilesNode extends ExplorerNode {
constructor(
public readonly status: GitStatus,
public readonly range: string | undefined,
protected readonly context: ExtensionContext,
protected readonly git: GitService,
private readonly explorer: GitExplorer,
public readonly branch?: GitBranch
) {
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
@ -33,7 +33,7 @@ export class StatusFilesNode extends ExplorerNode {
let log: GitLog | undefined;
if (this.range !== undefined) {
log = await this.git.getLogForRepo(repoPath, this.range, this.maxCount);
log = await this.explorer.git.getLogForRepo(repoPath, this.range, this.maxCount);
if (log !== undefined) {
statuses = Array.from(Iterables.flatMap(log.commits.values(), c => {
return c.fileStatuses.map(s => {
@ -56,14 +56,14 @@ export class StatusFilesNode extends ExplorerNode {
const groups = Arrays.groupBy(statuses, s => s.fileName);
let children: IFileExplorerNode[] = [
...Iterables.map(Objects.values(groups), statuses => new StatusFileCommitsNode(repoPath, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch))
...Iterables.map(Objects.values(groups), statuses => new StatusFileCommitsNode(repoPath, statuses[statuses.length - 1], statuses.map(s => s.commit), this.explorer, this.branch))
];
if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
if (this.explorer.config.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);
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.explorer.config.files.compact);
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.explorer);
children = await root.getChildren() as IFileExplorerNode[];
}
else {
@ -71,7 +71,7 @@ export class StatusFilesNode extends ExplorerNode {
}
if (log !== undefined && log.truncated) {
(children as (IFileExplorerNode | ShowAllNode)[]).push(new ShowAllNode('Show All Changes', this, this.context));
(children as (IFileExplorerNode | ShowAllNode)[]).push(new ShowAllNode('Show All Changes', this, this.explorer.context));
}
return children;
}
@ -80,7 +80,7 @@ export class StatusFilesNode extends ExplorerNode {
let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0;
if (this.status.upstream !== undefined) {
const stats = await this.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`);
const stats = await this.explorer.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`);
if (stats !== undefined) {
files += stats.files;
}
@ -90,14 +90,14 @@ export class StatusFilesNode extends ExplorerNode {
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath(`images/dark/icon-diff.svg`),
light: this.context.asAbsolutePath(`images/light/icon-diff.svg`)
dark: this.explorer.context.asAbsolutePath(`images/dark/icon-diff.svg`),
light: this.explorer.context.asAbsolutePath(`images/light/icon-diff.svg`)
};
return item;
}
private get includeWorkingTree(): boolean {
return this.git.config.gitExplorer.includeWorkingTree;
return this.explorer.config.includeWorkingTree;
}
}

+ 3
- 3
src/views/statusNode.ts Целия файл

@ -28,18 +28,18 @@ export class StatusNode extends ExplorerNode {
if (status === undefined) return this.children;
if (status.state.behind) {
this.children.push(new StatusUpstreamNode(status, 'behind', this.explorer.context, this.explorer.git));
this.children.push(new StatusUpstreamNode(status, 'behind', this.explorer));
}
if (status.state.ahead) {
this.children.push(new StatusUpstreamNode(status, 'ahead', this.explorer.context, this.explorer.git));
this.children.push(new StatusUpstreamNode(status, 'ahead', this.explorer));
}
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) {
const range = status.upstream
? `${status.upstream}..${status.branch}`
: undefined;
this.children.push(new StatusFilesNode(status, range, this.explorer.context, this.explorer.git));
this.children.push(new StatusFilesNode(status, range, this.explorer));
}
return this.children;

+ 10
- 10
src/views/statusUpstreamNode.ts Целия файл

@ -1,9 +1,10 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { CommitNode } from './commitNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitService, GitStatus, GitUri } from '../gitService';
import { GitExplorer } from './gitExplorer';
import { GitStatus, GitUri } from '../gitService';
export class StatusUpstreamNode extends ExplorerNode {
@ -12,8 +13,7 @@ export class StatusUpstreamNode extends ExplorerNode {
constructor(
public readonly status: GitStatus,
public readonly direction: 'ahead' | 'behind',
protected readonly context: ExtensionContext,
protected readonly git: GitService
private readonly explorer: GitExplorer
) {
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
}
@ -23,22 +23,22 @@ export class StatusUpstreamNode extends ExplorerNode {
? `${this.status.upstream}..${this.status.branch}`
: `${this.status.branch}..${this.status.upstream}`;
let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0);
let log = await this.explorer.git.getLogForRepo(this.uri.repoPath!, range, 0);
if (log === undefined) return [];
if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git))];
if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))];
// Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up
const commits = Array.from(log.commits.values());
const commit = commits[commits.length - 1];
if (commit.previousSha === undefined) {
log = await this.git.getLogForRepo(this.uri.repoPath!, commit.sha, 2);
log = await this.explorer.git.getLogForRepo(this.uri.repoPath!, commit.sha, 2);
if (log !== undefined) {
commits[commits.length - 1] = Iterables.first(log.commits.values());
}
}
return [...Iterables.map(commits, c => new CommitNode(c, this.context, this.git))];
return [...Iterables.map(commits, c => new CommitNode(c, this.explorer))];
}
async getTreeItem(): Promise<TreeItem> {
@ -50,8 +50,8 @@ export class StatusUpstreamNode extends ExplorerNode {
item.contextValue = this.resourceType;
item.iconPath = {
dark: this.context.asAbsolutePath(`images/dark/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`),
light: this.context.asAbsolutePath(`images/light/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`)
dark: this.explorer.context.asAbsolutePath(`images/dark/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`),
light: this.explorer.context.asAbsolutePath(`images/light/icon-${this.direction === 'ahead' ? 'upload' : 'download'}.svg`)
};
return item;

Зареждане…
Отказ
Запис