diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 38e1b42..60ceac2 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -14,8 +14,9 @@ import type { } from '../../../@types/vscode.git'; import { getCachedAvatarUri } from '../../../avatars'; import { configuration } from '../../../configuration'; -import { CoreGitConfiguration, GlyphChars, Schemes } from '../../../constants'; +import { ContextKeys, CoreGitConfiguration, GlyphChars, Schemes } from '../../../constants'; import type { Container } from '../../../container'; +import { getContext } from '../../../context'; import { emojify } from '../../../emojis'; import { Features } from '../../../features'; import { GitErrorHandling } from '../../../git/commandOptions'; @@ -61,6 +62,7 @@ import type { GitFile, GitFileStatus } from '../../../git/models/file'; import { GitFileChange } from '../../../git/models/file'; import type { GitGraph, + GitGraphRefMetadata, GitGraphRow, GitGraphRowContexts, GitGraphRowHead, @@ -1660,7 +1662,9 @@ export class LocalGitProvider implements GitProvider, Disposable { ); } + const hasConnectedRemotes = getContext(ContextKeys.HasConnectedRemotes); const avatars = new Map(); + const refMetadata = hasConnectedRemotes ? new Map() : undefined; const ids = new Set(); const reachableFromHEAD = new Set(); const skippedIds = new Set(); @@ -1706,7 +1710,7 @@ export class LocalGitProvider implements GitProvider, Disposable { if (cursorIndex === -1) { // If we didn't find any new commits, we must have them all so return that we have everything if (size === data.length) { - return { repoPath: repoPath, avatars: avatars, ids: ids, rows: [] }; + return { repoPath: repoPath, avatars: avatars, ids: ids, refMetadata: refMetadata, rows: [] }; } size = data.length; @@ -1732,7 +1736,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } } - if (!data) return { repoPath: repoPath, avatars: avatars, ids: ids, rows: [] }; + if (!data) return { repoPath: repoPath, avatars: avatars, ids: ids, refMetadata: refMetadata, rows: [] }; log = data; if (limit !== 0) { @@ -1812,6 +1816,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } } + branch = branchMap.get(tip); remoteName = getRemoteNameFromBranchName(tip); if (remoteName) { remote = remoteMap.get(remoteName); @@ -1820,6 +1825,7 @@ export class LocalGitProvider implements GitProvider, Disposable { if (branchName === 'HEAD') continue; refRemoteHeads.push({ + id: branch?.id ?? remote.id, name: branchName, owner: remote.name, url: remote.url, @@ -1845,7 +1851,6 @@ export class LocalGitProvider implements GitProvider, Disposable { } } - branch = branchMap.get(tip); refHeads.push({ name: tip, isCurrentHead: current, @@ -1963,6 +1968,7 @@ export class LocalGitProvider implements GitProvider, Disposable { avatars: avatars, ids: ids, skippedIds: skippedIds, + refMetadata: refMetadata, rows: rows, id: sha, diff --git a/src/git/models/graph.ts b/src/git/models/graph.ts index c7b365a..18f992b 100644 --- a/src/git/models/graph.ts +++ b/src/git/models/graph.ts @@ -1,9 +1,11 @@ -import type { GraphRow, Head, Remote, RowContexts, Tag } from '@gitkraken/gitkraken-components'; +import type { GraphRow, Head, HostingServiceType, RefMetadata, Remote, RowContexts, Tag } from '@gitkraken/gitkraken-components'; export type GitGraphRowHead = Head; export type GitGraphRowRemoteHead = Remote; export type GitGraphRowTag = Tag; export type GitGraphRowContexts = RowContexts; +export type GitGraphRefMetadata = RefMetadata; +export type GitGraphHostingServiceType = HostingServiceType; export const enum GitGraphRowType { Commit = 'commit-node', MergeCommit = 'merge-node', @@ -25,6 +27,8 @@ export interface GitGraph { readonly repoPath: string; /** A map of all avatar urls */ readonly avatars: Map; + /** A map of all ref metadata */ + readonly refMetadata: Map | undefined; /** A set of all "seen" commit ids */ readonly ids: Set; /** A set of all skipped commit ids -- typically for stash index/untracked commits */ diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index 34d1e43..09ff0af 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -14,7 +14,7 @@ import { encodeUtf8Hex } from '@env/hex'; import { configuration } from '../../configuration'; import { CharCode, ContextKeys, Schemes } from '../../constants'; import type { Container } from '../../container'; -import { setContext } from '../../context'; +import { getContext, setContext } from '../../context'; import { emojify } from '../../emojis'; import { AuthenticationError, @@ -48,6 +48,7 @@ import type { GitFile } from '../../git/models/file'; import { GitFileChange, GitFileIndexStatus } from '../../git/models/file'; import type { GitGraph, + GitGraphRefMetadata, GitGraphRow, GitGraphRowHead, GitGraphRowRemoteHead, @@ -56,6 +57,7 @@ import type { import { GitGraphRowType } from '../../git/models/graph'; import type { GitLog } from '../../git/models/log'; import type { GitMergeStatus } from '../../git/models/merge'; +import type { PullRequest } from '../../git/models/pullRequest'; import type { GitRebaseStatus } from '../../git/models/rebase'; import type { GitBranchReference, GitReference } from '../../git/models/reference'; import { GitRevision } from '../../git/models/reference'; @@ -1083,7 +1085,9 @@ export class GitHubGitProvider implements GitProvider, Disposable { this.getTags(repoPath), ]); + const hasConnectedRemotes = getContext(ContextKeys.HasConnectedRemotes); const avatars = new Map(); + const refMetadata = hasConnectedRemotes ? new Map() : undefined; const ids = new Set(); return this.getCommitsForGraphCore( @@ -1094,6 +1098,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { getSettledValue(remotesResult)?.[0], getSettledValue(tagsResult)?.values, avatars, + refMetadata, ids, options, ); @@ -1107,6 +1112,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { remote: GitRemote | undefined, tags: GitTag[] | undefined, avatars: Map, + refMetadata: Map | undefined, ids: Set, options?: { branch?: string; @@ -1119,6 +1125,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { return { repoPath: repoPath, avatars: avatars, + refMetadata: refMetadata, ids: ids, rows: [], }; @@ -1129,6 +1136,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { return { repoPath: repoPath, avatars: avatars, + refMetadata: refMetadata, ids: ids, rows: [], }; @@ -1154,6 +1162,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { ]; refRemoteHeads = [ { + id: remote.id, name: branch.name, owner: remote.name, url: remote.url, @@ -1213,6 +1222,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { return { repoPath: repoPath, avatars: avatars, + refMetadata: refMetadata, ids: ids, rows: rows, id: options?.ref, @@ -1232,6 +1242,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { remote, tags, avatars, + refMetadata, ids, options, ); diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index d9bf4d5..78fae1d 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -32,9 +32,10 @@ import type { Container } from '../../../container'; import { getContext, onDidChangeContext, setContext } from '../../../context'; import { PlusFeatures } from '../../../features'; import { GitSearchError } from '../../../git/errors'; +import type { GitBranch } from '../../../git/models/branch'; import type { GitCommit } from '../../../git/models/commit'; import { GitGraphRowType } from '../../../git/models/graph'; -import type { GitGraph } from '../../../git/models/graph'; +import type { GitGraph, GitGraphHostingServiceType } from '../../../git/models/graph'; import type { GitBranchReference, GitRevisionReference, @@ -76,6 +77,7 @@ import type { DismissBannerParams, EnsureRowParams, GetMissingAvatarsParams, + GetMissingRefMetadataParams, GetMoreRowsParams, GraphColumnConfig, GraphColumnName, @@ -96,6 +98,7 @@ import { DidChangeColumnsNotificationType, DidChangeGraphConfigurationNotificationType, DidChangeNotificationType, + DidChangeRefMetadataNotificationType, DidChangeRowsNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, @@ -105,6 +108,7 @@ import { DismissBannerCommandType, EnsureRowCommandType, GetMissingAvatarsCommandType, + GetMissingRefMetadataCommandType, GetMoreRowsCommandType, SearchCommandType, SearchOpenInViewCommandType, @@ -351,6 +355,9 @@ export class GraphWebview extends WebviewBase { case GetMissingAvatarsCommandType.method: onIpc(GetMissingAvatarsCommandType, e, params => this.onGetMissingAvatars(params)); break; + case GetMissingRefMetadataCommandType.method: + onIpc(GetMissingRefMetadataCommandType, e, params => this.onGetMissingRefMetadata(params)); + break; case GetMoreRowsCommandType.method: onIpc(GetMoreRowsCommandType, e, params => this.onGetMoreRows(params)); break; @@ -535,6 +542,52 @@ export class GraphWebview extends WebviewBase { } } + private async onGetMissingRefMetadata(e: GetMissingRefMetadataParams) { + if (this._graph == null) return; + const repoPath = this._graph.repoPath; + const hasConnectedRemotes = getContext(ContextKeys.HasConnectedRemotes); + if (!hasConnectedRemotes) return; + + async function getRefMetadata(this: GraphWebview, id: string, type: string) { + const newRefMetadata = { ...(this._graph!.refMetadata ? this._graph!.refMetadata.get(id) : undefined) }; + const foundBranchesResult = await this.container.git.getBranches(repoPath, { filter: b => b.id === id }); + const foundBranches = foundBranchesResult?.values; + const refBranch: GitBranch | undefined = foundBranches?.length ? foundBranches[0] : undefined; + switch (type) { + case 'pullRequests': + newRefMetadata.pullRequests = null; + if (refBranch != null) { + const pullRequest = await refBranch.getAssociatedPullRequest(); + if (pullRequest != null) { + const pullRequestMetadata = { + hostingServiceType: pullRequest.provider.name as GitGraphHostingServiceType, + id: Number.parseInt(pullRequest.id) || 0, + title: pullRequest.title + }; + newRefMetadata.pullRequests = [ pullRequestMetadata ]; + } + } + break; + default: + break; + } + this._graph!.refMetadata?.set(id, newRefMetadata); + } + + const promises: Promise[] = []; + + for (const [id, missingTypes] of Object.entries(e.missing)) { + for (const missingType of missingTypes) { + promises.push(getRefMetadata.call(this, id, missingType)); + } + } + + if (promises.length) { + await Promise.allSettled(promises); + this.updateRefMetadata(); + } + } + @gate() @debug() private async onGetMoreRows(e: GetMoreRowsParams, sendSelectedRows: boolean = false) { @@ -761,6 +814,33 @@ export class GraphWebview extends WebviewBase { }); } + private _notifyDidChangeRefMetadataDebounced: Deferrable | undefined = + undefined; + + @debug() + private updateRefMetadata(immediate: boolean = false) { + if (immediate) { + void this.notifyDidChangeRefMetadata(); + return; + } + + if (this._notifyDidChangeRefMetadataDebounced == null) { + this._notifyDidChangeRefMetadataDebounced = debounce(this.notifyDidChangeRefMetadata.bind(this), 100); + } + + void this._notifyDidChangeRefMetadataDebounced(); + } + + @debug() + private async notifyDidChangeRefMetadata() { + if (this._graph == null) return; + + const data = this._graph; + return this.notify(DidChangeRefMetadataNotificationType, { + refMetadata: data.refMetadata ? Object.fromEntries(data.refMetadata) : undefined, + }); + } + @debug() private async notifyDidChangeColumns() { if (!this.isReady || !this.visible) { @@ -797,6 +877,7 @@ export class GraphWebview extends WebviewBase { { rows: data.rows, avatars: Object.fromEntries(data.avatars), + refMetadata: data.refMetadata != undefined ? Object.fromEntries(data.refMetadata) : undefined, selectedRows: sendSelectedRows ? this._selectedRows : undefined, paging: { startingCursor: data.paging?.startingCursor, @@ -1088,6 +1169,7 @@ export class GraphWebview extends WebviewBase { subscription: access?.subscription.current, allowed: (access?.allowed ?? false) !== false, avatars: data != null ? Object.fromEntries(data.avatars) : undefined, + refMetadata: data?.refMetadata != undefined ? Object.fromEntries(data.refMetadata) : undefined, loading: deferRows, rows: data?.rows, paging: diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index 01694ba..2c969db 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -4,6 +4,7 @@ import type { GraphContexts, GraphRow, GraphZoneType, + RefMetadata, Remote, WorkDirStats, } from '@gitkraken/gitkraken-components'; @@ -18,6 +19,8 @@ import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol' export type GraphColumnsSettings = Record; export type GraphSelectedRows = Record; export type GraphAvatars = Record; +export type GraphRefMetadata = Record; +export type GraphMissingRefMetadata = Record; export interface State { repositories?: GraphRepository[]; @@ -28,6 +31,7 @@ export interface State { allowed: boolean; avatars?: GraphAvatars; loading?: boolean; + refMetadata?: GraphRefMetadata; rows?: GraphRow[]; paging?: GraphPaging; columns?: GraphColumnsSettings; @@ -117,6 +121,11 @@ export interface GetMissingAvatarsParams { } export const GetMissingAvatarsCommandType = new IpcCommandType('graph/avatars/get'); +export interface GetMissingRefMetadataParams { + missing: GraphMissingRefMetadata; +} +export const GetMissingRefMetadataCommandType = new IpcCommandType('graph/refMetadata/get'); + export interface GetMoreRowsParams { id?: string; } @@ -182,6 +191,13 @@ export const DidChangeAvatarsNotificationType = new IpcNotificationType( + 'graph/refMetadata/didChange', +); + export interface DidChangeColumnsParams { columns: GraphColumnsSettings | undefined; context?: string; @@ -195,6 +211,7 @@ export interface DidChangeRowsParams { rows: GraphRow[]; avatars: { [email: string]: string }; paging?: GraphPaging; + refMetadata: GraphRefMetadata | undefined; selectedRows?: GraphSelectedRows; } export const DidChangeRowsNotificationType = new IpcNotificationType('graph/rows/didChange'); diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 2842ef9..5929035 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -30,6 +30,7 @@ import { DidChangeAvatarsNotificationType, DidChangeColumnsNotificationType, DidChangeGraphConfigurationNotificationType, + DidChangeRefMetadataNotificationType, DidChangeRowsNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, @@ -52,6 +53,7 @@ export interface GraphWrapperProps { onSelectRepository?: (repository: GraphRepository) => void; onColumnChange?: (name: GraphColumnName, settings: GraphColumnConfig) => void; onMissingAvatars?: (emails: { [email: string]: string }) => void; + onMissingRefMetadata?: (missing: { [id: string]: string[] }) => void; onMoreRows?: (id?: string) => void; onSearch?: (search: SearchQuery | undefined, options?: { limit?: number }) => void; onSearchPromise?: ( @@ -132,6 +134,7 @@ export function GraphWrapper({ onColumnChange, onEnsureRowPromise, onMissingAvatars, + onMissingRefMetadata, onMoreRows, onSearch, onSearchPromise, @@ -148,6 +151,7 @@ export function GraphWrapper({ const [rows, setRows] = useState(state.rows ?? []); const [avatars, setAvatars] = useState(state.avatars); + const [refMetadata, setRefMetadata] = useState(state.refMetadata); const [repos, setRepos] = useState(state.repositories ?? []); const [repo, setRepo] = useState( repos.find(item => item.path === state.selectedRepository), @@ -205,6 +209,9 @@ export function GraphWrapper({ case DidChangeAvatarsNotificationType: setAvatars(state.avatars); break; + case DidChangeRefMetadataNotificationType: + setRefMetadata(state.refMetadata); + break; case DidChangeColumnsNotificationType: setColumns(state.columns); setContext(state.context); @@ -213,6 +220,7 @@ export function GraphWrapper({ setRows(state.rows ?? []); setSelectedRows(state.selectedRows); setAvatars(state.avatars); + setRefMetadata(state.refMetadata); setPagingHasMore(state.paging?.hasMore ?? false); setIsLoading(state.loading); break; @@ -248,6 +256,7 @@ export function GraphWrapper({ setSelectedRows(state.selectedRows); setContext(state.context); setAvatars(state.avatars ?? {}); + setRefMetadata(state.refMetadata ?? {}); setPagingHasMore(state.paging?.hasMore ?? false); setRepos(state.repositories ?? []); setRepo(repos.find(item => item.path === state.selectedRepository)); @@ -437,6 +446,10 @@ export function GraphWrapper({ onMissingAvatars?.(emails); }; + const handleMissingRefMetadata = (missing: { [id: string]: string[] }) => { + onMissingRefMetadata?.(missing); + }; + const handleToggleColumnSettings = (event: React.MouseEvent) => { const e = event.nativeEvent; const evt = new MouseEvent('contextmenu', { @@ -688,8 +701,10 @@ export function GraphWrapper({ onColumnResized={handleOnColumnResized} onSelectGraphRows={handleSelectGraphRows} onEmailsMissingAvatarUrls={handleMissingAvatars} + onRefsMissingMetadata={handleMissingRefMetadata} onShowMoreCommits={handleMoreCommits} platform={clientPlatform} + refMetadataById={refMetadata} shaLength={graphConfig?.idLength} themeOpacityFactor={styleProps?.themeOpacityFactor} useAuthorInitialsForAvatars={!graphConfig?.avatars} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index db836ee..9dbd0fa 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -18,6 +18,7 @@ import { DidChangeColumnsNotificationType, DidChangeGraphConfigurationNotificationType, DidChangeNotificationType, + DidChangeRefMetadataNotificationType, DidChangeRowsNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, @@ -27,6 +28,7 @@ import { DismissBannerCommandType, EnsureRowCommandType, GetMissingAvatarsCommandType, + GetMissingRefMetadataCommandType, GetMoreRowsCommandType, SearchCommandType, SearchOpenInViewCommandType, @@ -85,6 +87,7 @@ export class GraphApp extends App { 250, )} onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)} + onMissingRefMetadata={(...params) => this.onGetMissingRefMetadata(...params)} onMoreRows={(...params) => this.onGetMoreRows(...params)} onSearch={debounce((search, options) => this.onSearch(search, options), 250)} onSearchPromise={(...params) => this.onSearchPromise(...params)} @@ -141,6 +144,13 @@ export class GraphApp extends App { }); break; + case DidChangeRefMetadataNotificationType.method: + onIpc(DidChangeRefMetadataNotificationType, msg, (params, type) => { + this.state.refMetadata = params.refMetadata; + this.setState(this.state, type); + }); + break; + case DidChangeRowsNotificationType.method: onIpc(DidChangeRowsNotificationType, msg, (params, type) => { let rows; @@ -205,6 +215,7 @@ export class GraphApp extends App { } this.state.avatars = params.avatars; + this.state.refMetadata = params.refMetadata; this.state.rows = rows; this.state.paging = params.paging; if (params.selectedRows != null) { @@ -365,6 +376,10 @@ export class GraphApp extends App { this.sendCommand(GetMissingAvatarsCommandType, { emails: emails }); } + private onGetMissingRefMetadata(missing: { [id: string]: string[] }) { + this.sendCommand(GetMissingRefMetadataCommandType, { missing: missing }); + } + private onGetMoreRows(sha?: string) { return this.sendCommand(GetMoreRowsCommandType, { id: sha }); }