diff --git a/package.json b/package.json index 20c8dea..7e671c2 100644 --- a/package.json +++ b/package.json @@ -2243,6 +2243,13 @@ "scope": "window", "order": 25 }, + "gitlens.graph.showUpstreamStatus": { + "type": "boolean", + "default": false, + "markdownDescription": "Specifies whether to show a local branch's upstream status in the _Commit Graph_", + "scope": "window", + "order": 26 + }, "gitlens.graph.commitOrdering": { "type": "string", "default": "date", diff --git a/src/config.ts b/src/config.ts index c771f27..a9ff7a4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -393,6 +393,7 @@ export interface GraphConfig { showDetailsView: 'open' | 'selection' | false; showGhostRefsOnRowHover: boolean; showRemoteNames: boolean; + showUpstreamStatus: boolean; pageItemLimit: number; searchItemLimit: number; statusBar: { diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index fdf5506..52dd305 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -98,6 +98,7 @@ import type { GraphRefMetadata, GraphRepository, GraphSelectedRows, + GraphUpstreamMetadata, GraphWorkingTreeStats, SearchOpenInViewParams, SearchParams, @@ -545,7 +546,8 @@ export class GraphWebview extends WebviewBase { configuration.changed(e, 'graph.highlightRowsOnRefHover') || configuration.changed(e, 'graph.scrollRowPadding') || configuration.changed(e, 'graph.showGhostRefsOnRowHover') || - configuration.changed(e, 'graph.showRemoteNames') + configuration.changed(e, 'graph.showRemoteNames') || + configuration.changed(e, 'graph.showUpstreamStatus') ) { void this.notifyDidChangeConfiguration(); } @@ -718,58 +720,95 @@ export class GraphWebview extends WebviewBase { const repoPath = this._graph.repoPath; - async function getRefMetadata(this: GraphWebview, id: string, type: GraphMissingRefsMetadataType) { + async function getRefMetadata(this: GraphWebview, id: string, missingTypes: GraphMissingRefsMetadataType[]) { if (this._refsMetadata == null) { this._refsMetadata = new Map(); } + const branch = (await this.container.git.getBranches(repoPath, { filter: b => b.id === id }))?.values?.[0]; const metadata = { ...this._refsMetadata.get(id) }; - if (type !== 'pullRequests') { - (metadata as any)[type] = null; - this._refsMetadata.set(id, metadata); - return; - } - const branch = (await this.container.git.getBranches(repoPath, { filter: b => b.id === id && b.remote })) - ?.values?.[0]; - const pr = await branch?.getAssociatedPullRequest(); - if (pr == null) { - if (metadata.pullRequests === undefined || metadata.pullRequests?.length === 0) { - metadata.pullRequests = null; + if (branch == null) { + for (const type of missingTypes) { + (metadata as any)[type] = null; + this._refsMetadata.set(id, metadata); } - this._refsMetadata.set(id, metadata); + return; } - const prMetadata: GraphPullRequestMetadata = { - // TODO@eamodio: This is iffy, but works right now since `github` and `gitlab` are the only values possible currently - hostingServiceType: pr.provider.id as GraphHostingServiceType, - id: Number.parseInt(pr.id) || 0, - title: pr.title, - author: pr.author.name, - date: (pr.mergedDate ?? pr.closedDate ?? pr.date)?.getTime(), - state: pr.state, - url: pr.url, - context: serializeWebviewItemContext({ - webviewItem: 'gitlens:pullrequest', - webviewItemValue: { - type: 'pullrequest', - id: pr.id, + for (const type of missingTypes) { + if (type !== 'pullRequests' && type !== 'upstream') { + (metadata as any)[type] = null; + this._refsMetadata.set(id, metadata); + + continue; + } + + if (type === 'pullRequests') { + const pr = await branch?.getAssociatedPullRequest(); + + if (pr == null) { + if (metadata.pullRequests === undefined || metadata.pullRequests?.length === 0) { + metadata.pullRequests = null; + } + + this._refsMetadata.set(id, metadata); + continue; + } + + const prMetadata: GraphPullRequestMetadata = { + // TODO@eamodio: This is iffy, but works right now since `github` and `gitlab` are the only values possible currently + hostingServiceType: pr.provider.id as GraphHostingServiceType, + id: Number.parseInt(pr.id) || 0, + title: pr.title, + author: pr.author.name, + date: (pr.mergedDate ?? pr.closedDate ?? pr.date)?.getTime(), + state: pr.state, url: pr.url, - }, - }), - }; + context: serializeWebviewItemContext({ + webviewItem: 'gitlens:pullrequest', + webviewItemValue: { + type: 'pullrequest', + id: pr.id, + url: pr.url, + }, + }), + }; + + metadata.pullRequests = [prMetadata]; + + this._refsMetadata.set(id, metadata); + continue; + } + + if (type === 'upstream') { + const upstream = branch?.upstream; + + if (upstream == null || upstream == undefined || upstream.missing) { + metadata.upstream = null; + this._refsMetadata.set(id, metadata); + continue; + } - metadata.pullRequests = [prMetadata]; - this._refsMetadata.set(id, metadata); + const upstreamMetadata: GraphUpstreamMetadata = { + name: getBranchNameWithoutRemote(upstream.name), + owner: getRemoteNameFromBranchName(upstream.name), + ahead: branch.state.ahead, + behind: branch.state.behind, + }; + + metadata.upstream = upstreamMetadata; + + this._refsMetadata.set(id, metadata); + } + } } const promises: Promise[] = []; - for (const [id, missingTypes] of Object.entries(e.metadata)) { - for (const missingType of missingTypes) { - promises.push(getRefMetadata.call(this, id, missingType)); - } + for (const id of Object.keys(e.metadata)) { + promises.push(getRefMetadata.call(this, id, e.metadata[id])); } if (promises.length) { @@ -1521,6 +1560,7 @@ export class GraphWebview extends WebviewBase { scrollRowPadding: configuration.get('graph.scrollRowPadding'), showGhostRefsOnRowHover: configuration.get('graph.showGhostRefsOnRowHover'), showRemoteNamesOnRefs: configuration.get('graph.showRemoteNames'), + showUpstreamStatus: configuration.get('graph.showUpstreamStatus'), idLength: configuration.get('advanced.abbreviatedShaLength'), }; return config; diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index 6898b97..70d9fb4 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -16,6 +16,7 @@ import type { RefMetadataType, Remote, Tag, + UpstreamMetadata, WorkDirStats, } from '@gitkraken/gitkraken-components'; import type { DateStyle } from '../../../config'; @@ -33,6 +34,7 @@ export type GraphSelectedRows = Record; export type GraphAvatars = Record; export type GraphRefMetadata = RefMetadata | null; +export type GraphUpstreamMetadata = UpstreamMetadata | null; export type GraphRefsMetadata = Record; export type GraphHostingServiceType = HostingServiceType; export type GraphMissingRefsMetadataType = RefMetadataType; @@ -115,6 +117,7 @@ export interface GraphComponentConfig { scrollRowPadding?: number; showGhostRefsOnRowHover?: boolean; showRemoteNamesOnRefs?: boolean; + showUpstreamStatus?: boolean; idLength?: number; } diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 838a5d1..8a6b683 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -110,10 +110,19 @@ const createIconElements = (): { [key: string]: ReactElement } => { 'show', 'hide', ]; + + const miniIconList = [ + 'upstream-ahead', + 'upstream-behind', + ]; + const elementLibrary: { [key: string]: ReactElement } = {}; iconList.forEach(iconKey => { elementLibrary[iconKey] = createElement('span', { className: `graph-icon icon--${iconKey}` }); }); + miniIconList.forEach(iconKey => { + elementLibrary[iconKey] = createElement('span', { className: `graph-icon mini-icon icon--${iconKey}` }); + }); return elementLibrary; }; @@ -1035,6 +1044,7 @@ export function GraphWrapper({ platform={clientPlatform} refMetadataById={refsMetadata} shaLength={graphConfig?.idLength} + showUpstreamStatus={graphConfig?.showUpstreamStatus} themeOpacityFactor={styleProps?.themeOpacityFactor} useAuthorInitialsForAvatars={!graphConfig?.avatars} workDirStats={workingTreeStats} diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index 947e79e..d906d0c 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -372,6 +372,11 @@ button:not([disabled]), vertical-align: middle; line-height: 2rem; letter-spacing: normal; + + &.mini-icon { + font-size: 1rem; + line-height: 1.6rem; + } } .icon { @@ -474,6 +479,20 @@ button:not([disabled]), content: '\ea64'; } } + &--upstream-ahead { + &::before { + // codicon-arrow-up + font-family: codicon; + content: '\eaa1'; + } + } + &--upstream-behind { + &::before { + // codicon-arrow-down + font-family: codicon; + content: '\ea9a'; + } + } } .titlebar { diff --git a/src/webviews/apps/settings/partials/commit-graph.html b/src/webviews/apps/settings/partials/commit-graph.html index 6a649b1..03fb8da 100644 --- a/src/webviews/apps/settings/partials/commit-graph.html +++ b/src/webviews/apps/settings/partials/commit-graph.html @@ -160,6 +160,20 @@
+ + +
+
+ +
+