Browse Source

Adds Branches and History views

main
Eric Amodio 4 years ago
parent
commit
eee360bc95
36 changed files with 1854 additions and 387 deletions
  1. +393
    -49
      package.json
  2. +2
    -2
      src/commands/setViewsLayout.ts
  3. +132
    -43
      src/config.ts
  4. +31
    -2
      src/container.ts
  5. +28
    -20
      src/git/gitService.ts
  6. +7
    -5
      src/git/models/branch.ts
  7. +2
    -1
      src/git/models/models.ts
  8. +62
    -26
      src/git/models/repository.ts
  9. +7
    -1
      src/trackers/trackedDocument.ts
  10. +403
    -0
      src/views/branchesView.ts
  11. +18
    -26
      src/views/compareView.ts
  12. +15
    -21
      src/views/fileHistoryView.ts
  13. +326
    -0
      src/views/historyView.ts
  14. +15
    -21
      src/views/lineHistoryView.ts
  15. +49
    -19
      src/views/nodes/branchNode.ts
  16. +120
    -0
      src/views/nodes/branchTrackingStatusFilesNode.ts
  17. +28
    -11
      src/views/nodes/branchTrackingStatusNode.ts
  18. +15
    -9
      src/views/nodes/branchesNode.ts
  19. +3
    -3
      src/views/nodes/commitNode.ts
  20. +3
    -1
      src/views/nodes/compareNode.ts
  21. +1
    -1
      src/views/nodes/fileHistoryNode.ts
  22. +3
    -3
      src/views/nodes/folderNode.ts
  23. +1
    -1
      src/views/nodes/lineHistoryNode.ts
  24. +3
    -3
      src/views/nodes/reflogRecordNode.ts
  25. +1
    -1
      src/views/nodes/remoteNode.ts
  26. +11
    -9
      src/views/nodes/repositoryNode.ts
  27. +3
    -3
      src/views/nodes/resultsCommitsNode.ts
  28. +3
    -3
      src/views/nodes/resultsFilesNode.ts
  29. +2
    -2
      src/views/nodes/searchResultsCommitsNode.ts
  30. +3
    -3
      src/views/nodes/stashNode.ts
  31. +3
    -3
      src/views/nodes/stashesNode.ts
  32. +12
    -4
      src/views/nodes/statusFilesNode.ts
  33. +2
    -0
      src/views/nodes/viewNode.ts
  34. +16
    -53
      src/views/repositoriesView.ts
  35. +18
    -26
      src/views/searchView.ts
  36. +113
    -12
      src/views/viewBase.ts

+ 393
- 49
package.json View File

@ -1502,6 +1502,60 @@
"markdownDescription": "Specifies the description format of committed changes in the views. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.branches.avatars": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show avatar images instead of commit (or status) icons in the _Branches_ view",
"scope": "window"
},
"gitlens.views.branches.branches.layout": {
"type": "string",
"default": "tree",
"enum": [
"list",
"tree"
],
"enumDescriptions": [
"Displays branches as a list",
"Displays branches as a tree when branch names contain slashes `/`"
],
"markdownDescription": "Specifies how the _Branches_ view will display branches",
"scope": "window"
},
"gitlens.views.branches.files.compact": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to compact (flatten) unnecessary file nesting in the _Branches_ view. Only applies when `#gitlens.views.repositories.files.layout#` is set to `tree` or `auto`",
"scope": "window"
},
"gitlens.views.branches.files.layout": {
"type": "string",
"default": "auto",
"enum": [
"auto",
"list",
"tree"
],
"enumDescriptions": [
"Automatically switches between displaying files as a `tree` or `list` based on the `#gitlens.views.branches.files.threshold#` value and the number of files at each nesting level",
"Displays files as a list",
"Displays files as a tree"
],
"markdownDescription": "Specifies how the _Branches_ view will display files",
"scope": "window"
},
"gitlens.views.branches.files.threshold": {
"type": "number",
"default": 5,
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Branches_ view. Only applies when `#gitlens.views.branches.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.branches.showTrackingBranch": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show the tracking branch when displaying local branches in the _Branches_ view",
"scope": "window"
},
"gitlens.views.compare.avatars": {
"type": "boolean",
"default": true,
@ -1598,6 +1652,46 @@
"markdownDescription": "Specifies where to show the _File History_ view",
"scope": "window"
},
"gitlens.views.history.avatars": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show avatar images instead of commit (or status) icons in the _History_ view",
"scope": "window"
},
"gitlens.views.history.files.compact": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to compact (flatten) unnecessary file nesting in the _History_ view. Only applies when `#gitlens.views.repositories.files.layout#` is set to `tree` or `auto`",
"scope": "window"
},
"gitlens.views.history.files.layout": {
"type": "string",
"default": "auto",
"enum": [
"auto",
"list",
"tree"
],
"enumDescriptions": [
"Automatically switches between displaying files as a `tree` or `list` based on the `#gitlens.views.history.files.threshold#` value and the number of files at each nesting level",
"Displays files as a list",
"Displays files as a tree"
],
"markdownDescription": "Specifies how the _History_ view will display files",
"scope": "window"
},
"gitlens.views.history.files.threshold": {
"type": "number",
"default": 5,
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _History_ view. Only applies when `#gitlens.views.history.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.history.showTrackingBranch": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show the tracking branch when displaying local branches in the _History_ view",
"scope": "window"
},
"gitlens.views.lineHistory.avatars": {
"type": "boolean",
"default": true,
@ -2736,31 +2830,6 @@
"category": "GitLens"
},
{
"command": "gitlens.views.repositories.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.fileHistory.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.lineHistory.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.compare.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.search.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.pruneRemote",
"title": "Prune",
"category": "GitLens"
@ -3083,6 +3152,11 @@
"category": "GitLens"
},
{
"command": "gitlens.views.repositories.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.repositories.refresh",
"title": "Refresh",
"category": "GitLens",
@ -3157,6 +3231,65 @@
"category": "GitLens"
},
{
"command": "gitlens.views.branches.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.branches.refresh",
"title": "Refresh",
"category": "GitLens",
"icon": "$(refresh)"
},
{
"command": "gitlens.views.branches.setBranchesLayoutToList",
"title": "Toggle Branch Layout (Tree)",
"category": "GitLens",
"icon": "$(list-tree)"
},
{
"command": "gitlens.views.branches.setBranchesLayoutToTree",
"title": "Toggle Branch Layout (List)",
"category": "GitLens",
"icon": "$(list-flat)"
},
{
"command": "gitlens.views.branches.setFilesLayoutToAuto",
"title": "Toggle File Layout (Tree)",
"category": "GitLens",
"icon": "$(list-tree)"
},
{
"command": "gitlens.views.branches.setFilesLayoutToList",
"title": "Toggle File Layout (Auto)",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-view-auto.svg",
"light": "images/light/icon-view-auto.svg"
}
},
{
"command": "gitlens.views.branches.setFilesLayoutToTree",
"title": "Toggle File Layout (List)",
"category": "GitLens",
"icon": "$(list-flat)"
},
{
"command": "gitlens.views.branches.setShowAvatarsOn",
"title": "Show Avatars",
"category": "GitLens"
},
{
"command": "gitlens.views.branches.setShowAvatarsOff",
"title": "Hide Avatars",
"category": "GitLens"
},
{
"command": "gitlens.views.fileHistory.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.fileHistory.refresh",
"title": "Refresh",
"category": "GitLens",
@ -3225,6 +3358,53 @@
"category": "GitLens"
},
{
"command": "gitlens.views.history.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.history.refresh",
"title": "Refresh",
"category": "GitLens",
"icon": "$(refresh)"
},
{
"command": "gitlens.views.history.setFilesLayoutToAuto",
"title": "Toggle File Layout (Tree)",
"category": "GitLens",
"icon": "$(list-tree)"
},
{
"command": "gitlens.views.history.setFilesLayoutToList",
"title": "Toggle File Layout (Auto)",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-view-auto.svg",
"light": "images/light/icon-view-auto.svg"
}
},
{
"command": "gitlens.views.history.setFilesLayoutToTree",
"title": "Toggle File Layout (List)",
"category": "GitLens",
"icon": "$(list-flat)"
},
{
"command": "gitlens.views.history.setShowAvatarsOn",
"title": "Show Avatars",
"category": "GitLens"
},
{
"command": "gitlens.views.history.setShowAvatarsOff",
"title": "Hide Avatars",
"category": "GitLens"
},
{
"command": "gitlens.views.lineHistory.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.lineHistory.refresh",
"title": "Refresh",
"category": "GitLens",
@ -3269,6 +3449,11 @@
"category": "GitLens"
},
{
"command": "gitlens.views.compare.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.compare.selectForCompare",
"title": "Compare References...",
"category": "GitLens",
@ -3354,6 +3539,11 @@
}
},
{
"command": "gitlens.views.search.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.search.searchCommits",
"title": "Search Commits",
"category": "GitLens",
@ -3813,26 +4003,6 @@
"when": "false"
},
{
"command": "gitlens.views.repositories.copy",
"when": "false"
},
{
"command": "gitlens.views.fileHistory.copy",
"when": "false"
},
{
"command": "gitlens.views.lineHistory.copy",
"when": "false"
},
{
"command": "gitlens.views.compare.copy",
"when": "false"
},
{
"command": "gitlens.views.search.copy",
"when": "false"
},
{
"command": "gitlens.views.pruneRemote",
"when": "false"
},
@ -4049,6 +4219,10 @@
"when": "false"
},
{
"command": "gitlens.views.repositories.copy",
"when": "false"
},
{
"command": "gitlens.views.repositories.refresh",
"when": "false"
},
@ -4097,6 +4271,46 @@
"when": "false"
},
{
"command": "gitlens.views.branches.copy",
"when": "false"
},
{
"command": "gitlens.views.branches.refresh",
"when": "false"
},
{
"command": "gitlens.views.branches.setBranchesLayoutToList",
"when": "false"
},
{
"command": "gitlens.views.branches.setBranchesLayoutToTree",
"when": "false"
},
{
"command": "gitlens.views.branches.setFilesLayoutToAuto",
"when": "false"
},
{
"command": "gitlens.views.branches.setFilesLayoutToList",
"when": "false"
},
{
"command": "gitlens.views.branches.setFilesLayoutToTree",
"when": "false"
},
{
"command": "gitlens.views.branches.setShowAvatarsOn",
"when": "false"
},
{
"command": "gitlens.views.branches.setShowAvatarsOff",
"when": "false"
},
{
"command": "gitlens.views.fileHistory.copy",
"when": "false"
},
{
"command": "gitlens.views.fileHistory.refresh",
"when": "false"
},
@ -4145,6 +4359,38 @@
"when": "false"
},
{
"command": "gitlens.views.history.copy",
"when": "false"
},
{
"command": "gitlens.views.history.refresh",
"when": "false"
},
{
"command": "gitlens.views.history.setFilesLayoutToAuto",
"when": "false"
},
{
"command": "gitlens.views.history.setFilesLayoutToList",
"when": "false"
},
{
"command": "gitlens.views.history.setFilesLayoutToTree",
"when": "false"
},
{
"command": "gitlens.views.history.setShowAvatarsOn",
"when": "false"
},
{
"command": "gitlens.views.history.setShowAvatarsOff",
"when": "false"
},
{
"command": "gitlens.views.lineHistory.copy",
"when": "false"
},
{
"command": "gitlens.views.lineHistory.refresh",
"when": "false"
},
@ -4177,6 +4423,10 @@
"when": "false"
},
{
"command": "gitlens.views.compare.copy",
"when": "false"
},
{
"command": "gitlens.views.compare.selectForCompare",
"when": "gitlens:enabled && config.gitlens.views.compare.enabled"
},
@ -4225,6 +4475,10 @@
"when": "false"
},
{
"command": "gitlens.views.search.copy",
"when": "false"
},
{
"command": "gitlens.views.search.searchCommits",
"when": "false"
},
@ -4608,17 +4862,17 @@
"view/title": [
{
"command": "gitlens.pushRepositories",
"when": "gitlens:hasRemotes && !gitlens:readonly && view =~ /^gitlens\\.views\\.repositories:/",
"when": "gitlens:hasRemotes && !gitlens:readonly && view =~ /^gitlens\\.views\\.(repositories|history):/",
"group": "navigation@10"
},
{
"command": "gitlens.pullRepositories",
"when": "gitlens:hasRemotes && !gitlens:readonly && view =~ /^gitlens\\.views\\.repositories:/",
"when": "gitlens:hasRemotes && !gitlens:readonly && view =~ /^gitlens\\.views\\.(repositories|history):/",
"group": "navigation@11"
},
{
"command": "gitlens.fetchRepositories",
"when": "gitlens:hasRemotes && !gitlens:readonly && view =~ /^gitlens\\.views\\.repositories:/",
"when": "gitlens:hasRemotes && !gitlens:readonly && view =~ /^gitlens\\.views\\.(repositories|history):/",
"group": "navigation@12"
},
{
@ -4893,8 +5147,78 @@
},
{
"command": "gitlens.supportGitLens",
"when": "view =~ /^gitlens\\.views\\..*:/",
"when": "view =~ /^gitlens\\.views\\..*/",
"group": "9_gitlens@-1"
},
{
"command": "gitlens.views.branches.setBranchesLayoutToList",
"when": "view =~ /gitlens\\.views\\.branches/ && config.gitlens.views.branches.branches.layout == tree",
"group": "navigation@1"
},
{
"command": "gitlens.views.branches.setBranchesLayoutToTree",
"when": "view =~ /gitlens\\.views\\.branches/ && config.gitlens.views.branches.branches.layout == list",
"group": "navigation@1"
},
{
"command": "gitlens.views.branches.setFilesLayoutToList",
"when": "view =~ /^gitlens\\.views\\.branches/ && config.gitlens.views.branches.files.layout == auto",
"group": "navigation@13"
},
{
"command": "gitlens.views.branches.setFilesLayoutToTree",
"when": "view =~ /^gitlens\\.views\\.branches/ && config.gitlens.views.branches.files.layout == list",
"group": "navigation@13"
},
{
"command": "gitlens.views.branches.setFilesLayoutToAuto",
"when": "view =~ /^gitlens\\.views\\.branches/ && config.gitlens.views.branches.files.layout == tree",
"group": "navigation@13"
},
{
"command": "gitlens.views.branches.refresh",
"when": "view =~ /^gitlens\\.views\\.branches/",
"group": "navigation@99"
},
{
"command": "gitlens.views.branches.setShowAvatarsOn",
"when": "view =~ /^gitlens\\.views\\.branches/ && !config.gitlens.views.branches.avatars",
"group": "1_gitlens@0"
},
{
"command": "gitlens.views.branches.setShowAvatarsOff",
"when": "view =~ /^gitlens\\.views\\.branches/ && config.gitlens.views.branches.avatars",
"group": "1_gitlens@0"
},
{
"command": "gitlens.views.history.setFilesLayoutToList",
"when": "view =~ /^gitlens\\.views\\.history/ && config.gitlens.views.history.files.layout == auto",
"group": "navigation@13"
},
{
"command": "gitlens.views.history.setFilesLayoutToTree",
"when": "view =~ /^gitlens\\.views\\.history/ && config.gitlens.views.history.files.layout == list",
"group": "navigation@13"
},
{
"command": "gitlens.views.history.setFilesLayoutToAuto",
"when": "view =~ /^gitlens\\.views\\.history/ && config.gitlens.views.history.files.layout == tree",
"group": "navigation@13"
},
{
"command": "gitlens.views.history.refresh",
"when": "view =~ /^gitlens\\.views\\.history/",
"group": "navigation@99"
},
{
"command": "gitlens.views.history.setShowAvatarsOn",
"when": "view =~ /^gitlens\\.views\\.history/ && !config.gitlens.views.history.avatars",
"group": "1_gitlens@0"
},
{
"command": "gitlens.views.history.setShowAvatarsOff",
"when": "view =~ /^gitlens\\.views\\.history/ && config.gitlens.views.history.avatars",
"group": "1_gitlens@0"
}
],
"view/item/context": [
@ -6129,6 +6453,26 @@
"contextualTitle": "GitLens",
"icon": "images/views/search.svg",
"visibility": "collapsed"
},
{
"id": "gitlens.views.branches",
"name": "Branches"
},
{
"id": "gitlens.views.history",
"name": "History"
},
{
"id": "gitlens.views.tags",
"name": "Tags"
},
{
"id": "gitlens.views.remotes",
"name": "Remotes"
},
{
"id": "gitlens.views.contributors",
"name": "Contributors"
}
],
"explorer": [

+ 2
- 2
src/commands/setViewsLayout.ts View File

@ -1,6 +1,6 @@
'use strict';
import { commands, ConfigurationTarget, window } from 'vscode';
import { configuration, viewKeys, ViewLocation } from '../configuration';
import { configuration, ViewLocation, viewsWithLocationConfigKeys } from '../configuration';
import { command, Command, Commands } from './common';
import { extensionId } from '../constants';
@ -58,7 +58,7 @@ export class SetViewsLayoutCommand extends Command {
return;
}
for (const view of viewKeys) {
for (const view of viewsWithLocationConfigKeys) {
if (configuration.get('views', view, 'location') === location) {
await commands.executeCommand(`${extensionId}.views.${view}:${location}.resetViewLocation`);
} else {

+ 132
- 43
src/config.ts View File

@ -304,25 +304,6 @@ export interface CodeLensLanguageScope {
symbolScopes?: string[];
}
export interface CompareViewConfig {
avatars: boolean;
enabled: boolean;
files: ViewsFilesConfig;
location: ViewLocation;
}
export interface FileHistoryViewConfig {
avatars: boolean;
enabled: boolean;
location: ViewLocation;
}
export interface LineHistoryViewConfig {
avatars: boolean;
enabled: boolean;
location: ViewLocation;
}
export interface MenuConfig {
editor:
| false
@ -416,6 +397,130 @@ export interface RemotesUrlsConfig {
fileRange: string;
}
export interface ViewsCommonConfig {
commitFileDescriptionFormat: string;
commitFileFormat: string;
commitDescriptionFormat: string;
commitFormat: string;
defaultItemLimit: number;
pageItemLimit: number;
showRelativeDateMarkers: boolean;
stashFileDescriptionFormat: string;
stashFileFormat: string;
stashDescriptionFormat: string;
stashFormat: string;
statusFileDescriptionFormat: string;
statusFileFormat: string;
}
export const viewsCommonConfigKeys: (keyof ViewsCommonConfig)[] = [
'commitFileDescriptionFormat',
'commitFileFormat',
'commitDescriptionFormat',
'commitFormat',
'defaultItemLimit',
'pageItemLimit',
'showRelativeDateMarkers',
'stashFileDescriptionFormat',
'stashFileFormat',
'stashDescriptionFormat',
'stashFormat',
'statusFileDescriptionFormat',
'statusFileFormat',
];
interface ViewsConfigs {
branches: BranchesViewConfig;
compare: CompareViewConfig;
contributors: ContributorsViewConfig;
fileHistory: FileHistoryViewConfig;
history: HistoryViewConfig;
lineHistory: LineHistoryViewConfig;
remotes: RemotesViewConfig;
repositories: RepositoriesViewConfig;
search: SearchViewConfig;
stashes: StashesViewConfig;
tags: TagsViewConfig;
}
export type ViewsConfigKeys = keyof ViewsConfigs;
export const viewsConfigKeys: ViewsConfigKeys[] = [
'branches',
'compare',
'contributors',
'fileHistory',
'history',
'lineHistory',
'remotes',
'repositories',
'search',
'stashes',
'tags',
];
export type ViewsConfig = ViewsCommonConfig & ViewsConfigs;
type ViewsWithLocation = keyof Pick<
ViewsConfigs,
'compare' | 'fileHistory' | 'lineHistory' | 'repositories' | 'search'
>;
export const viewsWithLocationConfigKeys: ViewsWithLocation[] = [
'compare',
'fileHistory',
'lineHistory',
'repositories',
'search',
];
export interface BranchesViewConfig {
avatars: boolean;
branches: {
layout: ViewBranchesLayout;
};
files: ViewsFilesConfig;
showTrackingBranch: boolean;
}
export interface CompareViewConfig {
avatars: boolean;
enabled: boolean;
files: ViewsFilesConfig;
location: ViewLocation;
}
export interface ContributorsViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
}
export interface FileHistoryViewConfig {
avatars: boolean;
enabled: boolean;
location: ViewLocation;
}
export interface HistoryViewConfig {
avatars: boolean;
branches: undefined;
files: ViewsFilesConfig;
showTrackingBranch: boolean;
}
export interface LineHistoryViewConfig {
avatars: boolean;
enabled: boolean;
location: ViewLocation;
}
export interface RemotesViewConfig {
avatars: boolean;
branches: {
layout: ViewBranchesLayout;
};
files: ViewsFilesConfig;
}
export interface RepositoriesViewConfig {
autoRefresh: boolean;
autoReveal: boolean;
@ -439,25 +544,14 @@ export interface SearchViewConfig {
location: ViewLocation;
}
export interface ViewsConfig {
fileHistory: FileHistoryViewConfig;
commitFileDescriptionFormat: string;
commitFileFormat: string;
commitDescriptionFormat: string;
commitFormat: string;
compare: CompareViewConfig;
defaultItemLimit: number;
lineHistory: LineHistoryViewConfig;
pageItemLimit: number;
repositories: RepositoriesViewConfig;
search: SearchViewConfig;
showRelativeDateMarkers: boolean;
stashFileDescriptionFormat: string;
stashFileFormat: string;
stashDescriptionFormat: string;
stashFormat: string;
statusFileDescriptionFormat: string;
statusFileFormat: string;
export interface StashesViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
}
export interface TagsViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
}
export interface ViewsFilesConfig {
@ -465,8 +559,3 @@ export interface ViewsFilesConfig {
layout: ViewFilesLayout;
threshold: number;
}
export const viewKeys: (keyof Pick<
Config['views'],
'repositories' | 'fileHistory' | 'lineHistory' | 'compare' | 'search'
>)[] = ['repositories', 'fileHistory', 'lineHistory', 'compare', 'search'];

+ 31
- 2
src/container.ts View File

@ -6,7 +6,13 @@ import { LineAnnotationController } from './annotations/lineAnnotationController
import { clearAvatarCache } from './avatars';
import { GitCodeLensController } from './codelens/codeLensController';
import { Commands, ToggleFileAnnotationCommandArgs } from './commands';
import { AnnotationsToggleMode, Config, configuration, ConfigurationWillChangeEvent, viewKeys } from './configuration';
import {
AnnotationsToggleMode,
Config,
configuration,
ConfigurationWillChangeEvent,
viewsWithLocationConfigKeys,
} from './configuration';
import { extensionId } from './constants';
import { GitFileSystemProvider } from './git/fsProvider';
import { GitService } from './git/gitService';
@ -17,8 +23,10 @@ import { StatusBarController } from './statusbar/statusBarController';
import { GitTerminalLinkProvider } from './terminal/linkProvider';
import { GitDocumentTracker } from './trackers/gitDocumentTracker';
import { GitLineTracker } from './trackers/gitLineTracker';
import { BranchesView } from './views/branchesView';
import { CompareView } from './views/compareView';
import { FileHistoryView } from './views/fileHistoryView';
import { HistoryView } from './views/historyView';
import { LineHistoryView } from './views/lineHistoryView';
import { RepositoriesView } from './views/repositoriesView';
import { SearchView } from './views/searchView';
@ -56,6 +64,9 @@ export class Container {
context.subscriptions.push((this._settingsWebview = new SettingsWebview()));
context.subscriptions.push((this._welcomeWebview = new WelcomeWebview()));
context.subscriptions.push((this._branchesView = new BranchesView()));
context.subscriptions.push((this._historyView = new HistoryView()));
if (config.views.compare.enabled) {
context.subscriptions.push((this._compareView = new CompareView()));
} else {
@ -129,7 +140,7 @@ export class Container {
clearAvatarCache();
}
for (const view of viewKeys) {
for (const view of viewsWithLocationConfigKeys) {
if (configuration.changed(e.change, 'views', view, 'location')) {
setTimeout(
() =>
@ -167,6 +178,15 @@ export class Container {
return this._codeLensController;
}
private static _branchesView: BranchesView | undefined;
static get branchesView() {
if (this._branchesView === undefined) {
this._context.subscriptions.push((this._branchesView = new BranchesView()));
}
return this._branchesView;
}
private static _compareView: CompareView | undefined;
static get compareView() {
if (this._compareView === undefined) {
@ -226,6 +246,15 @@ export class Container {
}
}
private static _historyView: HistoryView | undefined;
static get historyView() {
if (this._historyView === undefined) {
this._context.subscriptions.push((this._historyView = new HistoryView()));
}
return this._historyView;
}
private static _keyboard: Keyboard;
static get keyboard() {
return this._keyboard;

+ 28
- 20
src/git/gitService.ts View File

@ -164,7 +164,7 @@ export class GitService implements Disposable {
}
private onAnyRepositoryChanged(repo: Repository, e: RepositoryChangeEvent) {
if (e.changed(RepositoryChange.Stashes, true)) return;
if (e.changed(RepositoryChange.Stash, true)) return;
this._branchesCache.delete(repo.path);
this._tagsCache.delete(repo.path);
@ -1036,27 +1036,14 @@ export class GitService implements Disposable {
async getBranch(repoPath: string | undefined): Promise<GitBranch | undefined> {
if (repoPath == null) return undefined;
const data = await Git.rev_parse__currentBranch(repoPath);
if (data == null) return undefined;
const committerDate = await Git.log__recent_committerdate(repoPath);
const branch = data[0].split('\n');
return new GitBranch(
repoPath,
branch[0],
false,
true,
committerDate == null ? undefined : new Date(Number(committerDate) * 1000),
data[1],
branch[1],
);
const [branch] = await this.getBranches(repoPath, { filter: b => b.current });
return branch;
}
@log()
async getBranches(
repoPath: string | undefined,
options: { filter?: (b: GitBranch) => boolean; sort?: boolean } = {},
options: { filter?: (b: GitBranch) => boolean; sort?: boolean | { current: boolean } } = {},
): Promise<GitBranch[]> {
if (repoPath == null) return [];
@ -1065,7 +1052,24 @@ export class GitService implements Disposable {
const data = await Git.for_each_ref__branch(repoPath, { all: true });
// If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch
if (data == null || data.length === 0) {
const current = await this.getBranch(repoPath);
let current;
const data = await Git.rev_parse__currentBranch(repoPath);
if (data != null) {
const committerDate = await Git.log__recent_committerdate(repoPath);
const [name, tracking] = data[0].split('\n');
current = new GitBranch(
repoPath,
name,
false,
true,
committerDate == null ? undefined : new Date(Number(committerDate) * 1000),
data[1],
tracking,
);
}
branches = current != null ? [current] : [];
} else {
branches = GitBranchParser.parse(data, repoPath);
@ -1083,8 +1087,9 @@ export class GitService implements Disposable {
branches = branches.filter(options.filter);
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (options.sort) {
GitBranch.sort(branches);
GitBranch.sort(branches, typeof options.sort === 'boolean' ? undefined : options.sort);
}
return branches;
@ -1097,12 +1102,13 @@ export class GitService implements Disposable {
filterBranches,
filterTags,
include,
sort,
...options
}: {
filterBranches?: (b: GitBranch) => boolean;
filterTags?: (t: GitTag) => boolean;
include?: 'all' | 'branches' | 'tags';
sort?: boolean;
sort?: boolean | { current: boolean };
} = {},
) {
const [branches, tags] = await Promise.all<GitBranch[] | undefined, GitTag[] | undefined>([
@ -1110,12 +1116,14 @@ export class GitService implements Disposable {
? this.getBranches(repoPath, {
...options,
filter: filterBranches,
sort: sort,
})
: undefined,
include === 'all' || include === 'tags'
? this.getTags(repoPath, {
...options,
filter: filterTags,
sort: Boolean(sort),
})
: undefined,
]);

+ 7
- 5
src/git/models/branch.ts View File

@ -33,14 +33,16 @@ export class GitBranch implements GitBranchReference {
return branch?.refType === 'branch';
}
static sort(branches: GitBranch[]) {
static sort(branches: GitBranch[], options?: { current: boolean }) {
const order = configuration.get('sortBranchesBy');
const opts = { current: true, ...options };
switch (order) {
case BranchSorting.DateAsc:
return branches.sort(
(a, b) =>
(a.current ? -1 : 1) - (b.current ? -1 : 1) ||
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
(b.remote ? -1 : 1) - (a.remote ? -1 : 1) ||
(a.date === undefined ? -1 : a.date.getTime()) - (b.date === undefined ? -1 : b.date.getTime()),
@ -48,7 +50,7 @@ export class GitBranch implements GitBranchReference {
case BranchSorting.DateDesc:
return branches.sort(
(a, b) =>
(a.current ? -1 : 1) - (b.current ? -1 : 1) ||
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
(b.remote ? -1 : 1) - (a.remote ? -1 : 1) ||
(b.date === undefined ? -1 : b.date.getTime()) - (a.date === undefined ? -1 : a.date.getTime()),
@ -56,7 +58,7 @@ export class GitBranch implements GitBranchReference {
case BranchSorting.NameAsc:
return branches.sort(
(a, b) =>
(a.current ? -1 : 1) - (b.current ? -1 : 1) ||
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
(a.name === 'main' ? -1 : 1) - (b.name === 'main' ? -1 : 1) ||
(a.name === 'master' ? -1 : 1) - (b.name === 'master' ? -1 : 1) ||
@ -67,7 +69,7 @@ export class GitBranch implements GitBranchReference {
default:
return branches.sort(
(a, b) =>
(a.current ? -1 : 1) - (b.current ? -1 : 1) ||
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
(a.name === 'main' ? -1 : 1) - (b.name === 'main' ? -1 : 1) ||
(a.name === 'master' ? -1 : 1) - (b.name === 'master' ? -1 : 1) ||

+ 2
- 1
src/git/models/models.ts View File

@ -182,7 +182,8 @@ export namespace GitReference {
};
default:
return {
name: options.name ?? GitRevision.shorten(ref, { force: true }),
name:
options.name ?? GitRevision.shorten(ref, { force: true, strings: { working: 'Working Tree' } }),
ref: ref,
refType: 'revision',
repoPath: repoPath,

+ 62
- 26
src/git/models/repository.ts View File

@ -27,22 +27,26 @@ import { runGitCommandInTerminal } from '../../terminal';
import { GitBranchReference, GitReference, GitTagReference } from './models';
const ignoreGitRegex = /\.git(?:\/|\\|$)/;
const refsRegex = /\.git\/refs\/(heads|remotes|tags)/;
export enum RepositoryChange {
Config = 'config',
Closed = 'closed',
// FileSystem = 'file-system',
Heads = 'heads',
Index = 'index',
Ignores = 'ignores',
Remotes = 'remotes',
Repository = 'repository',
Stashes = 'stashes',
Stash = 'stash',
Tags = 'tags',
Unknown = 'unknown',
}
export class RepositoryChangeEvent {
constructor(public readonly repository?: Repository, public readonly changes: RepositoryChange[] = []) {}
changed(change: RepositoryChange, solely: boolean = false) {
if (solely) return this.changes.length === 1 && this.changes[0] === change;
changed(change: RepositoryChange, only: boolean = false) {
if (only) return this.changes.length === 1 && this.changes[0] === change;
return this.changes.includes(change);
@ -188,35 +192,65 @@ export class Repository implements Disposable {
}
private onRepositoryChanged(uri: Uri | undefined) {
if (uri?.path.endsWith('refs/stash')) {
this.fireChange(RepositoryChange.Stashes);
if (uri == null) {
this.fireChange(RepositoryChange.Unknown);
return;
}
this._branch = undefined;
if (uri?.path.endsWith('refs/remotes')) {
if (uri.path.endsWith('.git/config')) {
this.resetRemotesCache();
this.fireChange(RepositoryChange.Remotes);
this.fireChange(RepositoryChange.Config, RepositoryChange.Remotes);
return;
}
if (uri?.path.endsWith('refs/tags')) {
this.fireChange(RepositoryChange.Tags);
if (uri.path.endsWith('.git/index')) {
this.fireChange(RepositoryChange.Index);
return;
}
if (uri?.path.endsWith('config')) {
this.resetRemotesCache();
this.fireChange(RepositoryChange.Config, RepositoryChange.Remotes);
if (uri.path.endsWith('.git/HEAD') || uri.path.endsWith('.git/ORIG_HEAD')) {
this._branch = undefined;
this.fireChange(RepositoryChange.Heads);
return;
}
if (uri.path.endsWith('.git/refs/stash')) {
this.fireChange(RepositoryChange.Stash);
return;
}
if (uri.path.endsWith('/.gitignore')) {
this.fireChange(RepositoryChange.Ignores);
return;
}
this.fireChange(RepositoryChange.Repository);
const match = refsRegex.exec(uri.path);
if (match != null) {
switch (match[1]) {
case 'heads':
this._branch = undefined;
this.fireChange(RepositoryChange.Heads);
return;
case 'remotes':
this.resetRemotesCache();
this.fireChange(RepositoryChange.Remotes);
return;
case 'tags':
this.fireChange(RepositoryChange.Tags);
return;
}
}
this.fireChange(RepositoryChange.Unknown);
}
private _closed: boolean = false;
@ -297,7 +331,7 @@ export class Repository implements Disposable {
try {
void (await Container.git.fetch(this.path, options));
this.fireChange(RepositoryChange.Repository);
this.fireChange(RepositoryChange.Unknown);
} catch (ex) {
Logger.error(ex);
void Messages.showGenericErrorMessage('Unable to fetch repository');
@ -311,7 +345,9 @@ export class Repository implements Disposable {
return this._branch;
}
getBranches(options: { filter?: (b: GitBranch) => boolean; sort?: boolean } = {}): Promise<GitBranch[]> {
getBranches(
options: { filter?: (b: GitBranch) => boolean; sort?: boolean | { current: boolean } } = {},
): Promise<GitBranch[]> {
return Container.git.getBranches(this.path, options);
}
@ -320,7 +356,7 @@ export class Repository implements Disposable {
filterBranches?: (b: GitBranch) => boolean;
filterTags?: (t: GitTag) => boolean;
include?: 'all' | 'branches' | 'tags';
sort?: boolean;
sort?: boolean | { current: boolean };
} = {},
) {
return Container.git.getBranchesAndOrTags(this.path, options);
@ -436,7 +472,7 @@ export class Repository implements Disposable {
void (await Container.git.fetch(this.path));
}
this.fireChange(RepositoryChange.Repository);
this.fireChange(RepositoryChange.Unknown);
} catch (ex) {
Logger.error(ex);
void Messages.showGenericErrorMessage('Unable to pull repository');
@ -494,7 +530,7 @@ export class Repository implements Disposable {
void (await commands.executeCommand(options.force ? 'git.pushForce' : 'git.push', this.path));
}
this.fireChange(RepositoryChange.Repository);
this.fireChange(RepositoryChange.Unknown);
} catch (ex) {
Logger.error(ex);
void Messages.showGenericErrorMessage('Unable to push repository');
@ -549,7 +585,7 @@ export class Repository implements Disposable {
async stashApply(stashName: string, options: { deleteAfter?: boolean } = {}) {
void (await Container.git.stashApply(this.path, stashName, options));
if (!this.supportsChangeEvents) {
this.fireChange(RepositoryChange.Stashes);
this.fireChange(RepositoryChange.Stash);
}
}
@ -558,7 +594,7 @@ export class Repository implements Disposable {
async stashDelete(stashName: string) {
void (await Container.git.stashDelete(this.path, stashName));
if (!this.supportsChangeEvents) {
this.fireChange(RepositoryChange.Stashes);
this.fireChange(RepositoryChange.Stash);
}
}
@ -567,7 +603,7 @@ export class Repository implements Disposable {
async stashSave(message?: string, uris?: Uri[], options: { includeUntracked?: boolean; keepIndex?: boolean } = {}) {
void (await Container.git.stashSave(this.path, message, uris, options));
if (!this.supportsChangeEvents) {
this.fireChange(RepositoryChange.Stashes);
this.fireChange(RepositoryChange.Stash);
}
}
@ -591,7 +627,7 @@ export class Repository implements Disposable {
try {
void (await Container.git.checkout(this.path, ref, options));
this.fireChange(RepositoryChange.Repository);
this.fireChange(RepositoryChange.Unknown);
} catch (ex) {
Logger.error(ex);
void Messages.showGenericErrorMessage('Unable to switch to reference');
@ -730,7 +766,7 @@ export class Repository implements Disposable {
const parsedArgs = args.map(arg => (arg.startsWith('#') ? `"${arg}"` : arg));
runGitCommandInTerminal(command, parsedArgs.join(' '), this.path, true);
if (!this.supportsChangeEvents) {
this.fireChange(RepositoryChange.Repository);
this.fireChange(RepositoryChange.Unknown);
}
}
}

+ 7
- 1
src/trackers/trackedDocument.ts View File

@ -69,7 +69,13 @@ export class TrackedDocument implements Disposable {
}
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (!e.changed(RepositoryChange.Repository)) return;
if (
!e.changed(RepositoryChange.Index) &&
!e.changed(RepositoryChange.Heads) &&
!e.changed(RepositoryChange.Unknown)
) {
return;
}
// Reset any cached state
this.reset('repository');

+ 403
- 0
src/views/branchesView.ts View File

@ -0,0 +1,403 @@
'use strict';
import {
CancellationToken,
commands,
ConfigurationChangeEvent,
ProgressLocation,
TreeItem,
TreeItemCollapsibleState,
window,
} from 'vscode';
import { BranchesViewConfig, configuration, ViewBranchesLayout, ViewFilesLayout } from '../configuration';
import { Container } from '../container';
import {
GitBranch,
GitBranchReference,
GitLogCommit,
GitReference,
GitRevisionReference,
Repository,
RepositoryChange,
RepositoryChangeEvent,
} from '../git/git';
import { GitUri } from '../git/gitUri';
import {
BranchesNode,
BranchNode,
BranchOrTagFolderNode,
ContextValues,
MessageNode,
RemoteNode,
RemotesNode,
RepositoriesNode,
RepositoryNode,
SubscribeableViewNode,
unknownGitUri,
ViewNode,
} from './nodes';
import { debug, gate } from '../system';
import { ViewBase } from './viewBase';
export class BranchesRepositoryNode extends SubscribeableViewNode<BranchesView> {
private child: ViewNode | undefined;
constructor(
uri: GitUri,
view: BranchesView,
parent: ViewNode,
public readonly repo: Repository,
private readonly root: boolean,
) {
super(uri, view, parent);
}
async getChildren(): Promise<ViewNode[]> {
if (this.child == null) {
this.child = new BranchesNode(this.uri, this.view, this, this.repo);
void this.ensureSubscription();
}
return this.child.getChildren();
}
getTreeItem(): TreeItem {
const item = new TreeItem(
this.repo.formattedName ?? this.uri.repoPath ?? '',
TreeItemCollapsibleState.Expanded,
);
item.contextValue = ContextValues.RepositoryFolder;
void this.ensureSubscription();
return item;
}
@gate()
@debug()
async refresh(reset: boolean = false) {
await this.child?.triggerChange(reset);
await this.ensureSubscription();
}
@debug()
protected subscribe() {
return this.repo.onDidChange(this.onRepositoryChanged, this);
}
@debug({
args: {
0: (e: RepositoryChangeEvent) =>
`{ repository: ${e.repository ? e.repository.name : ''}, changes: ${e.changes.join()} }`,
},
})
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (e.changed(RepositoryChange.Closed)) {
this.dispose();
void this.parent?.triggerChange(true);
return;
}
if (e.changed(RepositoryChange.Heads)) {
void this.triggerChange(true);
if (this.root) {
void this.parent?.triggerChange(true);
}
}
}
}
export class BranchesViewNode extends ViewNode<BranchesView> {
private children: BranchesRepositoryNode[] | undefined;
constructor(view: BranchesView) {
super(unknownGitUri, view);
}
async getChildren(): Promise<ViewNode[]> {
if (this.children != null) {
for (const child of this.children) {
child.dispose?.();
}
this.children = undefined;
}
const repositories = await Container.git.getOrderedRepositories();
if (repositories.length === 0) return [new MessageNode(this.view, this, 'No branches could be found.')];
const root = repositories.length === 1;
this.children = repositories.map(
r => new BranchesRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, root),
);
if (root) {
const [child] = this.children;
const branches = await child.repo.getBranches({ filter: b => !b.remote });
this.view.description = branches.length === 0 ? undefined : `(${branches.length})`;
return child.getChildren();
}
return this.children;
}
getTreeItem(): TreeItem {
const item = new TreeItem('Branches', TreeItemCollapsibleState.Expanded);
return item;
}
}
export class BranchesView extends ViewBase<BranchesViewNode, BranchesViewConfig> {
protected readonly configKey = 'branches';
constructor() {
super('gitlens.views.branches', 'Branches');
}
getRoot() {
return new BranchesViewNode(this);
}
protected registerCommands() {
void Container.viewCommands;
commands.registerCommand(
this.getQualifiedCommand('copy'),
() => commands.executeCommand('gitlens.views.copy', this.selection),
this,
);
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(true), this);
commands.registerCommand(
this.getQualifiedCommand('setBranchesLayoutToList'),
() => this.setBranchesLayout(ViewBranchesLayout.List),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setBranchesLayoutToTree'),
() => this.setBranchesLayout(ViewBranchesLayout.Tree),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToAuto'),
() => this.setFilesLayout(ViewFilesLayout.Auto),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToList'),
() => this.setFilesLayout(ViewFilesLayout.List),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToTree'),
() => this.setFilesLayout(ViewFilesLayout.Tree),
this,
);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOn'), () => this.setShowAvatars(true), this);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
}
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
!configuration.changed(e, 'defaultGravatarsStyle') &&
!configuration.changed(e, 'sortBranchesBy')
) {
return false;
}
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.initializing(e)) {
this.initialize(undefined, { showCollapseAll: true });
}
if (!configuration.initializing(e) && this._root != null) {
void this.refresh(true);
}
}
findBranch(branch: GitBranchReference, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(branch.repoPath);
if (branch.remote) {
return this.findNode((n: any) => n.branch !== undefined && n.branch.ref === branch.ref, {
allowPaging: true,
maxDepth: 6,
canTraverse: n => {
// Only search for branch nodes in the same repo within BranchesNode
if (n instanceof RepositoriesNode) return true;
if (n instanceof RemoteNode) {
if (!n.id.startsWith(repoNodeId)) return false;
return branch.remote && n.remote.name === GitBranch.getRemote(branch.name); //branch.getRemoteName();
}
if (
n instanceof RepositoryNode ||
n instanceof BranchesNode ||
n instanceof RemotesNode ||
n instanceof BranchOrTagFolderNode
) {
return n.id.startsWith(repoNodeId);
}
return false;
},
token: token,
});
}
return this.findNode((n: any) => n.branch !== undefined && n.branch.ref === branch.ref, {
allowPaging: true,
maxDepth: 5,
canTraverse: n => {
// Only search for branch nodes in the same repo within BranchesNode
if (n instanceof RepositoriesNode) return true;
if (n instanceof RepositoryNode || n instanceof BranchesNode || n instanceof BranchOrTagFolderNode) {
return n.id.startsWith(repoNodeId);
}
return false;
},
token: token,
});
}
async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
// Get all the branches the commit is on
let branches = await Container.git.getCommitBranches(commit.repoPath, commit.ref);
if (branches.length !== 0) {
return this.findNode((n: any) => n.commit !== undefined && n.commit.ref === commit.ref, {
allowPaging: true,
maxDepth: 6,
canTraverse: async n => {
// Only search for commit nodes in the same repo within BranchNodes
if (n instanceof RepositoriesNode) return true;
if (n instanceof BranchNode) {
if (n.id.startsWith(repoNodeId) && branches.includes(n.branch.name)) {
await n.showMore({ until: commit.ref });
return true;
}
}
if (
n instanceof RepositoryNode ||
n instanceof BranchesNode ||
n instanceof BranchOrTagFolderNode
) {
return n.id.startsWith(repoNodeId);
}
return false;
},
token: token,
});
}
// If we didn't find the commit on any local branches, check remote branches
branches = await Container.git.getCommitBranches(commit.repoPath, commit.ref, { remotes: true });
if (branches.length === 0) return undefined;
const remotes = branches.map(b => b.split('/', 1)[0]);
return this.findNode((n: any) => n.commit !== undefined && n.commit.ref === commit.ref, {
allowPaging: true,
maxDepth: 8,
canTraverse: n => {
// Only search for commit nodes in the same repo within BranchNodes
if (n instanceof RepositoriesNode) return true;
if (n instanceof RemoteNode) {
return n.id.startsWith(repoNodeId) && remotes.includes(n.remote.name);
}
if (n instanceof BranchNode) {
return n.id.startsWith(repoNodeId) && branches.includes(n.branch.name);
}
if (n instanceof RepositoryNode || n instanceof RemotesNode || n instanceof BranchOrTagFolderNode) {
return n.id.startsWith(repoNodeId);
}
return false;
},
token: token,
});
}
@gate(() => '')
revealBranch(
branch: GitBranchReference,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `Revealing ${GitReference.toString(branch, { icon: false })} in the Repositories view...`,
cancellable: true,
},
async (progress, token) => {
const node = await this.findBranch(branch, token);
if (node === undefined) return node;
await this.ensureRevealNode(node, options);
return node;
},
);
}
@gate(() => '')
async revealCommit(
commit: GitRevisionReference,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `Revealing ${GitReference.toString(commit, { icon: false })} in the Repositories view...`,
cancellable: true,
},
async (progress, token) => {
const node = await this.findCommit(commit, token);
if (node === undefined) return node;
await this.ensureRevealNode(node, options);
return node;
},
);
}
private setBranchesLayout(layout: ViewBranchesLayout) {
return configuration.updateEffective('views', this.configKey, 'branches', 'layout', layout);
}
private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective('views', this.configKey, 'files', 'layout', layout);
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 18
- 26
src/views/compareView.ts View File

@ -1,6 +1,6 @@
'use strict';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { CompareViewConfig, configuration, ViewFilesLayout, ViewsConfig } from '../configuration';
import { CompareViewConfig, configuration, ViewFilesLayout } from '../configuration';
import {
CommandContext,
NamedRef,
@ -13,7 +13,9 @@ import { Container } from '../container';
import { CompareNode, CompareResultsNode, nodeSupportsConditionalDismissal, ViewNode } from './nodes';
import { ViewBase } from './viewBase';
export class CompareView extends ViewBase<CompareNode> {
export class CompareView extends ViewBase<CompareNode, CompareViewConfig> {
protected readonly configKey = 'compare';
constructor() {
super('gitlens.views.compare', 'Compare');
@ -68,51 +70,41 @@ export class CompareView extends ViewBase {
commands.registerCommand(this.getQualifiedCommand('compareWithSelected'), this.compareWithSelected, this);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!configuration.changed(e, 'views', 'compare') &&
!configuration.changed(e, 'views', 'commitFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFileFormat') &&
!configuration.changed(e, 'views', 'commitDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFormat') &&
!configuration.changed(e, 'views', 'defaultItemLimit') &&
!configuration.changed(e, 'views', 'pageItemLimit') &&
!configuration.changed(e, 'views', 'showRelativeDateMarkers') &&
!configuration.changed(e, 'views', 'statusFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'statusFileFormat') &&
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
!configuration.changed(e, 'defaultGravatarsStyle')
) {
return;
return false;
}
if (configuration.changed(e, 'views', 'compare', 'location')) {
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'views', this.configKey, 'location')) {
this.initialize(this.config.location, { showCollapseAll: true });
}
if (!configuration.initializing(e) && this._root !== undefined) {
if (!configuration.initializing(e) && this._root != null) {
void this.refresh(true);
}
}
get config(): ViewsConfig & CompareViewConfig {
return { ...Container.config.views, ...Container.config.views.compare };
}
get keepResults(): boolean {
return Container.context.workspaceState.get<boolean>(WorkspaceState.ViewsCompareKeepResults, false);
}
clear() {
if (this._root === undefined) return;
this._root.clear();
this._root?.clear();
}
dismissNode(node: ViewNode) {
if (this._root === undefined) return;
if (this._root == null) return;
if (nodeSupportsConditionalDismissal(node) && node.canDismiss() === false) return;
this._root.dismiss(node);
@ -174,7 +166,7 @@ export class CompareView extends ViewBase {
}
private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective('views', 'compare', 'files', 'layout', layout);
return configuration.updateEffective('views', this.configKey, 'files', 'layout', layout);
}
private setKeepResults(enabled: boolean) {
@ -183,7 +175,7 @@ export class CompareView extends ViewBase {
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', 'compare', 'avatars', enabled);
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
private pinComparison(node: ViewNode) {

+ 15
- 21
src/views/fileHistoryView.ts View File

@ -1,13 +1,15 @@
'use strict';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, FileHistoryViewConfig, ViewsConfig } from '../configuration';
import { configuration, FileHistoryViewConfig } from '../configuration';
import { CommandContext, setCommandContext } from '../constants';
import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { FileHistoryTrackerNode, LineHistoryTrackerNode } from './nodes';
import { ViewBase } from './viewBase';
export class FileHistoryView extends ViewBase<FileHistoryTrackerNode | LineHistoryTrackerNode> {
export class FileHistoryView extends ViewBase<FileHistoryTrackerNode | LineHistoryTrackerNode, FileHistoryViewConfig> {
protected readonly configKey = 'fileHistory';
constructor() {
super('gitlens.views.fileHistory', 'File History');
}
@ -74,18 +76,10 @@ export class FileHistoryView extends ViewBase
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!configuration.changed(e, 'views', 'fileHistory') &&
!configuration.changed(e, 'views', 'commitFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFileFormat') &&
!configuration.changed(e, 'views', 'commitDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFormat') &&
!configuration.changed(e, 'views', 'defaultItemLimit') &&
!configuration.changed(e, 'views', 'pageItemLimit') &&
!configuration.changed(e, 'views', 'showRelativeDateMarkers') &&
!configuration.changed(e, 'views', 'statusFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'statusFileFormat') &&
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
@ -93,15 +87,19 @@ export class FileHistoryView extends ViewBase
!configuration.changed(e, 'advanced', 'fileHistoryFollowsRenames') &&
!configuration.changed(e, 'advanced', 'fileHistoryShowAllBranches')
) {
return;
return false;
}
if (configuration.changed(e, 'views', 'fileHistory', 'enabled')) {
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'views', this.configKey, 'enabled')) {
void setCommandContext(CommandContext.ViewsFileHistoryEditorFollowing, this._followEditor);
void setCommandContext(CommandContext.ViewsFileHistoryCursorFollowing, this._followCursor);
}
if (configuration.changed(e, 'views', 'fileHistory', 'location')) {
if (configuration.changed(e, 'views', this.configKey, 'location')) {
this.initialize(this.config.location);
}
@ -110,10 +108,6 @@ export class FileHistoryView extends ViewBase
}
}
get config(): ViewsConfig & FileHistoryViewConfig {
return { ...Container.config.views, ...Container.config.views.fileHistory };
}
async showHistoryForUri(uri: GitUri, baseRef?: string) {
this.setCursorFollowing(false);
this.setEditorFollowing(false);
@ -160,6 +154,6 @@ export class FileHistoryView extends ViewBase
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', 'fileHistory', 'avatars', enabled);
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 326
- 0
src/views/historyView.ts View File

@ -0,0 +1,326 @@
'use strict';
import {
CancellationToken,
commands,
ConfigurationChangeEvent,
ProgressLocation,
TreeItem,
TreeItemCollapsibleState,
window,
} from 'vscode';
import { configuration, HistoryViewConfig, ViewFilesLayout } from '../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import {
GitLogCommit,
GitReference,
GitRevisionReference,
Repository,
RepositoryChange,
RepositoryChangeEvent,
} from '../git/git';
import { GitUri } from '../git/gitUri';
import {
BranchesNode,
BranchNode,
BranchOrTagFolderNode,
ContextValues,
MessageNode,
RemoteNode,
RemotesNode,
RepositoriesNode,
RepositoryNode,
SubscribeableViewNode,
unknownGitUri,
ViewNode,
} from './nodes';
import { debug, gate } from '../system';
import { ViewBase } from './viewBase';
export class HistoryRepositoryNode extends SubscribeableViewNode<HistoryView> {
private children: BranchNode[] | undefined;
constructor(
uri: GitUri,
view: HistoryView,
parent: ViewNode,
public readonly repo: Repository,
private readonly root: boolean,
) {
super(uri, view, parent);
}
async getChildren(): Promise<ViewNode[]> {
if (this.children == null) {
const branch = await this.repo.getBranch();
if (branch == null) return [new MessageNode(this.view, this, 'No commits could be found.')];
this.children = [
new BranchNode(this.uri, this.view, this, branch, true, {
expanded: true,
showCurrent: false,
showTracking: true,
}),
];
void this.ensureSubscription();
}
return this.children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(
this.repo.formattedName ?? this.uri.repoPath ?? '',
TreeItemCollapsibleState.Expanded,
);
item.contextValue = ContextValues.RepositoryFolder;
void this.ensureSubscription();
return item;
}
@gate()
@debug()
async refresh(reset: boolean = false) {
if (reset) {
this.children = undefined;
} else {
void this.parent?.triggerChange(false);
}
await this.ensureSubscription();
}
@debug()
protected subscribe() {
return this.repo.onDidChange(this.onRepositoryChanged, this);
}
@debug({
args: {
0: (e: RepositoryChangeEvent) =>
`{ repository: ${e.repository ? e.repository.name : ''}, changes: ${e.changes.join()} }`,
},
})
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (e.changed(RepositoryChange.Closed)) {
this.dispose();
void this.parent?.triggerChange(true);
return;
}
if (e.changed(RepositoryChange.Index) || e.changed(RepositoryChange.Heads)) {
void this.triggerChange(true);
if (this.root) {
void this.parent?.triggerChange(true);
}
}
}
}
export class HistoryViewNode extends ViewNode<HistoryView> {
private children: HistoryRepositoryNode[] | undefined;
constructor(view: HistoryView) {
super(unknownGitUri, view);
}
async getChildren(): Promise<ViewNode[]> {
if (this.children != null) {
for (const child of this.children) {
child.dispose();
}
this.children = undefined;
}
const repositories = await Container.git.getOrderedRepositories();
if (repositories.length === 0) return [new MessageNode(this.view, this, 'No commits could be found.')];
const root = repositories.length === 1;
this.children = repositories.map(
r => new HistoryRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, root),
);
if (root) {
const [child] = this.children;
const branch = await child.repo.getBranch();
const status = branch?.getTrackingStatus();
this.view.titleContext =
branch != null ? `${branch.name}${status ? ` ${GlyphChars.Dot} ${status}` : ''}` : undefined;
return child.getChildren();
}
return this.children;
}
getTreeItem(): TreeItem {
const item = new TreeItem('History', TreeItemCollapsibleState.Expanded);
return item;
}
}
export class HistoryView extends ViewBase<HistoryViewNode, HistoryViewConfig> {
protected readonly configKey = 'history';
constructor() {
super('gitlens.views.history', 'History');
}
getRoot() {
return new HistoryViewNode(this);
}
protected registerCommands() {
void Container.viewCommands;
commands.registerCommand(
this.getQualifiedCommand('copy'),
() => commands.executeCommand('gitlens.views.copy', this.selection),
this,
);
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(true), this);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToAuto'),
() => this.setFilesLayout(ViewFilesLayout.Auto),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToList'),
() => this.setFilesLayout(ViewFilesLayout.List),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToTree'),
() => this.setFilesLayout(ViewFilesLayout.Tree),
this,
);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOn'), () => this.setShowAvatars(true), this);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
}
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
!configuration.changed(e, 'defaultGravatarsStyle')
) {
return false;
}
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.initializing(e)) {
this.initialize(undefined, { showCollapseAll: true });
}
if (!configuration.initializing(e) && this._root !== undefined) {
void this.refresh(true);
}
}
async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
// Get all the branches the commit is on
let branches = await Container.git.getCommitBranches(commit.repoPath, commit.ref);
if (branches.length !== 0) {
return this.findNode((n: any) => n.commit !== undefined && n.commit.ref === commit.ref, {
allowPaging: true,
maxDepth: 6,
canTraverse: async n => {
// Only search for commit nodes in the same repo within BranchNodes
if (n instanceof RepositoriesNode) return true;
if (n instanceof BranchNode) {
if (n.id.startsWith(repoNodeId) && branches.includes(n.branch.name)) {
await n.showMore({ until: commit.ref });
return true;
}
}
if (
n instanceof RepositoryNode ||
n instanceof BranchesNode ||
n instanceof BranchOrTagFolderNode
) {
return n.id.startsWith(repoNodeId);
}
return false;
},
token: token,
});
}
// If we didn't find the commit on any local branches, check remote branches
branches = await Container.git.getCommitBranches(commit.repoPath, commit.ref, { remotes: true });
if (branches.length === 0) return undefined;
const remotes = branches.map(b => b.split('/', 1)[0]);
return this.findNode((n: any) => n.commit !== undefined && n.commit.ref === commit.ref, {
allowPaging: true,
maxDepth: 8,
canTraverse: n => {
// Only search for commit nodes in the same repo within BranchNodes
if (n instanceof RepositoriesNode) return true;
if (n instanceof RemoteNode) {
return n.id.startsWith(repoNodeId) && remotes.includes(n.remote.name);
}
if (n instanceof BranchNode) {
return n.id.startsWith(repoNodeId) && branches.includes(n.branch.name);
}
if (n instanceof RepositoryNode || n instanceof RemotesNode || n instanceof BranchOrTagFolderNode) {
return n.id.startsWith(repoNodeId);
}
return false;
},
token: token,
});
}
@gate(() => '')
async revealCommit(
commit: GitRevisionReference,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `Revealing ${GitReference.toString(commit, { icon: false })} in the Repositories view...`,
cancellable: true,
},
async (progress, token) => {
const node = await this.findCommit(commit, token);
if (node === undefined) return node;
await this.ensureRevealNode(node, options);
return node;
},
);
}
private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective('views', this.configKey, 'files', 'layout', layout);
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 15
- 21
src/views/lineHistoryView.ts View File

@ -1,12 +1,14 @@
'use strict';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, LineHistoryViewConfig, ViewsConfig } from '../configuration';
import { configuration, LineHistoryViewConfig } from '../configuration';
import { CommandContext, setCommandContext } from '../constants';
import { Container } from '../container';
import { LineHistoryTrackerNode } from './nodes';
import { ViewBase } from './viewBase';
export class LineHistoryView extends ViewBase<LineHistoryTrackerNode> {
export class LineHistoryView extends ViewBase<LineHistoryTrackerNode, LineHistoryViewConfig> {
protected readonly configKey = 'lineHistory';
constructor() {
super('gitlens.views.lineHistory', 'Line History');
}
@ -53,32 +55,28 @@ export class LineHistoryView extends ViewBase {
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!configuration.changed(e, 'views', 'lineHistory') &&
!configuration.changed(e, 'views', 'commitFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFileFormat') &&
!configuration.changed(e, 'views', 'commitDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFormat') &&
!configuration.changed(e, 'views', 'defaultItemLimit') &&
!configuration.changed(e, 'views', 'pageItemLimit') &&
!configuration.changed(e, 'views', 'showRelativeDateMarkers') &&
!configuration.changed(e, 'views', 'statusFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'statusFileFormat') &&
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
!configuration.changed(e, 'defaultGravatarsStyle') &&
!configuration.changed(e, 'advanced', 'fileHistoryFollowsRenames')
) {
return;
return false;
}
if (configuration.changed(e, 'views', 'lineHistory', 'enabled')) {
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'views', this.configKey, 'enabled')) {
void setCommandContext(CommandContext.ViewsLineHistoryEditorFollowing, true);
}
if (configuration.changed(e, 'views', 'lineHistory', 'location')) {
if (configuration.changed(e, 'views', this.configKey, 'location')) {
this.initialize(this.config.location);
}
@ -87,10 +85,6 @@ export class LineHistoryView extends ViewBase {
}
}
get config(): ViewsConfig & LineHistoryViewConfig {
return { ...Container.config.views, ...Container.config.views.lineHistory };
}
private changeBase() {
void this._root?.changeBase();
}
@ -106,6 +100,6 @@ export class LineHistoryView extends ViewBase {
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', 'lineHistory', 'avatars', enabled);
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 49
- 19
src/views/nodes/branchNode.ts View File

@ -1,36 +1,59 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchesView } from '../branchesView';
import { BranchTrackingStatusNode } from './branchTrackingStatusNode';
import { CommitNode } from './commitNode';
import { HistoryView } from '../historyView';
import { MessageNode, ShowMoreNode } from './common';
import { ViewBranchesLayout } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { BranchDateFormatting, GitBranch, GitBranchReference, GitLog, GitRemoteType } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { debug, gate, Iterables, log, Strings } from '../../system';
import { RepositoriesView } from '../repositoriesView';
import { BranchTrackingStatusNode } from './branchTrackingStatusNode';
import { CommitNode } from './commitNode';
import { MessageNode, ShowMoreNode } from './common';
import { insertDateMarkers } from './helpers';
import { ContextValues, PageableViewNode, ViewNode, ViewRefNode } from './viewNode';
import { RepositoriesView } from '../repositoriesView';
import { RepositoryNode } from './repositoryNode';
import { debug, gate, Iterables, log, Strings } from '../../system';
import { ContextValues, PageableViewNode, ViewNode, ViewRefNode } from './viewNode';
export class BranchNode extends ViewRefNode<RepositoriesView, GitBranchReference> implements PageableViewNode {
export class BranchNode extends ViewRefNode<RepositoriesView | BranchesView | HistoryView, GitBranchReference>
implements PageableViewNode {
static key = ':branch';
static getId(repoPath: string, name: string, root: boolean): string {
return `${RepositoryNode.getId(repoPath)}${this.key}(${name})${root ? ':root' : ''}`;
}
private _children: ViewNode[] | undefined;
private readonly options: {
expanded: boolean;
showCurrent: boolean;
showTracking: boolean;
};
constructor(
uri: GitUri,
view: RepositoriesView,
view: RepositoriesView | BranchesView | HistoryView,
parent: ViewNode,
public readonly branch: GitBranch,
// Specifies that the node is shown as a root under the repository node
public readonly root: boolean = false,
private readonly root: boolean,
options?: {
expanded?: boolean;
showCurrent?: boolean;
showTracking?: boolean;
},
) {
super(uri, view, parent);
this.options = {
expanded: false,
// Hide the current branch checkmark when the node is displayed as a root under the repository node
showCurrent: !this.root,
// Don't show tracking info the node is displayed as a root under the repository node
showTracking: !this.root,
...options,
};
}
toClipboard(): string {
@ -49,9 +72,12 @@ export class BranchNode extends ViewRefNode
get label(): string {
const branchName = this.branch.getNameWithoutRemote();
if (this.view.config.branches.layout === ViewBranchesLayout.List) return branchName;
return this.compacted || this.root || this.current || this.branch.detached || this.branch.starred
return this.view.config.branches?.layout !== ViewBranchesLayout.Tree ||
this.compacted ||
this.root ||
this.current ||
this.branch.detached ||
this.branch.starred
? branchName
: this.branch.getBasename();
}
@ -69,7 +95,7 @@ export class BranchNode extends ViewRefNode
async getChildren(): Promise<ViewNode[]> {
if (this._children === undefined) {
const children = [];
if (!this.root && this.branch.tracking) {
if (this.options.showTracking && this.branch.tracking) {
const status = {
ref: this.branch.ref,
repoPath: this.branch.repoPath,
@ -78,11 +104,15 @@ export class BranchNode extends ViewRefNode
};
if (this.branch.state.behind) {
children.push(new BranchTrackingStatusNode(this.view, this, this.branch, status, 'behind'));
children.push(
new BranchTrackingStatusNode(this.view, this, this.branch, status, 'behind', this.root),
);
}
if (this.branch.state.ahead) {
children.push(new BranchTrackingStatusNode(this.view, this, this.branch, status, 'ahead'));
children.push(
new BranchTrackingStatusNode(this.view, this, this.branch, status, 'ahead', this.root),
);
}
}
@ -194,9 +224,8 @@ export class BranchNode extends ViewRefNode
}
const item = new TreeItem(
// Hide the current branch checkmark when the node is displayed as a root under the repository node
`${!this.root && this.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`,
TreeItemCollapsibleState.Collapsed,
`${this.options.showCurrent && this.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`,
this.options.expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed,
);
item.contextValue = contextValue;
item.description = description;
@ -219,7 +248,8 @@ export class BranchNode extends ViewRefNode
@log()
async unstar() {
await this.branch.unstar();
void this.parent!.triggerChange();
void this.view.refresh(true);
// void this.parent!.triggerChange();
}
@gate()

+ 120
- 0
src/views/nodes/branchTrackingStatusFilesNode.ts View File

@ -0,0 +1,120 @@
'use strict';
import * as paths from 'path';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
import { BranchTrackingStatus } from './branchTrackingStatusNode';
import { ViewFilesLayout } from '../../configuration';
import { Container } from '../../container';
import { FileNode, FolderNode } from './folderNode';
import { GitBranch, GitFileWithCommit, GitRevision } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { StatusFileNode } from './statusFileNode';
import { Arrays, Iterables, Objects, Strings } from '../../system';
import { ViewsWithFiles } from '../viewBase';
import { ContextValues, ViewNode } from './viewNode';
export class BranchTrackingStatusFilesNode extends ViewNode<ViewsWithFiles> {
static key = ':status-branch:files';
static getId(repoPath: string, name: string, root: boolean, upstream: string, direction: string): string {
return `${BranchNode.getId(repoPath, name, root)}${this.key}(${upstream}|${direction})`;
}
readonly repoPath: string;
constructor(
view: ViewsWithFiles,
parent: ViewNode,
public readonly branch: GitBranch,
public readonly status: Required<BranchTrackingStatus>,
public readonly direction: 'ahead' | 'behind',
private readonly root: boolean = false,
) {
super(GitUri.fromRepoPath(status.repoPath), view, parent);
this.repoPath = status.repoPath;
}
get id(): string {
return BranchTrackingStatusFilesNode.getId(
this.status.repoPath,
this.status.ref,
this.root,
this.status.upstream,
this.direction,
);
}
async getChildren(): Promise<ViewNode[]> {
const log = await Container.git.getLog(this.repoPath, {
limit: 0,
ref: GitRevision.createRange(this.status.upstream, this.branch.ref),
});
const files =
log != null
? [
...Iterables.flatMap(log.commits.values(), c =>
c.files.map(s => {
const file: GitFileWithCommit = { ...s, commit: c };
return file;
}),
),
]
: [];
files.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
const groups = Arrays.groupBy(files, s => s.fileName);
let children: FileNode[] = [
...Iterables.map(
Objects.values(groups),
files =>
new StatusFileNode(
this.view,
this,
this.repoPath,
files[files.length - 1],
files.map(s => s.commit),
),
),
];
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(
children,
n => n.uri.relativePath.split('/'),
(...parts: string[]) => Strings.normalizePath(paths.join(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy, true);
children = root.getChildren() as FileNode[];
} else {
children.sort(
(a, b) =>
a.priority - b.priority ||
a.label!.localeCompare(b.label!, undefined, { numeric: true, sensitivity: 'base' }),
);
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
// if (this.status.state.ahead > 0) {
const stats = await Container.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`);
const files = stats?.files ?? 0;
// }
const label = `${Strings.pluralize('file', files)} changed`;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ContextValues.StatusFiles;
// item.iconPath = {
// dark: Container.context.asAbsolutePath('images/dark/icon-diff.svg'),
// light: Container.context.asAbsolutePath('images/light/icon-diff.svg'),
// };
return item;
}
}

+ 28
- 11
src/views/nodes/branchTrackingStatusNode.ts View File

@ -1,15 +1,16 @@
'use strict';
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
import { BranchTrackingStatusFilesNode } from './branchTrackingStatusFilesNode';
import { CommitNode } from './commitNode';
import { ShowMoreNode } from './common';
import { Container } from '../../container';
import { GitBranch, GitLog, GitRevision, GitTrackingState } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { debug, gate, Iterables, Strings } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { CommitNode } from './commitNode';
import { ShowMoreNode } from './common';
import { insertDateMarkers } from './helpers';
import { debug, gate, Iterables, Strings } from '../../system';
import { ViewsWithFiles } from '../viewBase';
import { ContextValues, PageableViewNode, ViewNode } from './viewNode';
import { BranchNode } from './branchNode';
export interface BranchTrackingStatus {
ref: string;
@ -18,20 +19,20 @@ export interface BranchTrackingStatus {
upstream?: string;
}
export class BranchTrackingStatusNode extends ViewNode<ViewWithFiles> implements PageableViewNode {
static key = ':status:upstream';
export class BranchTrackingStatusNode extends ViewNode<ViewsWithFiles> implements PageableViewNode {
static key = ':status-branch:upstream';
static getId(repoPath: string, name: string, root: boolean, upstream: string, direction: string): string {
return `${BranchNode.getId(repoPath, name, root)}${this.key}(${upstream}|${direction})`;
}
constructor(
view: ViewWithFiles,
view: ViewsWithFiles,
parent: ViewNode,
public readonly branch: GitBranch,
public readonly status: BranchTrackingStatus,
public readonly direction: 'ahead' | 'behind',
// Specifies that the node is shown as a root under the repository node
private readonly _root: boolean = false,
private readonly root: boolean = false,
) {
super(GitUri.fromRepoPath(status.repoPath), view, parent);
}
@ -48,7 +49,7 @@ export class BranchTrackingStatusNode extends ViewNode implements
return BranchTrackingStatusNode.getId(
this.status.repoPath,
this.status.ref,
this._root,
this.root,
this.status.upstream!,
this.direction,
);
@ -94,6 +95,22 @@ export class BranchTrackingStatusNode extends ViewNode implements
if (log.hasMore) {
children.push(new ShowMoreNode(this.view, this, children[children.length - 1]));
}
if (this.status.upstream && this.direction === 'ahead' && this.status.state.ahead > 0) {
children.splice(
0,
0,
new BranchTrackingStatusFilesNode(
this.view,
this,
this.branch,
this.status as Required<BranchTrackingStatus>,
this.direction,
this.root,
),
);
}
return children;
}
@ -105,7 +122,7 @@ export class BranchTrackingStatusNode extends ViewNode implements
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
if (this._root) {
if (this.root) {
item.contextValue = ahead ? ContextValues.StatusAheadOfUpstream : ContextValues.StatusBehindUpstream;
} else {
item.contextValue = ahead

+ 15
- 9
src/views/nodes/branchesNode.ts View File

@ -1,18 +1,19 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchesView } from '../branchesView';
import { BranchNode } from './branchNode';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
import { MessageNode } from './common';
import { ViewBranchesLayout } from '../../configuration';
import { Container } from '../../container';
import { Repository } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { Arrays, debug, gate } from '../../system';
import { RepositoriesView } from '../repositoriesView';
import { BranchNode } from './branchNode';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
import { ContextValues, ViewNode } from './viewNode';
import { RepositoryNode } from './repositoryNode';
import { MessageNode } from './common';
import { Arrays, debug, gate } from '../../system';
import { ContextValues, ViewNode } from './viewNode';
export class BranchesNode extends ViewNode<RepositoriesView> {
export class BranchesNode extends ViewNode<RepositoriesView | BranchesView> {
static key = ':branches';
static getId(repoPath: string): string {
return `${RepositoryNode.getId(repoPath)}${this.key}`;
@ -20,7 +21,12 @@ export class BranchesNode extends ViewNode {
private _children: ViewNode[] | undefined;
constructor(uri: GitUri, view: RepositoriesView, parent: ViewNode, public readonly repo: Repository) {
constructor(
uri: GitUri,
view: RepositoriesView | BranchesView,
parent: ViewNode,
public readonly repo: Repository,
) {
super(uri, view, parent);
}
@ -33,12 +39,12 @@ export class BranchesNode extends ViewNode {
const branches = await this.repo.getBranches({
// only show local branches
filter: b => !b.remote,
sort: true,
sort: this.view instanceof RepositoriesView ? true : { current: false },
});
if (branches.length === 0) return [new MessageNode(this.view, this, 'No branches could be found.')];
const branchNodes = branches.map(
b => new BranchNode(GitUri.fromRepoPath(this.uri.repoPath!, b.ref), this.view, this, b),
b => new BranchNode(GitUri.fromRepoPath(this.uri.repoPath!, b.ref), this.view, this, b, false),
);
if (this.view.config.branches.layout === ViewBranchesLayout.List) return branchNodes;

+ 3
- 3
src/views/nodes/commitNode.ts View File

@ -7,14 +7,14 @@ import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { CommitFormatter, GitBranch, GitLogCommit, GitRevisionReference } from '../../git/git';
import { Arrays, Iterables, Strings } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { CommitFileNode } from './commitFileNode';
import { FileNode, FolderNode } from './folderNode';
import { ContextValues, ViewNode, ViewRefNode } from './viewNode';
export class CommitNode extends ViewRefNode<ViewWithFiles, GitRevisionReference> {
export class CommitNode extends ViewRefNode<ViewsWithFiles, GitRevisionReference> {
constructor(
view: ViewWithFiles,
view: ViewsWithFiles,
parent: ViewNode,
public readonly commit: GitLogCommit,
public readonly branch?: GitBranch,

+ 3
- 1
src/views/nodes/compareNode.ts View File

@ -215,6 +215,8 @@ export class CompareNode extends ViewNode {
}
private getRefName(ref: string | NamedRef) {
return typeof ref === 'string' ? GitRevision.shorten(ref)! : ref.label ?? GitRevision.shorten(ref.ref)!;
return typeof ref === 'string'
? GitRevision.shorten(ref, { strings: { working: 'Working Tree' } })!
: ref.label ?? GitRevision.shorten(ref.ref)!;
}
}

+ 1
- 1
src/views/nodes/fileHistoryNode.ts View File

@ -132,7 +132,7 @@ export class FileHistoryNode extends SubscribeableViewNode implements PageableVi
}
private onRepoChanged(e: RepositoryChangeEvent) {
if (!e.changed(RepositoryChange.Repository)) return;
if (!e.changed(RepositoryChange.Heads)) return;
Logger.log(`FileHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);

+ 3
- 3
src/views/nodes/folderNode.ts View File

@ -3,7 +3,7 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ViewFilesLayout, ViewsFilesConfig } from '../../configuration';
import { GitUri } from '../../git/gitUri';
import { Arrays } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { ContextValues, ViewNode } from './viewNode';
export interface FileNode extends ViewNode {
@ -14,11 +14,11 @@ export interface FileNode extends ViewNode {
root?: Arrays.HierarchicalItem<FileNode>;
}
export class FolderNode extends ViewNode<ViewWithFiles> {
export class FolderNode extends ViewNode<ViewsWithFiles> {
readonly priority: number = 1;
constructor(
view: ViewWithFiles,
view: ViewsWithFiles,
parent: ViewNode,
public readonly repoPath: string,
public readonly folderName: string,

+ 1
- 1
src/views/nodes/lineHistoryNode.ts View File

@ -258,7 +258,7 @@ export class LineHistoryNode extends SubscribeableViewNode implements PageableVi
}
private onRepoChanged(e: RepositoryChangeEvent) {
if (!e.changed(RepositoryChange.Repository)) return;
if (!e.changed(RepositoryChange.Heads)) return;
Logger.log(`LineHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);

+ 3
- 3
src/views/nodes/reflogRecordNode.ts View File

@ -5,13 +5,13 @@ import { Container } from '../../container';
import { GitLog, GitReflogRecord } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { debug, gate, Iterables } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { CommitNode } from './commitNode';
import { MessageNode, ShowMoreNode } from './common';
import { ContextValues, PageableViewNode, ViewNode } from './viewNode';
import { RepositoryNode } from './repositoryNode';
export class ReflogRecordNode extends ViewNode<ViewWithFiles> implements PageableViewNode {
export class ReflogRecordNode extends ViewNode<ViewsWithFiles> implements PageableViewNode {
static key = ':reflog-record';
static getId(
repoPath: string,
@ -26,7 +26,7 @@ export class ReflogRecordNode extends ViewNode implements Pageabl
}|${date.getTime()})`;
}
constructor(view: ViewWithFiles, parent: ViewNode, public readonly record: GitReflogRecord) {
constructor(view: ViewsWithFiles, parent: ViewNode, public readonly record: GitReflogRecord) {
super(GitUri.fromRepoPath(record.repoPath), view, parent);
}

+ 1
- 1
src/views/nodes/remoteNode.ts View File

@ -46,7 +46,7 @@ export class RemoteNode extends ViewNode {
if (branches.length === 0) return [new MessageNode(this.view, this, 'No branches could be found.')];
const branchNodes = branches.map(
b => new BranchNode(GitUri.fromRepoPath(this.uri.repoPath!, b.ref), this.view, this, b),
b => new BranchNode(GitUri.fromRepoPath(this.uri.repoPath!, b.ref), this.view, this, b, false),
);
if (this.view.config.branches.layout === ViewBranchesLayout.List) return branchNodes;

+ 11
- 9
src/views/nodes/repositoryNode.ts View File

@ -90,7 +90,7 @@ export class RepositoryNode extends SubscribeableViewNode {
children.push(new CompareBranchNode(this.uri, this.view, this, branch));
}
if (!this.view.config.repositories.compact) {
if (!this.view.config.compact) {
children.push(new MessageNode(this.view, this, '', GlyphChars.Dash.repeat(2), ''));
}
}
@ -243,7 +243,7 @@ export class RepositoryNode extends SubscribeableViewNode {
@debug()
protected subscribe() {
const disposables = [this.repo.onDidChange(this.onRepoChanged, this)];
const disposables = [this.repo.onDidChange(this.onRepositoryChanged, this)];
// if (Container.config.defaultDateStyle === DateStyle.Relative) {
// disposables.push(Functions.interval(() => void this.updateLastFetched(), 60000));
@ -307,7 +307,7 @@ export class RepositoryNode extends SubscribeableViewNode {
`{ repository: ${e.repository ? e.repository.name : ''}, changes: ${e.changes.join()} }`,
},
})
private onRepoChanged(e: RepositoryChangeEvent) {
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (e.changed(RepositoryChange.Closed)) {
this.dispose();
@ -316,23 +316,25 @@ export class RepositoryNode extends SubscribeableViewNode {
if (
this._children === undefined ||
e.changed(RepositoryChange.Repository) ||
e.changed(RepositoryChange.Config)
e.changed(RepositoryChange.Config) ||
e.changed(RepositoryChange.Index) ||
e.changed(RepositoryChange.Heads) ||
e.changed(RepositoryChange.Unknown)
) {
void this.triggerChange(true);
return;
}
if (e.changed(RepositoryChange.Stashes)) {
const node = this._children.find(c => c instanceof StashesNode);
if (e.changed(RepositoryChange.Remotes)) {
const node = this._children.find(c => c instanceof RemotesNode);
if (node !== undefined) {
void this.view.triggerNodeChange(node);
}
}
if (e.changed(RepositoryChange.Remotes)) {
const node = this._children.find(c => c instanceof RemotesNode);
if (e.changed(RepositoryChange.Stash)) {
const node = this._children.find(c => c instanceof StashesNode);
if (node !== undefined) {
void this.view.triggerNodeChange(node);
}

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

@ -4,7 +4,7 @@ import { Container } from '../../container';
import { GitLog } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { debug, gate, Iterables, Promises } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { CommitNode } from './commitNode';
import { ShowMoreNode } from './common';
import { insertDateMarkers } from './helpers';
@ -17,9 +17,9 @@ export interface CommitsQueryResults {
more?(limit: number | undefined): Promise<void>;
}
export class ResultsCommitsNode extends ViewNode<ViewWithFiles> implements PageableViewNode {
export class ResultsCommitsNode extends ViewNode<ViewsWithFiles> implements PageableViewNode {
constructor(
view: ViewWithFiles,
view: ViewsWithFiles,
parent: ViewNode,
public readonly repoPath: string,
private _label: string,

+ 3
- 3
src/views/nodes/resultsFilesNode.ts View File

@ -5,7 +5,7 @@ import { ViewFilesLayout } from '../../configuration';
import { GitFile } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { Arrays, debug, gate, Iterables, Promises, Strings } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { FileNode, FolderNode } from './folderNode';
import { ResultsFileNode } from './resultsFileNode';
import { ContextValues, ViewNode } from './viewNode';
@ -15,9 +15,9 @@ export interface FilesQueryResults {
diff: GitFile[] | undefined;
}
export class ResultsFilesNode extends ViewNode<ViewWithFiles> {
export class ResultsFilesNode extends ViewNode<ViewsWithFiles> {
constructor(
view: ViewWithFiles,
view: ViewsWithFiles,
parent: ViewNode,
public readonly repoPath: string,
public readonly ref1: string,

+ 2
- 2
src/views/nodes/searchResultsCommitsNode.ts View File

@ -1,7 +1,7 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, SearchCommitsCommandArgs } from '../../commands';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { CommitsQueryResults, ResultsCommitsNode } from './resultsCommitsNode';
import { ContextValues, ViewNode } from './viewNode';
import { RepositoryNode } from './repositoryNode';
@ -20,7 +20,7 @@ export class SearchResultsCommitsNode extends ResultsCommitsNode {
private _instanceId: number;
constructor(
view: ViewWithFiles,
view: ViewsWithFiles,
parent: ViewNode,
repoPath: string,
public readonly search: SearchPattern,

+ 3
- 3
src/views/nodes/stashNode.ts View File

@ -4,20 +4,20 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../../container';
import { CommitFormatter, GitStashCommit, GitStashReference } from '../../git/git';
import { Arrays, Iterables, Strings } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { StashFileNode } from './stashFileNode';
import { ContextValues, ViewNode, ViewRefNode } from './viewNode';
import { RepositoryNode } from './repositoryNode';
import { FileNode, FolderNode } from '../nodes';
import { ViewFilesLayout } from '../../config';
export class StashNode extends ViewRefNode<ViewWithFiles, GitStashReference> {
export class StashNode extends ViewRefNode<ViewsWithFiles, GitStashReference> {
static key = ':stash';
static getId(repoPath: string, ref: string): string {
return `${RepositoryNode.getId(repoPath)}${this.key}(${ref})`;
}
constructor(view: ViewWithFiles, parent: ViewNode, public readonly commit: GitStashCommit) {
constructor(view: ViewsWithFiles, parent: ViewNode, public readonly commit: GitStashCommit) {
super(commit.toGitUri(), view, parent);
}

+ 3
- 3
src/views/nodes/stashesNode.ts View File

@ -4,19 +4,19 @@ import { Container } from '../../container';
import { Repository } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { Iterables } from '../../system';
import { ViewWithFiles } from '../viewBase';
import { ViewsWithFiles } from '../viewBase';
import { MessageNode } from './common';
import { StashNode } from './stashNode';
import { ContextValues, ViewNode } from './viewNode';
import { RepositoryNode } from './repositoryNode';
export class StashesNode extends ViewNode<ViewWithFiles> {
export class StashesNode extends ViewNode<ViewsWithFiles> {
static key = ':stashes';
static getId(repoPath: string): string {
return `${RepositoryNode.getId(repoPath)}${this.key}`;
}
constructor(uri: GitUri, view: ViewWithFiles, parent: ViewNode, public readonly repo: Repository) {
constructor(uri: GitUri, view: ViewsWithFiles, parent: ViewNode, public readonly repo: Repository) {
super(uri, view, parent);
}

+ 12
- 4
src/views/nodes/statusFilesNode.ts View File

@ -11,6 +11,7 @@ import {
GitRevision,
GitStatus,
GitStatusFile,
GitTrackingState,
} from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { Arrays, Iterables, Objects, Strings } from '../../system';
@ -31,7 +32,14 @@ export class StatusFilesNode extends ViewNode {
constructor(
view: RepositoriesView,
parent: ViewNode,
public readonly status: GitStatus,
public readonly status:
| GitStatus
| {
readonly repoPath: string;
readonly files: GitStatusFile[];
readonly state: GitTrackingState;
readonly upstream?: string;
},
public readonly range: string | undefined,
) {
super(GitUri.fromRepoPath(status.repoPath), view, parent);
@ -39,7 +47,7 @@ export class StatusFilesNode extends ViewNode {
}
get id(): string {
return StatusFilesNode.getId(this.status.repoPath);
return StatusFilesNode.getId(this.repoPath);
}
async getChildren(): Promise<ViewNode[]> {
@ -125,9 +133,9 @@ export class StatusFilesNode extends ViewNode {
}
async getTreeItem(): Promise<TreeItem> {
let files = this.status.files !== undefined && this.includeWorkingTree ? this.status.files.length : 0;
let files = this.includeWorkingTree ? this.status.files.length : 0;
if (this.status.upstream !== undefined && this.status.state.ahead > 0) {
if (this.status.upstream != null && this.status.state.ahead > 0) {
if (files > 0) {
const aheadFiles = await Container.git.getDiffStatus(this.repoPath, `${this.status.upstream}...`);

+ 2
- 0
src/views/nodes/viewNode.ts View File

@ -13,6 +13,7 @@ export enum ContextValues {
Branches = 'gitlens:branches',
BranchStatusAheadOfUpstream = 'gitlens:status-branch:upstream:ahead',
BranchStatusBehindUpstream = 'gitlens:status-branch:upstream:behind',
BranchStatusFiles = 'gitlens:status-branch:files',
Commit = 'gitlens:commit',
Commits = 'gitlens:commits',
Compare = 'gitlens:compare',
@ -34,6 +35,7 @@ export enum ContextValues {
Remotes = 'gitlens:remotes',
Repositories = 'gitlens:repositories',
Repository = 'gitlens:repository',
RepositoryFolder = 'gitlens:repo-folder',
ResultsCommits = 'gitlens:results:commits',
ResultsFile = 'gitlens:file:results',
ResultsFiles = 'gitlens:results:files',

+ 16
- 53
src/views/repositoriesView.ts View File

@ -13,7 +13,6 @@ import {
RepositoriesViewConfig,
ViewBranchesLayout,
ViewFilesLayout,
ViewsConfig,
ViewShowBranchComparison,
} from '../configuration';
import { CommandContext, setCommandContext, WorkspaceState } from '../constants';
@ -44,7 +43,9 @@ import {
import { gate } from '../system';
import { ViewBase } from './viewBase';
export class RepositoriesView extends ViewBase<RepositoriesNode> {
export class RepositoriesView extends ViewBase<RepositoriesNode, RepositoriesViewConfig> {
protected readonly configKey = 'repositories';
constructor() {
super('gitlens.views.repositories', 'Repositories');
}
@ -120,22 +121,10 @@ export class RepositoriesView extends ViewBase {
);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!configuration.changed(e, 'views', 'repositories') &&
!configuration.changed(e, 'views', 'commitFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFileFormat') &&
!configuration.changed(e, 'views', 'commitDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFormat') &&
!configuration.changed(e, 'views', 'defaultItemLimit') &&
!configuration.changed(e, 'views', 'pageItemLimit') &&
!configuration.changed(e, 'views', 'showRelativeDateMarkers') &&
!configuration.changed(e, 'views', 'stashFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'stashFileFormat') &&
!configuration.changed(e, 'views', 'stashDescriptionFormat') &&
!configuration.changed(e, 'views', 'stashFormat') &&
!configuration.changed(e, 'views', 'statusFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'statusFileFormat') &&
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
@ -143,18 +132,21 @@ export class RepositoriesView extends ViewBase {
!configuration.changed(e, 'sortBranchesBy') &&
!configuration.changed(e, 'sortTagsBy')
) {
return;
return false;
}
if (configuration.changed(e, 'views', 'repositories', 'autoRefresh')) {
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'views', this.configKey, 'autoRefresh')) {
void this.setAutoRefresh(Container.config.views.repositories.autoRefresh);
}
if (configuration.changed(e, 'views', 'repositories', 'location')) {
if (configuration.changed(e, 'views', this.configKey, 'location')) {
this.initialize(this.config.location, { showCollapseAll: true });
}
if (!configuration.initializing(e) && this._root !== undefined) {
if (!configuration.initializing(e) && this._root != null) {
void this.refresh(true);
}
}
@ -166,10 +158,6 @@ export class RepositoriesView extends ViewBase {
);
}
get config(): ViewsConfig & RepositoriesViewConfig {
return { ...Container.config.views, ...Container.config.views.repositories };
}
findBranch(branch: GitBranchReference, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(branch.repoPath);
@ -555,31 +543,6 @@ export class RepositoriesView extends ViewBase {
return node;
}
private async ensureRevealNode(
node: ViewNode,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
// Not sure why I need to reveal each parent, but without it the node won't be revealed
const nodes: ViewNode[] = [];
let parent: ViewNode | undefined = node;
while (parent !== undefined) {
nodes.push(parent);
parent = parent.getParent();
}
nodes.pop();
for (const n of nodes.reverse()) {
try {
await this.reveal(n, options);
} catch {}
}
}
private async setAutoRefresh(enabled: boolean, workspaceEnabled?: boolean) {
if (enabled) {
if (workspaceEnabled === undefined) {
@ -607,14 +570,14 @@ export class RepositoriesView extends ViewBase {
}
private setBranchesLayout(layout: ViewBranchesLayout) {
return configuration.updateEffective('views', 'repositories', 'branches', 'layout', layout);
return configuration.updateEffective('views', this.configKey, 'branches', 'layout', layout);
}
private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective('views', 'repositories', 'files', 'layout', layout);
return configuration.updateEffective('views', this.configKey, 'files', 'layout', layout);
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', 'repositories', 'avatars', enabled);
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 18
- 26
src/views/searchView.ts View File

@ -1,6 +1,6 @@
'use strict';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, SearchViewConfig, ViewFilesLayout, ViewsConfig } from '../configuration';
import { configuration, SearchViewConfig, ViewFilesLayout } from '../configuration';
import { CommandContext, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
import { GitLog, SearchPattern } from '../git/git';
@ -15,7 +15,9 @@ interface SearchQueryResults {
more?(limit: number | undefined): Promise<void>;
}
export class SearchView extends ViewBase<SearchNode> {
export class SearchView extends ViewBase<SearchNode, SearchViewConfig> {
protected readonly configKey = 'search';
constructor() {
super('gitlens.views.search', 'Search Commits');
@ -65,51 +67,41 @@ export class SearchView extends ViewBase {
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!configuration.changed(e, 'views', 'search') &&
!configuration.changed(e, 'views', 'commitFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFileFormat') &&
!configuration.changed(e, 'views', 'commitDescriptionFormat') &&
!configuration.changed(e, 'views', 'commitFormat') &&
!configuration.changed(e, 'views', 'defaultItemLimit') &&
!configuration.changed(e, 'views', 'pageItemLimit') &&
!configuration.changed(e, 'views', 'showRelativeDateMarkers') &&
!configuration.changed(e, 'views', 'statusFileDescriptionFormat') &&
!configuration.changed(e, 'views', 'statusFileFormat') &&
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
!configuration.changed(e, 'defaultGravatarsStyle')
) {
return;
return false;
}
if (configuration.changed(e, 'views', 'search', 'location')) {
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'views', this.configKey, 'location')) {
this.initialize(this.config.location, { showCollapseAll: true });
}
if (!configuration.initializing(e) && this._root !== undefined) {
if (!configuration.initializing(e) && this._root != null) {
void this.refresh(true);
}
}
get config(): ViewsConfig & SearchViewConfig {
return { ...Container.config.views, ...Container.config.views.search };
}
get keepResults(): boolean {
return Container.context.workspaceState.get<boolean>(WorkspaceState.ViewsSearchKeepResults, false);
}
clear() {
if (this._root === undefined) return;
this._root.clear();
this._root?.clear();
}
dismissNode(node: ViewNode) {
if (this._root === undefined) return;
if (this._root == null) return;
if (nodeSupportsConditionalDismissal(node) && node.canDismiss() === false) return;
this._root.dismiss(node);
@ -287,7 +279,7 @@ export class SearchView extends ViewBase {
}
private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective('views', 'search', 'files', 'layout', layout);
return configuration.updateEffective('views', this.configKey, 'files', 'layout', layout);
}
private setKeepResults(enabled: boolean) {
@ -296,6 +288,6 @@ export class SearchView extends ViewBase {
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', 'search', 'avatars', enabled);
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 113
- 12
src/views/viewBase.ts View File

@ -16,26 +16,66 @@ import {
TreeViewVisibilityChangeEvent,
window,
} from 'vscode';
import { configuration } from '../configuration';
import { BranchesView } from './branchesView';
import { HistoryView } from './historyView';
import { CompareView } from './compareView';
import {
BranchesViewConfig,
CompareViewConfig,
configuration,
ContributorsViewConfig,
FileHistoryViewConfig,
HistoryViewConfig,
LineHistoryViewConfig,
RemotesViewConfig,
RepositoriesViewConfig,
SearchViewConfig,
StashesViewConfig,
TagsViewConfig,
ViewsCommonConfig,
viewsCommonConfigKeys,
ViewsConfigKeys,
viewsConfigKeys,
} from '../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { Logger } from '../logger';
import { debug, Functions, log, Promises, Strings } from '../system';
import { CompareView } from './compareView';
import { FileHistoryView } from './fileHistoryView';
import { LineHistoryView } from './lineHistoryView';
import { Logger } from '../logger';
import { PageableViewNode, ViewNode } from './nodes';
import { RepositoriesView } from './repositoriesView';
import { SearchView } from './searchView';
import { debug, Functions, log, Promises, Strings } from '../system';
export type View = RepositoriesView | FileHistoryView | LineHistoryView | CompareView | SearchView;
export type ViewWithFiles = RepositoriesView | CompareView | SearchView;
export type View =
| BranchesView
| CompareView
| FileHistoryView
| HistoryView
| LineHistoryView
| RepositoriesView
| SearchView;
export type ViewsWithFiles = BranchesView | CompareView | HistoryView | RepositoriesView | SearchView;
export interface TreeViewNodeStateChangeEvent<T> extends TreeViewExpansionEvent<T> {
state: TreeItemCollapsibleState;
}
export abstract class ViewBase<TRoot extends ViewNode<View>> implements TreeDataProvider<ViewNode>, Disposable {
export abstract class ViewBase<
RootNode extends ViewNode<View>,
ViewConfig extends
| BranchesViewConfig
| CompareViewConfig
| ContributorsViewConfig
| FileHistoryViewConfig
| HistoryViewConfig
| LineHistoryViewConfig
| RemotesViewConfig
| RepositoriesViewConfig
| SearchViewConfig
| StashesViewConfig
| TagsViewConfig
> implements TreeDataProvider<ViewNode>, Disposable {
protected _onDidChangeTreeData = new EventEmitter<ViewNode | undefined>();
get onDidChangeTreeData(): Event<ViewNode | undefined> {
return this._onDidChangeTreeData.event;
@ -53,13 +93,13 @@ export abstract class ViewBase> implements TreeData
protected _disposable: Disposable | undefined;
private readonly _lastKnownLimits = new Map<string, number | undefined>();
protected _root: TRoot | undefined;
protected _root: RootNode | undefined;
protected _tree: TreeView<ViewNode> | undefined;
constructor(public readonly id: string, public readonly name: string) {
if (Logger.isDebugging) {
const fn = this.getTreeItem;
this.getTreeItem = async function (this: ViewBase<TRoot>, node: ViewNode) {
this.getTreeItem = async function (this: ViewBase<RootNode, ViewConfig>, node: ViewNode) {
const item = await fn.apply(this, [node]);
const parent = node.getParent();
@ -78,10 +118,28 @@ export abstract class ViewBase> implements TreeData
this.registerCommands();
Container.context.subscriptions.push(configuration.onDidChange(this.onConfigurationChanged, this));
Container.context.subscriptions.push(
configuration.onDidChange(e => {
if (!this.filterConfigurationChanged(e)) return;
this._config = undefined;
this.onConfigurationChanged(e);
}, this),
);
setImmediate(() => this.onConfigurationChanged(configuration.initializingChangeEvent));
}
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
if (!configuration.changed(e, 'views')) return false;
if (configuration.changed(e, 'views', this.configKey)) return true;
for (const key of viewsCommonConfigKeys) {
if (configuration.changed(e, 'views', key)) return true;
}
return false;
}
dispose() {
this._disposable?.dispose();
}
@ -136,9 +194,11 @@ export abstract class ViewBase> implements TreeData
return `${this.id}.${command}`;
}
protected abstract get location(): string;
protected get location(): string | undefined {
return undefined;
}
protected abstract getRoot(): TRoot;
protected abstract getRoot(): RootNode;
protected abstract registerCommands(): void;
protected abstract onConfigurationChanged(e: ConfigurationChangeEvent): void;
@ -351,6 +411,31 @@ export abstract class ViewBase> implements TreeData
return undefined;
}
protected async ensureRevealNode(
node: ViewNode,
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
},
) {
// Not sure why I need to reveal each parent, but without it the node won't be revealed
const nodes: ViewNode[] = [];
let parent: ViewNode | undefined = node;
while (parent !== undefined) {
nodes.push(parent);
parent = parent.getParent();
}
nodes.pop();
for (const n of nodes.reverse()) {
try {
await this.reveal(n, options);
} catch {}
}
}
@debug()
async refresh(reset: boolean = false) {
await this._root?.refresh?.(reset);
@ -454,4 +539,20 @@ export abstract class ViewBase> implements TreeData
// Since the root node won't actually refresh, force everything
this._onDidChangeTreeData.fire(node != null && node !== this._root ? node : undefined);
}
protected abstract readonly configKey: ViewsConfigKeys;
private _config: (ViewConfig & ViewsCommonConfig) | undefined;
get config(): ViewConfig & ViewsCommonConfig {
if (this._config == null) {
const cfg = { ...Container.config.views };
for (const view of viewsConfigKeys) {
delete cfg[view];
}
this._config = { ...(cfg as ViewsCommonConfig), ...(Container.config.views[this.configKey] as ViewConfig) };
}
return this._config;
}
}

Loading…
Cancel
Save