Skip to content

Commit 9419199

Browse files
committed
Improves overview branch autolinks
- skip redirected PR autolinks if the refset is non-prefixed - filter autolinks by type=issue before render in the issues section - add unlink feature
1 parent 06f8cf2 commit 9419199

File tree

7 files changed

+124
-26
lines changed

7 files changed

+124
-26
lines changed

src/autolinks/autolinks.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export class Autolinks implements Disposable {
223223
linkIntegration = undefined;
224224
}
225225
}
226-
const issueOrPullRequestPromise =
226+
let issueOrPullRequestPromise =
227227
remote?.provider != null &&
228228
integration != null &&
229229
link.provider?.id === integration.id &&
@@ -235,6 +235,13 @@ export class Autolinks implements Disposable {
235235
: link.descriptor != null
236236
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link))
237237
: undefined;
238+
// we consider that all non-prefixed links are came from branch names and linked to issues
239+
// skip if it's a PR link
240+
if (!link.prefix) {
241+
issueOrPullRequestPromise = issueOrPullRequestPromise?.then(x =>
242+
x?.type === 'pullrequest' ? undefined : x,
243+
);
244+
}
238245
enrichedAutolinks.set(id, [issueOrPullRequestPromise, link]);
239246
}
240247

src/constants.commands.ts

+1
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,7 @@ export type TreeViewCommandSuffixesByViewType<T extends TreeViewTypes> = Extract
688688
>;
689689

690690
type HomeWebviewCommands = `home.${
691+
| 'unlinkIssue'
691692
| 'openMergeTargetComparison'
692693
| 'openPullRequestChanges'
693694
| 'openPullRequestComparison'

src/constants.storage.ts

+4
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export type GlobalStorage = {
7979
'graph:searchMode': StoredGraphSearchMode;
8080
'views:scm:grouped:welcome:dismissed': boolean;
8181
'integrations:configured': StoredIntegrationConfigurations;
82+
'autolinks:branches:ignore': IgnoredBranchesAutolinks;
83+
'autolinks:branches:ignore:skipPrompt': boolean | undefined;
8284
} & { [key in `plus:preview:${FeaturePreviews}:usages`]: StoredFeaturePreviewUsagePeriod[] } & {
8385
[key in `confirm:ai:tos:${AIProviders}`]: boolean;
8486
} & {
@@ -95,6 +97,8 @@ export type GlobalStorage = {
9597

9698
export type StoredIntegrationConfigurations = Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>;
9799

100+
export type IgnoredBranchesAutolinks = Record<string, string[] | undefined>;
101+
98102
export interface StoredConfiguredIntegrationDescriptor {
99103
cloud: boolean;
100104
integrationId: IntegrationId;

src/git/models/branch.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Container } from '../../container';
44
import { formatDate, fromNow } from '../../system/date';
55
import { memoize } from '../../system/decorators/-webview/memoize';
66
import { debug } from '../../system/decorators/log';
7+
import { forEach } from '../../system/iterable';
78
import { getLoggableName } from '../../system/logger';
89
import type { MaybePausedResult } from '../../system/promise';
910
import {
@@ -118,9 +119,17 @@ export class GitBranch implements GitBranchReference {
118119
}
119120

120121
@memoize()
121-
async getEnrichedAutolinks(): Promise<Map<string, EnrichedAutolink> | undefined> {
122+
async getEnrichedAutolinks(ignoredLinks?: string[]): Promise<Map<string, EnrichedAutolink> | undefined> {
122123
const remote = await this.container.git.remotes(this.repoPath).getBestRemoteWithProvider();
123124
const branchAutolinks = await this.container.autolinks.getBranchAutolinks(this.name, remote);
125+
if (ignoredLinks?.length) {
126+
const ignoredMap = Object.fromEntries(ignoredLinks.map(x => [x, true]));
127+
forEach(branchAutolinks, ([key, link]) => {
128+
if (ignoredMap[link.url]) {
129+
branchAutolinks.delete(key);
130+
}
131+
});
132+
}
124133
return this.container.autolinks.getEnrichedAutolinks(branchAutolinks, remote);
125134
}
126135

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

+54-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { AssociateIssueWithBranchCommandArgs } from '../../../../../plus/st
1515
import { createCommandLink } from '../../../../../system/commands';
1616
import { fromNow } from '../../../../../system/date';
1717
import { interpolate, pluralize } from '../../../../../system/string';
18-
import type { BranchRef, GetOverviewBranch, OpenInGraphParams } from '../../../../home/protocol';
18+
import type { BranchIssueLink, BranchRef, GetOverviewBranch, OpenInGraphParams } from '../../../../home/protocol';
1919
import { renderBranchName } from '../../../shared/components/branch-name';
2020
import type { GlCard } from '../../../shared/components/card/card';
2121
import { GlElement, observe } from '../../../shared/components/element';
@@ -58,6 +58,25 @@ export const branchCardStyles = css`
5858
flex-direction: column;
5959
gap: 0.4rem;
6060
}
61+
62+
.branch-item__unplug {
63+
padding: 0.2em;
64+
margin-block: -0.2em;
65+
opacity: 0;
66+
border-radius: 3px;
67+
}
68+
69+
.branch-item__section:hover .branch-item__unplug,
70+
.branch-item__section:focus-within .branch-item__unplug {
71+
opacity: 1;
72+
}
73+
74+
.branch-item__unplug:hover,
75+
.branch-item__unplug:focus {
76+
background-color: var(--vscode-toolbar-hoverBackground);
77+
outline: 1px dashed var(--vscode-toolbar-hoverOutline);
78+
}
79+
6180
.branch-item__section > * {
6281
margin-block: 0;
6382
}
@@ -499,19 +518,47 @@ export abstract class GlBranchCardBase extends GlElement {
499518
this.toggleExpanded(true);
500519
}
501520

502-
protected renderIssues(): TemplateResult | NothingType {
521+
private getIssues(): BranchIssueLink[] {
503522
const { autolinks, issues } = this;
504-
const issuesSource = issues?.length ? issues : autolinks;
505-
if (!issuesSource?.length) return nothing;
523+
const issuesMap: Record<string, BranchIssueLink> = {};
524+
autolinks?.map(autolink => {
525+
if (autolink.type !== 'issue') {
526+
return;
527+
}
528+
issuesMap[autolink.url] = autolink;
529+
});
530+
issues?.map(issue => {
531+
issuesMap[issue.url] = issue;
532+
});
533+
return Object.values(issuesMap);
534+
}
506535

536+
protected renderIssues(issues: BranchIssueLink[]) {
537+
if (!issues.length) return nothing;
507538
return html`
508-
${issuesSource.map(issue => {
539+
${issues.map(issue => {
509540
return html`
510541
<p class="branch-item__grouping">
511542
<span class="branch-item__icon">
512543
<issue-icon state=${issue.state} issue-id=${issue.id}></issue-icon>
513544
</span>
514545
<a href=${issue.url} class="branch-item__name branch-item__name--secondary">${issue.title}</a>
546+
${when(
547+
issue.isAutolink && this.expanded,
548+
() => html`
549+
<gl-tooltip>
550+
<a
551+
class="branch-item__unplug"
552+
href=${createCommandLink('gitlens.home.unlinkIssue', {
553+
issue: issue,
554+
reference: this.branch.reference,
555+
})}
556+
><code-icon icon="gl-unplug"></code-icon
557+
></a>
558+
<div slot="content">Unlink automatically linked issue</div>
559+
</gl-tooltip>
560+
`,
561+
)}
515562
<span class="branch-item__identifier">#${issue.id}</span>
516563
</p>
517564
`;
@@ -791,7 +838,7 @@ export abstract class GlBranchCardBase extends GlElement {
791838
}
792839

793840
protected renderIssuesItem(): TemplateResult | NothingType {
794-
const issues = [...(this.issues ?? []), ...(this.autolinks ?? [])];
841+
const issues = this.getIssues();
795842
if (!issues.length) {
796843
if (!this.expanded) return nothing;
797844

@@ -821,7 +868,7 @@ export abstract class GlBranchCardBase extends GlElement {
821868

822869
return html`
823870
<gl-work-item ?expanded=${this.expanded} ?nested=${!this.branch.opened} .indicator=${indicator}>
824-
<div class="branch-item__section">${this.renderIssues()}</div>
871+
<div class="branch-item__section">${this.renderIssues(issues)}</div>
825872
</gl-work-item>
826873
`;
827874
}

src/webviews/home/homeWebview.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { GitFileChangeShape } from '../../git/models/fileChange';
2424
import type { Issue } from '../../git/models/issue';
2525
import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus';
2626
import type { PullRequest } from '../../git/models/pullRequest';
27+
import type { GitBranchReference } from '../../git/models/reference';
2728
import { RemoteResourceType } from '../../git/models/remoteResource';
2829
import type { Repository, RepositoryFileSystemChangeEvent } from '../../git/models/repository';
2930
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../git/models/repository';
@@ -67,6 +68,7 @@ import type { IpcMessage } from '../protocol';
6768
import type { WebviewHost, WebviewProvider, WebviewShowingArgs } from '../webviewProvider';
6869
import type { WebviewShowOptions } from '../webviewsController';
6970
import type {
71+
BranchIssueLink,
7072
BranchRef,
7173
CollapseSectionParams,
7274
DidChangeRepositoriesParams,
@@ -332,6 +334,7 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
332334
registerCommand('gitlens.home.continuePausedOperation', this.continuePausedOperation, this),
333335
registerCommand('gitlens.home.abortPausedOperation', this.abortPausedOperation, this),
334336
registerCommand('gitlens.home.openRebaseEditor', this.openRebaseEditor, this),
337+
registerCommand('gitlens.home.unlinkIssue', this.unlinkIssue, this),
335338
];
336339
}
337340

@@ -545,6 +548,35 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
545548
});
546549
}
547550

551+
private async unlinkIssue({ issue, reference }: { reference: GitBranchReference; issue: BranchIssueLink }) {
552+
const skipPrompt = this.container.storage.get('autolinks:branches:ignore:skipPrompt') || undefined;
553+
const item =
554+
skipPrompt ??
555+
(await window.showWarningMessage(
556+
`This action will unlink the issue ${issue.url} from the branch ${reference.name} forever`,
557+
{
558+
modal: true,
559+
},
560+
`OK`,
561+
`OK, Don't ask again`,
562+
));
563+
if (!item) {
564+
return;
565+
}
566+
if (item === `OK, Don't ask again`) {
567+
void this.container.storage.store('autolinks:branches:ignore:skipPrompt', true);
568+
}
569+
const prev = this.container.storage.get('autolinks:branches:ignore') ?? {};
570+
const refId = reference.id ?? `${reference.repoPath}/${reference.remote}/${reference.ref}`;
571+
await this.container.storage
572+
.store('autolinks:branches:ignore', {
573+
...prev,
574+
[refId]: [...(prev[refId] ?? []), issue.url],
575+
})
576+
.catch();
577+
void this.host.notify(DidChangeRepositoryWip, undefined);
578+
}
579+
548580
private async createCloudPatch(ref: BranchRef) {
549581
const status = await this.container.git.status(ref.repoPath).getStatus();
550582
if (status == null) return;
@@ -1357,11 +1389,12 @@ function getOverviewBranchesCore(
13571389
for (const branch of branches) {
13581390
const wt = worktreesByBranch.get(branch.id);
13591391

1392+
const ignored = container.storage.get('autolinks:branches:ignore')?.[branch.id];
13601393
const timestamp = branch.date?.getTime();
13611394

13621395
if (isPro === true) {
13631396
prPromises.set(branch.id, getPullRequestInfo(container, branch, launchpadPromise));
1364-
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks());
1397+
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks(ignored));
13651398
issuePromises.set(
13661399
branch.id,
13671400
getAssociatedIssuesForBranch(container, branch).then(issues => issues.value),
@@ -1472,6 +1505,8 @@ async function getAutolinkIssuesInfo(links: Map<string, EnrichedAutolink> | unde
14721505
title: issue.title,
14731506
url: issue.url,
14741507
state: issue.state,
1508+
type: issue.type,
1509+
isAutolink: true,
14751510
};
14761511
}),
14771512
);

src/webviews/home/protocol.ts

+11-16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IntegrationDescriptor } from '../../constants.integrations';
22
import type { GitBranchMergedStatus } from '../../git/gitProvider';
33
import type { GitBranchStatus, GitTrackingState } from '../../git/models/branch';
44
import type { Issue } from '../../git/models/issue';
5+
import type { IssueOrPullRequestType } from '../../git/models/issueOrPullRequest';
56
import type { MergeConflict } from '../../git/models/mergeConflict';
67
import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus';
78
import type { GitBranchReference } from '../../git/models/reference';
@@ -62,6 +63,14 @@ export const GetLaunchpadSummary = new IpcRequest<GetLaunchpadSummaryRequest, Ge
6263
'launchpad/summary',
6364
);
6465

66+
export interface BranchIssueLink {
67+
id: string;
68+
title: string;
69+
url: string;
70+
state: Omit<Issue['state'], 'merged'>;
71+
isAutolink?: boolean;
72+
}
73+
6574
export interface GetOverviewBranch {
6675
reference: GitBranchReference;
6776

@@ -161,23 +170,9 @@ export interface GetOverviewBranch {
161170
| undefined
162171
>;
163172

164-
autolinks?: Promise<
165-
{
166-
id: string;
167-
title: string;
168-
url: string;
169-
state: Omit<Issue['state'], 'merged'>;
170-
}[]
171-
>;
173+
autolinks?: Promise<(BranchIssueLink & { type: IssueOrPullRequestType })[]>;
172174

173-
issues?: Promise<
174-
{
175-
id: string;
176-
title: string;
177-
url: string;
178-
state: Omit<Issue['state'], 'merged'>;
179-
}[]
180-
>;
175+
issues?: Promise<BranchIssueLink[]>;
181176

182177
worktree?: {
183178
name: string;

0 commit comments

Comments
 (0)