Skip to content

Commit 8ec8bb7

Browse files
Adds autolinked issues to branches in home view
- Fixes getEnrichedAutolinks failing to get issue for branch type autolinks - Adds getEnrichedAutolinks to branch model - Adds autolinks to home webview state and renders in current and recent branch sections - Adds issue-icon component - Moves link on PR and issue lines from the id to the title, to avoid issues with hover actions on top
1 parent 279a32b commit 8ec8bb7

File tree

8 files changed

+184
-14
lines changed

8 files changed

+184
-14
lines changed

src/autolinks/autolinks.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,10 @@ export class Autolinks implements Disposable {
329329
integration != null &&
330330
link.provider?.id === integration.id &&
331331
link.provider?.domain === integration.domain
332-
? integration.getIssueOrPullRequest(link.descriptor ?? remote.provider.repoDesc, id)
332+
? integration.getIssueOrPullRequest(
333+
link.descriptor ?? remote.provider.repoDesc,
334+
link.referenceType === 'branch' ? link.id : id,
335+
)
333336
: link.descriptor != null
334337
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link))
335338
: undefined;

src/git/models/branch.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CancellationToken } from 'vscode';
2+
import type { EnrichedAutolink } from '../../autolinks';
23
import type { BranchSorting } from '../../config';
34
import type { GitConfigKeys } from '../../constants';
45
import type { Container } from '../../container';
@@ -140,6 +141,13 @@ export class GitBranch implements GitBranchReference {
140141
);
141142
}
142143

144+
@memoize()
145+
async getEnrichedAutolinks(): Promise<Map<string, EnrichedAutolink> | undefined> {
146+
const remote = await this.container.git.getBestRemoteWithProvider(this.repoPath);
147+
const branchAutolinks = await this.container.autolinks.getBranchAutolinks(this.name, remote);
148+
return this.container.autolinks.getEnrichedAutolinks(branchAutolinks, remote);
149+
}
150+
143151
@memoize()
144152
getBasename(): string {
145153
const name = this.getNameWithoutRemote();

src/webviews/apps/plus/home/components/active-work.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import '../../../shared/components/menu/menu-item';
2323
import '../../../shared/components/overlays/popover';
2424
import '../../../shared/components/overlays/tooltip';
2525
import '../../../shared/components/pills/tracking';
26+
import '../../../shared/components/rich/issue-icon';
2627
import '../../../shared/components/rich/pr-icon';
2728

2829
export const activeWorkTagName = 'gl-active-work';
@@ -177,7 +178,7 @@ export class GlActiveWork extends SignalWatcher(LitElement) {
177178
}
178179

179180
private renderRepoBranchCard(branch: GetOverviewBranch, repo: string, isFetching: boolean) {
180-
const { name, pr, state, workingTreeState, upstream } = branch;
181+
const { name, pr, autolinks, state, workingTreeState, upstream } = branch;
181182
return html`
182183
<gl-card class="branch-item" active>
183184
<div class="branch-item__container">
@@ -196,18 +197,38 @@ export class GlActiveWork extends SignalWatcher(LitElement) {
196197
<span class="branch-item__icon">
197198
<pr-icon state=${pr.state} pr-id=${pr.id}></pr-icon>
198199
</span>
199-
<span class="branch-item__name">${pr.title}</span>
200-
<a href=${pr.url} class="branch-item__identifier">#${pr.id}</a>
200+
<a href=${pr.url} class="branch-item__name">${pr.title}</a>
201+
<spanclass="branch-item__identifier">#${pr.id}</span>
201202
</p>
202203
</div>`;
203204
})}
205+
${when(autolinks, () => this.renderAutolinks(autolinks))}
204206
${when(workingTreeState, () => this.renderStatus(workingTreeState, state))}
205207
</div>
206208
${this.renderActions(branch, repo)}
207209
</gl-card>
208210
`;
209211
}
210212

213+
private renderAutolinks(autolinks: { id: string; title: string; state: string; url: string }[] | undefined) {
214+
if (!autolinks) return nothing;
215+
return html`
216+
<div class="branch-item__section">
217+
${autolinks.map(autolink => {
218+
return html`
219+
<p class="branch-item__grouping">
220+
<span class="branch-item__icon">
221+
<issue-icon state=${autolink.state} issue-id=${autolink.id}></issue-icon>
222+
</span>
223+
<a href=${autolink.url} class="branch-item__name">${autolink.title}</a>
224+
<span class="branch-item__identifier">#${autolink.id}</span>
225+
</p>
226+
`;
227+
})}
228+
</div>
229+
`;
230+
}
231+
211232
private renderActions(branch: GetOverviewBranch, repo: string) {
212233
const branchRefs = {
213234
repoPath: repo,

src/webviews/apps/plus/home/components/branch-section.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../../../shared/components/card/card';
1414
import '../../../shared/components/commit/commit-stats';
1515
import '../../../shared/components/formatted-date';
1616
import '../../../shared/components/pills/tracking';
17+
import '../../../shared/components/rich/issue-icon';
1718
import '../../../shared/components/rich/pr-icon';
1819
import '../../../shared/components/actions/action-item';
1920
import '../../../shared/components/actions/action-nav';
@@ -161,10 +162,6 @@ export const branchCardStyles = css`
161162
text-decoration: none;
162163
}
163164
164-
.branch-item__identifier:hover {
165-
text-decoration: underline;
166-
}
167-
168165
.branch-item__grouping {
169166
display: inline-flex;
170167
align-items: center;
@@ -223,7 +220,7 @@ export class GlBranchCard extends LitElement {
223220
}
224221

225222
override render() {
226-
const { name, pr, opened: active, timestamp: date } = this.branch;
223+
const { name, pr, autolinks, opened: active, timestamp: date } = this.branch;
227224
return html`
228225
<gl-card class="branch-item" .active=${active}>
229226
<div class="branch-item__container">
@@ -233,12 +230,12 @@ export class GlBranchCard extends LitElement {
233230
${when(
234231
pr,
235232
pr =>
236-
html`<span class="branch-item__name">${pr.title} </span
237-
><a href=${pr.url} class="branch-item__identifier">#${pr.id}</a>`,
233+
html`<a href=${pr.url} class="branch-item__name">${pr.title} </a
234+
><span class="branch-item__identifier">#${pr.id}</span>`,
238235
() => html`<span class="branch-item__name">${name}</span>`,
239236
)}
240237
</p>
241-
${this.renderPrBranch(this.branch)}
238+
${this.renderPrBranch(this.branch)} ${when(autolinks, () => this.renderAutolinks(autolinks))}
242239
</div>
243240
<div class="branch-item__section branch-item__section--details">
244241
${this.renderChanges(this.branch)}
@@ -256,6 +253,23 @@ export class GlBranchCard extends LitElement {
256253
`;
257254
}
258255

256+
private renderAutolinks(autolinks: { id: string; title: string; state: string; url: string }[] | undefined) {
257+
if (!autolinks) return nothing;
258+
return html`
259+
${autolinks.map(autolink => {
260+
return html`
261+
<p class="branch-item__grouping branch-item__grouping--secondary">
262+
<span class="branch-item__icon">
263+
<issue-icon state=${autolink.state} issue-id=${autolink.id}></issue-icon>
264+
</span>
265+
<a href=${autolink.url} class="branch-item__name">${autolink.title}</a>
266+
<span class="branch-item__identifier">#${autolink.id}</span>
267+
</p>
268+
`;
269+
})}
270+
`;
271+
}
272+
259273
private renderPrBranch(branch: OverviewBranch) {
260274
if (!branch.pr) {
261275
return nothing;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { html, LitElement } from 'lit';
2+
import { customElement, property } from 'lit/decorators.js';
3+
import { ifDefined } from 'lit/directives/if-defined.js';
4+
import { issueIconStyles } from './issue.css';
5+
import '../code-icon';
6+
import '../overlays/tooltip';
7+
8+
@customElement('issue-icon')
9+
export class IssueIcon extends LitElement {
10+
static override styles = [issueIconStyles];
11+
12+
@property()
13+
state?: 'opened' | 'closed' | string;
14+
15+
@property({ attribute: 'issue-id' })
16+
issueId?: string;
17+
18+
get icon() {
19+
let issueIcon = 'issues';
20+
if (this.state) {
21+
switch (this.state) {
22+
case 'opened':
23+
issueIcon = 'issues';
24+
break;
25+
case 'closed':
26+
issueIcon = 'pass';
27+
break;
28+
}
29+
}
30+
return issueIcon;
31+
}
32+
33+
get classes() {
34+
if (!this.state) return 'issue-icon';
35+
36+
return `issue-icon issue-icon--${this.state}`;
37+
}
38+
39+
get label() {
40+
if (!this.state) return 'Issue';
41+
42+
return `Issue ${this.issueId ? `#${this.issueId}` : ''} is ${this.state}`;
43+
}
44+
45+
override render() {
46+
if (!this.state) {
47+
return html`<code-icon
48+
class=${this.classes}
49+
icon=${this.icon}
50+
aria-label=${ifDefined(this.state)}
51+
></code-icon>`;
52+
}
53+
54+
return html`<gl-tooltip>
55+
<code-icon class=${this.classes} icon=${this.icon} aria-label=${ifDefined(this.state)}></code-icon>
56+
<span slot="content">${this.label}</span>
57+
</gl-tooltip>`;
58+
}
59+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { css } from 'lit';
2+
3+
export const issueIconStyles = css`
4+
.issue-icon--opened {
5+
color: var(--vscode-gitlens-openAutolinkedIssueIconColor);
6+
}
7+
.issue-icon--closed {
8+
color: var(--vscode-gitlens-closedAutolinkedIssueIconColor);
9+
}
10+
`;

src/webviews/home/homeWebview.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ConfigurationChangeEvent } from 'vscode';
22
import { Disposable, workspace } from 'vscode';
33
import type { CreatePullRequestActionContext } from '../../api/gitlens';
4+
import type { EnrichedAutolink } from '../../autolinks';
45
import { getAvatarUriFromGravatarEmail } from '../../avatars';
56
import type { OpenPullRequestOnRemoteCommandArgs } from '../../commands/openPullRequestOnRemote';
67
import { GlyphChars, urls } from '../../constants';
@@ -915,6 +916,7 @@ async function getOverviewBranches(
915916

916917
let repoStatusPromise: Promise<GitStatus | undefined> | undefined;
917918
const prPromises = new Map<string, Promise<PullRequest | undefined>>();
919+
const autolinkPromises = new Map<string, Promise<Map<string, EnrichedAutolink> | undefined>>();
918920
const statusPromises = new Map<string, Promise<GitStatus | undefined>>();
919921
const contributorPromises = new Map<string, Promise<BranchContributorOverview | undefined>>();
920922

@@ -929,6 +931,7 @@ async function getOverviewBranches(
929931
if (branch.current || wt?.opened) {
930932
const forceOptions = options?.forceActive ? { force: true } : undefined;
931933
prPromises.set(branch.id, branch.getAssociatedPullRequest({ avatarSize: 16 }));
934+
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks());
932935
if (wt != null) {
933936
statusPromises.set(branch.id, wt.getStatus(forceOptions));
934937
} else {
@@ -955,6 +958,7 @@ async function getOverviewBranches(
955958

956959
if (timestamp != null && timestamp > recentThreshold) {
957960
prPromises.set(branch.id, branch.getAssociatedPullRequest());
961+
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks());
958962
if (wt != null) {
959963
statusPromises.set(branch.id, wt.getStatus());
960964
}
@@ -982,6 +986,7 @@ async function getOverviewBranches(
982986
orderBy: 'date:asc',
983987
});
984988
for (const branch of branches) {
989+
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks());
985990
if (overviewBranches.stale.length > 9) break;
986991

987992
if (
@@ -1025,7 +1030,7 @@ async function getOverviewBranches(
10251030
}
10261031
}
10271032

1028-
await enrichOverviewBranches(overviewBranches, prPromises, statusPromises, contributorPromises);
1033+
await enrichOverviewBranches(overviewBranches, prPromises, autolinkPromises, statusPromises, contributorPromises);
10291034

10301035
return overviewBranches;
10311036
}
@@ -1034,11 +1039,17 @@ async function getOverviewBranches(
10341039
async function enrichOverviewBranches(
10351040
overviewBranches: GetOverviewBranches,
10361041
prPromises: Map<string, Promise<PullRequest | undefined>>,
1042+
autolinkPromises: Map<string, Promise<Map<string, EnrichedAutolink> | undefined>>,
10371043
statusPromises: Map<string, Promise<GitStatus | undefined>>,
10381044
contributorPromises: Map<string, Promise<BranchContributorOverview | undefined>>,
10391045
) {
1040-
const [prResults, statusResults, contributorResults] = await Promise.allSettled([
1046+
const [prResults, autolinkResults, statusResults, contributorResults] = await Promise.allSettled([
10411047
Promise.allSettled(map(prPromises, ([id, pr]) => pr.then<[string, PullRequest | undefined]>(pr => [id, pr]))),
1048+
Promise.allSettled(
1049+
map(autolinkPromises, ([id, autolinks]) =>
1050+
autolinks.then<[string, Map<string, EnrichedAutolink> | undefined]>(a => [id, a]),
1051+
),
1052+
),
10421053
Promise.allSettled(
10431054
map(statusPromises, ([id, status]) => status.then<[string, GitStatus | undefined]>(status => [id, status])),
10441055
),
@@ -1065,6 +1076,40 @@ async function enrichOverviewBranches(
10651076
]),
10661077
);
10671078

1079+
const enrichedAutolinkPromises = new Map(
1080+
getSettledValue(autolinkResults)
1081+
?.filter(r => r.status === 'fulfilled')
1082+
.map(({ value: [autolinkId, autolinks] }) => [
1083+
autolinkId,
1084+
autolinks
1085+
? [...autolinks.values()]
1086+
.filter(autolink => autolink?.[0] != null)
1087+
.map(async autolink => {
1088+
const issue = await autolink[0]!;
1089+
return {
1090+
id: autolink[1].id,
1091+
title: issue?.title ?? autolink[1].title ?? `Issue #${autolink[1].id}`,
1092+
state: issue?.state === 'closed' ? 'closed' : 'opened',
1093+
url: autolink[1].url,
1094+
hasIssue: issue != null,
1095+
};
1096+
})
1097+
: undefined,
1098+
]),
1099+
);
1100+
1101+
const autolinks = new Map<string, GetOverviewBranch['autolinks']>();
1102+
1103+
for (const [id, enrichedAutolinkPromise] of enrichedAutolinkPromises.entries()) {
1104+
if (enrichedAutolinkPromise == null) continue;
1105+
const enrichedAutolinks = await Promise.all(enrichedAutolinkPromise);
1106+
if (!enrichedAutolinks.length) continue;
1107+
autolinks.set(
1108+
id,
1109+
enrichedAutolinks.filter(a => a.hasIssue),
1110+
);
1111+
}
1112+
10681113
const statuses = new Map(
10691114
getSettledValue(statusResults)
10701115
?.filter(r => r.status === 'fulfilled')
@@ -1081,6 +1126,9 @@ async function enrichOverviewBranches(
10811126
const pr = prs.get(branch.id);
10821127
branch.pr = pr;
10831128

1129+
const autolinksForBranch = autolinks.get(branch.id);
1130+
branch.autolinks = autolinksForBranch;
1131+
10841132
const status = statuses.get(branch.id);
10851133
if (status != null) {
10861134
branch.workingTreeState = status.getDiffStatus();

src/webviews/home/protocol.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ export interface GetOverviewBranch {
100100
state: string;
101101
url: string;
102102
};
103+
autolinks?: {
104+
id: string;
105+
title: string;
106+
url: string;
107+
state: string;
108+
hasIssue: boolean;
109+
}[];
103110
worktree?: {
104111
name: string;
105112
uri: string;

0 commit comments

Comments
 (0)