import { CancellationToken, commands, ConfigurationChangeEvent, Disposable, ProgressLocation, TreeItem, TreeItemCollapsibleState, window, } from 'vscode'; import { BranchesViewConfig, configuration, ViewBranchesLayout, ViewFilesLayout, ViewShowBranchComparison, } from '../configuration'; import { Commands } from '../constants'; import { Container } from '../container'; import { GitUri } from '../git/gitUri'; import { GitBranchReference, GitCommit, GitReference, GitRevisionReference, RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, } from '../git/models'; import { executeCommand } from '../system/command'; import { gate } from '../system/decorators/gate'; import { BranchesNode, BranchNode, BranchOrTagFolderNode, RepositoriesSubscribeableNode, RepositoryFolderNode, RepositoryNode, ViewNode, } from './nodes'; import { ViewBase } from './viewBase'; export class BranchesRepositoryNode extends RepositoryFolderNode { async getChildren(): Promise { if (this.child == null) { this.child = new BranchesNode(this.uri, this.view, this, this.repo); } return this.child.getChildren(); } protected changed(e: RepositoryChangeEvent) { return e.changed( RepositoryChange.Config, RepositoryChange.Heads, RepositoryChange.Index, RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChange.Status, RepositoryChange.Unknown, RepositoryChangeComparisonMode.Any, ); } } export class BranchesViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No branches could be found.'; return []; } this.view.message = undefined; const splat = repositories.length === 1; this.children = repositories.map( r => new BranchesRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, splat), ); } if (this.children.length === 1) { const [child] = this.children; const branches = await child.repo.getBranches({ filter: b => !b.remote }); if (branches.values.length === 0) { this.view.message = 'No branches could be found.'; this.view.title = 'Branches'; void child.ensureSubscription(); return []; } this.view.message = undefined; this.view.title = `Branches (${branches.values.length})`; return child.getChildren(); } return this.children; } getTreeItem(): TreeItem { const item = new TreeItem('Branches', TreeItemCollapsibleState.Expanded); return item; } } export class BranchesView extends ViewBase { protected readonly configKey = 'branches'; constructor(container: Container) { super('gitlens.views.branches', 'Branches', container); } override get canReveal(): boolean { return this.config.reveal || !configuration.get('views.repositories.showBranches'); } protected getRoot() { return new BranchesViewNode(this); } protected registerCommands(): Disposable[] { void this.container.viewCommands; return [ commands.registerCommand( this.getQualifiedCommand('copy'), () => executeCommand(Commands.ViewsCopy, this.selection), this, ), commands.registerCommand( this.getQualifiedCommand('refresh'), () => { this.container.git.resetCaches('branches'); return this.refresh(true); }, this, ), commands.registerCommand( this.getQualifiedCommand('setLayoutToList'), () => this.setLayout(ViewBranchesLayout.List), this, ), commands.registerCommand( this.getQualifiedCommand('setLayoutToTree'), () => this.setLayout(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, ), commands.registerCommand( this.getQualifiedCommand('setShowBranchComparisonOn'), () => this.setShowBranchComparison(true), this, ), commands.registerCommand( this.getQualifiedCommand('setShowBranchComparisonOff'), () => this.setShowBranchComparison(false), this, ), commands.registerCommand( this.getQualifiedCommand('setShowBranchPullRequestOn'), () => this.setShowBranchPullRequest(true), this, ), commands.registerCommand( this.getQualifiedCommand('setShowBranchPullRequestOff'), () => this.setShowBranchPullRequest(false), this, ), ]; } protected override filterConfigurationChanged(e: ConfigurationChangeEvent) { const changed = super.filterConfigurationChanged(e); if ( !changed && !configuration.changed(e, 'defaultDateFormat') && !configuration.changed(e, 'defaultDateShortFormat') && !configuration.changed(e, 'defaultDateSource') && !configuration.changed(e, 'defaultDateStyle') && !configuration.changed(e, 'defaultGravatarsStyle') && !configuration.changed(e, 'defaultTimeFormat') && !configuration.changed(e, 'sortBranchesBy') ) { return false; } return true; } findBranch(branch: GitBranchReference, token?: CancellationToken) { if (branch.remote) return undefined; const repoNodeId = RepositoryNode.getId(branch.repoPath); return this.findNode((n: any) => n.branch?.ref === branch.ref, { allowPaging: true, maxDepth: 4, canTraverse: n => { if (n instanceof BranchesViewNode) return true; if (n instanceof BranchesRepositoryNode || n instanceof BranchOrTagFolderNode) { return n.id.startsWith(repoNodeId); } return false; }, token: token, }); } async findCommit(commit: GitCommit | { repoPath: string; ref: string }, token?: CancellationToken) { const repoNodeId = RepositoryNode.getId(commit.repoPath); // Get all the branches the commit is on const branches = await this.container.git.getCommitBranches( commit.repoPath, commit.ref, GitCommit.is(commit) ? { commitDate: commit.committer.date } : undefined, ); if (branches.length === 0) return undefined; return this.findNode((n: any) => n.commit?.ref === commit.ref, { allowPaging: true, maxDepth: 5, canTraverse: async n => { if (n instanceof BranchesViewNode) return true; if (n instanceof BranchesRepositoryNode || n instanceof BranchOrTagFolderNode) { return n.id.startsWith(repoNodeId); } if (n instanceof BranchNode && branches.includes(n.branch.name)) { await n.loadMore({ until: commit.ref }); return true; } 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, quoted: true })} in the side bar...`, cancellable: true, }, async (progress, token) => { const node = await this.findBranch(branch, token); if (node == null) return undefined; 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, quoted: true })} in the side bar...`, cancellable: true, }, async (progress, token) => { const node = await this.findCommit(commit, token); if (node == null) return undefined; await this.ensureRevealNode(node, options); return node; }, ); } @gate(() => '') async revealRepository( repoPath: string, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }, ) { const node = await this.findNode(RepositoryFolderNode.getId(repoPath), { maxDepth: 1, canTraverse: n => n instanceof BranchesViewNode || n instanceof RepositoryFolderNode, }); if (node !== undefined) { await this.reveal(node, options); } return node; } private setLayout(layout: ViewBranchesLayout) { return configuration.updateEffective(`views.${this.configKey}.branches.layout` as const, layout); } private setFilesLayout(layout: ViewFilesLayout) { return configuration.updateEffective(`views.${this.configKey}.files.layout` as const, layout); } private setShowAvatars(enabled: boolean) { return configuration.updateEffective(`views.${this.configKey}.avatars` as const, enabled); } private setShowBranchComparison(enabled: boolean) { return configuration.updateEffective( `views.${this.configKey}.showBranchComparison` as const, enabled ? ViewShowBranchComparison.Branch : false, ); } private async setShowBranchPullRequest(enabled: boolean) { await configuration.updateEffective(`views.${this.configKey}.pullRequests.showForBranches` as const, enabled); await configuration.updateEffective(`views.${this.configKey}.pullRequests.enabled` as const, enabled); } }