From 5999a261c811d3e66f16e67b7da07b5d08a5fd00 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 13 Oct 2023 14:57:09 -0400 Subject: [PATCH] Optimizes git calls for focus view Adds PageableResult class helper --- src/git/models/branch.ts | 23 +++++------ src/git/models/worktree.ts | 29 ++++++++----- src/plus/webviews/focus/focusWebview.ts | 72 ++++++++++++++++++++++++--------- src/system/paging.ts | 31 ++++++++++++++ 4 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 src/system/paging.ts diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index e8e647a..a1a0f40 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -5,6 +5,7 @@ import { formatDate, fromNow } from '../../system/date'; import { debug } from '../../system/decorators/log'; import { memoize } from '../../system/decorators/memoize'; import { getLoggableName } from '../../system/logger'; +import { PageableResult } from '../../system/paging'; import { sortCompare } from '../../system/string'; import type { PullRequest, PullRequestState } from './pullRequest'; import type { GitBranchReference, GitReference } from './reference'; @@ -291,6 +292,7 @@ export function sortBranches(branches: GitBranch[], options?: BranchSortOptions) export async function getLocalBranchByUpstream( repo: Repository, remoteBranchName: string, + branches?: PageableResult, ): Promise { let qualifiedRemoteBranchName; if (remoteBranchName.startsWith('remotes/')) { @@ -300,19 +302,16 @@ export async function getLocalBranchByUpstream( qualifiedRemoteBranchName = `remotes/${remoteBranchName}`; } - let branches; - do { - branches = await repo.getBranches(branches != null ? { paging: branches.paging } : undefined); - for (const branch of branches.values) { - if ( - !branch.remote && - branch.upstream?.name != null && - (branch.upstream.name === remoteBranchName || branch.upstream.name === qualifiedRemoteBranchName) - ) { - return branch; - } + branches ??= new PageableResult(p => repo.getBranches(p != null ? { paging: p } : undefined)); + for await (const branch of branches.values()) { + if ( + !branch.remote && + branch.upstream?.name != null && + (branch.upstream.name === remoteBranchName || branch.upstream.name === qualifiedRemoteBranchName) + ) { + return branch; } - } while (branches.paging?.more); + } return undefined; } diff --git a/src/git/models/worktree.ts b/src/git/models/worktree.ts index 4c38a1a..ad8f3f9 100644 --- a/src/git/models/worktree.ts +++ b/src/git/models/worktree.ts @@ -2,6 +2,7 @@ import type { Uri, WorkspaceFolder } from 'vscode'; import { workspace } from 'vscode'; import { Container } from '../../container'; import { memoize } from '../../system/decorators/memoize'; +import { PageableResult } from '../../system/paging'; import { normalizePath, relative } from '../../system/path'; import type { GitBranch } from './branch'; import { shortenRevision } from './reference'; @@ -84,26 +85,34 @@ export class GitWorktree { export async function getWorktreeForBranch( repo: Repository, branchName: string, - upstreamNames?: string | string[], + upstreamNames: string | string[], + worktrees?: GitWorktree[], + branches?: PageableResult, ): Promise { if (upstreamNames != null && !Array.isArray(upstreamNames)) { upstreamNames = [upstreamNames]; } - const worktrees = await repo.getWorktrees(); + worktrees ??= await repo.getWorktrees(); for (const worktree of worktrees) { if (worktree.branch === branchName) return worktree; if (upstreamNames == null || worktree.branch == null) continue; - const branch = await repo.getBranch(worktree.branch); - if ( - branch?.upstream?.name != null && - (upstreamNames.includes(branch.upstream.name) || - (branch.upstream.name.startsWith('remotes/') && - upstreamNames.includes(branch.upstream.name.substring(8)))) - ) { - return worktree; + branches ??= new PageableResult(p => repo.getBranches(p != null ? { paging: p } : undefined)); + for await (const branch of branches.values()) { + if (branch.name === worktree.branch) { + if ( + branch.upstream?.name != null && + (upstreamNames.includes(branch.upstream.name) || + (branch.upstream.name.startsWith('remotes/') && + upstreamNames.includes(branch.upstream.name.substring(8)))) + ) { + return worktree; + } + + break; + } } } diff --git a/src/plus/webviews/focus/focusWebview.ts b/src/plus/webviews/focus/focusWebview.ts index 50e73a5..4d9078d 100644 --- a/src/plus/webviews/focus/focusWebview.ts +++ b/src/plus/webviews/focus/focusWebview.ts @@ -20,11 +20,15 @@ import { createReference } from '../../../git/models/reference'; import type { GitRemote } from '../../../git/models/remote'; import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository'; import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository'; +import type { GitWorktree } from '../../../git/models/worktree'; import { getWorktreeForBranch } from '../../../git/models/worktree'; import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser'; import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider'; import { executeCommand, registerCommand } from '../../../system/command'; import { debug } from '../../../system/decorators/log'; +import { Logger } from '../../../system/logger'; +import { getLogScope } from '../../../system/logger.scope'; +import { PageableResult } from '../../../system/paging'; import { getSettledValue } from '../../../system/promise'; import type { IpcMessage } from '../../../webviews/protocol'; import { onIpc } from '../../../webviews/protocol'; @@ -541,22 +545,30 @@ export class FocusWebviewProvider implements WebviewProvider { richRepos: RepoWithRichRemote[], force?: boolean, ): Promise { + const scope = getLogScope(); + if (force || this._pullRequests == null) { const allPrs: SearchedPullRequestWithRemote[] = []; - for (const richRepo of richRepos) { - const remote = richRepo.remote; - const prs = await this.container.git.getMyPullRequests(remote); - if (prs == null) { - continue; + + const branchesByRepo = new Map>(); + const worktreesByRepo = new Map(); + + const queries = richRepos.map(r => [r, this.container.git.getMyPullRequests(r.remote)] as const); + for (const [r, query] of queries) { + let prs; + try { + prs = await query; + } catch (ex) { + Logger.error(ex, scope, `Failed to get prs for '${r.remote.url}'`); } + if (prs == null) continue; for (const pr of prs) { - if (pr.reasons.length === 0) { - continue; - } + if (pr.reasons.length === 0) continue; + const entry: SearchedPullRequestWithRemote = { ...pr, - repoAndRemote: richRepo, + repoAndRemote: r, isCurrentWorktree: false, isCurrentBranch: false, rank: getPrRank(pr), @@ -566,15 +578,32 @@ export class FocusWebviewProvider implements WebviewProvider { entry.pullRequest.refs!.head.branch }`; // TODO@eamodio really need to check for upstream url rather than name + let branches = branchesByRepo.get(entry.repoAndRemote.repo); + if (branches == null) { + branches = new PageableResult(paging => + entry.repoAndRemote.repo.getBranches(paging != null ? { paging: paging } : undefined), + ); + branchesByRepo.set(entry.repoAndRemote.repo, branches); + } + + let worktrees = worktreesByRepo.get(entry.repoAndRemote.repo); + if (worktrees == null) { + worktrees = await entry.repoAndRemote.repo.getWorktrees(); + worktreesByRepo.set(entry.repoAndRemote.repo, worktrees); + } + const worktree = await getWorktreeForBranch( entry.repoAndRemote.repo, entry.pullRequest.refs!.head.branch, remoteBranchName, + worktrees, + branches, ); + entry.hasWorktree = worktree != null; entry.isCurrentWorktree = worktree?.opened === true; - const branch = await getLocalBranchByUpstream(richRepo.repo, remoteBranchName); + const branch = await getLocalBranchByUpstream(r.repo, remoteBranchName, branches); if (branch) { entry.branch = branch; entry.hasLocalBranch = true; @@ -601,22 +630,27 @@ export class FocusWebviewProvider implements WebviewProvider { @debug({ args: { 0: false } }) private async getMyIssues(richRepos: RepoWithRichRemote[], force?: boolean): Promise { + const scope = getLogScope(); + if (force || this._pullRequests == null) { const allIssues = []; - for (const richRepo of richRepos) { - const remote = richRepo.remote; - const issues = await this.container.git.getMyIssues(remote); - if (issues == null) { - continue; + + const queries = richRepos.map(r => [r, this.container.git.getMyIssues(r.remote)] as const); + for (const [r, query] of queries) { + let issues; + try { + issues = await query; + } catch (ex) { + Logger.error(ex, scope, `Failed to get issues for '${r.remote.url}'`); } + if (issues == null) continue; for (const issue of issues) { - if (issue.reasons.length === 0) { - continue; - } + if (issue.reasons.length === 0) continue; + allIssues.push({ ...issue, - repoAndRemote: richRepo, + repoAndRemote: r, rank: 0, // getIssueRank(issue), }); } diff --git a/src/system/paging.ts b/src/system/paging.ts new file mode 100644 index 0000000..c7a448c --- /dev/null +++ b/src/system/paging.ts @@ -0,0 +1,31 @@ +import type { PagedResult } from '../git/gitProvider'; + +export class PageableResult { + private cached: Mutable> | undefined; + + constructor(private readonly fetch: (paging: PagedResult['paging']) => Promise>) {} + + async *values(): AsyncIterable> { + if (this.cached != null) { + for (const value of this.cached.values) { + yield value; + } + } + + let results = this.cached; + while (results == null || results.paging?.more) { + results = await this.fetch(results?.paging); + + if (this.cached == null) { + this.cached = results; + } else { + this.cached.values.push(...results.values); + this.cached.paging = results.paging; + } + + for (const value of results.values) { + yield value; + } + } + } +}