'use strict'; import { CancellationToken, commands, ConfigurationChangeEvent, ProgressLocation, TreeItem, TreeItemCollapsibleState, window, } from 'vscode'; import { configuration, RemotesViewConfig, ViewBranchesLayout, ViewFilesLayout } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; import { GitBranch, GitBranchReference, GitLogCommit, GitReference, GitRemote, GitRevisionReference, RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent, } from '../git/git'; import { GitUri } from '../git/gitUri'; import { BranchNode, BranchOrTagFolderNode, RemoteNode, RemotesNode, RepositoryFolderNode, RepositoryNode, unknownGitUri, ViewNode, } from './nodes'; import { debug, gate, Strings } from '../system'; import { ViewBase } from './viewBase'; export class RemotesRepositoryNode extends RepositoryFolderNode { async getChildren(): Promise { if (this.child == null) { this.child = new RemotesNode(this.uri, this.view, this, this.repo); } return this.child.getChildren(); } protected changed(e: RepositoryChangeEvent) { return e.changed( RepositoryChange.Config, RepositoryChange.Remotes, RepositoryChange.Unknown, RepositoryChangeComparisonMode.Any, ); } } export class RemotesViewNode extends ViewNode { protected splatted = true; private children: RemotesRepositoryNode[] | undefined; constructor(view: RemotesView) { super(unknownGitUri, view); } async getChildren(): Promise { if (this.children == null) { const repositories = await Container.git.getOrderedRepositories(); if (repositories.length === 0) { this.view.message = 'No remotes could be found.'; return []; } this.view.message = undefined; const splat = repositories.length === 1; this.children = repositories.map( r => new RemotesRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, splat), ); } if (this.children.length === 1) { const [child] = this.children; if (!child.repo.supportsChangeEvents) { this.view.description = `${Strings.pad(GlyphChars.Warning, 0, 2)}Auto-refresh unavailable`; } const remotes = await child.repo.getRemotes(); if (remotes.length === 0) { this.view.message = 'No remotes could be found.'; this.view.title = 'Remotes'; void child.ensureSubscription(); return []; } this.view.message = undefined; this.view.title = `Remotes (${remotes.length})`; return child.getChildren(); } return this.children; } getTreeItem(): TreeItem { const item = new TreeItem('Remotes', TreeItemCollapsibleState.Expanded); return item; } async getSplattedChild() { if (this.children == null) { await this.getChildren(); } return this.children?.length === 1 ? this.children[0] : undefined; } @gate() @debug() refresh(reset: boolean = false) { if (reset && this.children != null) { for (const child of this.children) { child.dispose(); } this.children = undefined; } } } export class RemotesView extends ViewBase { protected readonly configKey = 'remotes'; constructor() { super('gitlens.views.remotes', 'Remotes'); } getRoot() { return new RemotesViewNode(this); } protected registerCommands() { void Container.viewCommands; commands.registerCommand( this.getQualifiedCommand('copy'), () => commands.executeCommand('gitlens.views.copy', this.selection), this, ); commands.registerCommand( this.getQualifiedCommand('refresh'), async () => { await Container.git.resetCaches('branches', 'remotes'); 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('setShowBranchPullRequestOn'), () => this.setShowBranchPullRequest(true), this, ); commands.registerCommand( this.getQualifiedCommand('setShowBranchPullRequestOff'), () => this.setShowBranchPullRequest(false), this, ); } protected 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, 'integrations', 'enabled') && !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: 5, canTraverse: n => { if (n instanceof RemotesViewNode) return true; if (n instanceof RemotesRepositoryNode || n instanceof BranchOrTagFolderNode) { return n.id.startsWith(repoNodeId); } if (n instanceof RemoteNode) { if (!n.id.startsWith(repoNodeId)) return false; return n.remote.name === GitBranch.getRemote(branch.name); } return false; }, token: token, }); } async findCommit(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) { const repoNodeId = RepositoryNode.getId(commit.repoPath); // Get all the remote branches the commit is on const 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: 6, canTraverse: n => { if (n instanceof RemotesViewNode) return true; if (n instanceof RemotesRepositoryNode || n instanceof BranchOrTagFolderNode) { return n.id.startsWith(repoNodeId); } 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, }); } findRemote(remote: GitRemote, token?: CancellationToken) { const repoNodeId = RepositoryNode.getId(remote.repoPath); return this.findNode((n: any) => n.remote?.name === remote.name, { allowPaging: true, maxDepth: 2, canTraverse: n => { if (n instanceof RemotesViewNode) return true; if (n instanceof RemotesRepositoryNode) { 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 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 })} 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(() => '') revealRemote( remote: GitRemote, options?: { select?: boolean; focus?: boolean; expand?: boolean | number; }, ) { return window.withProgress( { location: ProgressLocation.Notification, title: `Revealing remote ${remote.name} in the side bar...`, cancellable: true, }, async (progress, token) => { const node = await this.findRemote(remote, token); if (node == null) return undefined; await this.ensureRevealNode(node, options); return node; }, ); } private setLayout(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); } private async setShowBranchPullRequest(enabled: boolean) { await configuration.updateEffective('views', this.configKey, 'pullRequests', 'showForBranches', enabled); await configuration.updateEffective('views', this.configKey, 'pullRequests', 'enabled', enabled); } }