Skip to content

Commit

Permalink
Improves overview branch autolinks
Browse files Browse the repository at this point in the history
- skip redirected PR autolinks if the refset is non-prefixed
- filter autolinks by type=issue before render in the issues section
- add unlink feature
  • Loading branch information
nzaytsev committed Feb 3, 2025
1 parent 06f8cf2 commit c60eda3
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 29 deletions.
9 changes: 8 additions & 1 deletion src/autolinks/autolinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class Autolinks implements Disposable {
linkIntegration = undefined;
}
}
const issueOrPullRequestPromise =
let issueOrPullRequestPromise =
remote?.provider != null &&
integration != null &&
link.provider?.id === integration.id &&
Expand All @@ -235,6 +235,13 @@ export class Autolinks implements Disposable {
: link.descriptor != null
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link))
: undefined;
// we consider that all non-prefixed links are came from branch names and linked to issues
// skip if it's a PR link
if (!link.prefix) {
issueOrPullRequestPromise = issueOrPullRequestPromise?.then(x =>
x?.type === 'pullrequest' ? undefined : x,
);
}
enrichedAutolinks.set(id, [issueOrPullRequestPromise, link]);
}

Expand Down
1 change: 1 addition & 0 deletions src/constants.commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ export type TreeViewCommandSuffixesByViewType<T extends TreeViewTypes> = Extract
>;

type HomeWebviewCommands = `home.${
| 'unlinkIssue'
| 'openMergeTargetComparison'
| 'openPullRequestChanges'
| 'openPullRequestComparison'
Expand Down
4 changes: 4 additions & 0 deletions src/constants.storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export type GlobalStorage = {
'graph:searchMode': StoredGraphSearchMode;
'views:scm:grouped:welcome:dismissed': boolean;
'integrations:configured': StoredIntegrationConfigurations;
'autolinks:branches:ignore': IgnoredBranchesAutolinks;
'autolinks:branches:ignore:skipPrompt': boolean | undefined;
} & { [key in `plus:preview:${FeaturePreviews}:usages`]: StoredFeaturePreviewUsagePeriod[] } & {
[key in `confirm:ai:tos:${AIProviders}`]: boolean;
} & {
Expand All @@ -95,6 +97,8 @@ export type GlobalStorage = {

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

export type IgnoredBranchesAutolinks = Record<string, string[] | undefined>;

export interface StoredConfiguredIntegrationDescriptor {
cloud: boolean;
integrationId: IntegrationId;
Expand Down
14 changes: 11 additions & 3 deletions src/git/models/branch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-restricted-imports */ /* TODO need to deal with sharing rich class shapes to webviews */
/* TODO need to deal with sharing rich class shapes to webviews */
import type { EnrichedAutolink } from '../../autolinks';
import type { Container } from '../../container';
import { formatDate, fromNow } from '../../system/date';
import { memoize } from '../../system/decorators/-webview/memoize';
import { debug } from '../../system/decorators/log';
import { getLoggableName } from '../../system/logger';
import type { MaybePausedResult } from '../../system/promise';
Expand Down Expand Up @@ -118,9 +117,18 @@ export class GitBranch implements GitBranchReference {
}

@memoize()
async getEnrichedAutolinks(): Promise<Map<string, EnrichedAutolink> | undefined> {
async getEnrichedAutolinks(ignoredLinks?: string[]): Promise<Map<string, EnrichedAutolink> | undefined> {
const remote = await this.container.git.remotes(this.repoPath).getBestRemoteWithProvider();
const branchAutolinks = await this.container.autolinks.getBranchAutolinks(this.name, remote);
if (ignoredLinks?.length) {
const ignoredMap = Object.fromEntries(ignoredLinks.map(x => [x, true]));
forEach(branchAutolinks, ([key, link]) => {
if (ignoredMap[link.url]) {
console.log('ignored', link.url, branchAutolinks);
branchAutolinks.delete(key);
}
});
}
return this.container.autolinks.getEnrichedAutolinks(branchAutolinks, remote);
}

Expand Down
61 changes: 54 additions & 7 deletions src/webviews/apps/plus/home/components/branch-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { AssociateIssueWithBranchCommandArgs } from '../../../../../plus/st
import { createCommandLink } from '../../../../../system/commands';
import { fromNow } from '../../../../../system/date';
import { interpolate, pluralize } from '../../../../../system/string';
import type { BranchRef, GetOverviewBranch, OpenInGraphParams } from '../../../../home/protocol';
import type { BranchRef, GetOverviewBranch, OpenInGraphParams, OverviewBranchIssue } from '../../../../home/protocol';
import { renderBranchName } from '../../../shared/components/branch-name';
import type { GlCard } from '../../../shared/components/card/card';
import { GlElement, observe } from '../../../shared/components/element';
Expand Down Expand Up @@ -58,6 +58,25 @@ export const branchCardStyles = css`
flex-direction: column;
gap: 0.4rem;
}
.branch-item__unplug {
padding: 0.2em;
margin-block: -0.2em;
opacity: 0;
border-radius: 3px;
}
.branch-item__section:hover .branch-item__unplug,
.branch-item__section:focus-within .branch-item__unplug {
opacity: 1;
}
.branch-item__unplug:hover,
.branch-item__unplug:focus {
background-color: var(--vscode-toolbar-hoverBackground);
outline: 1px dashed var(--vscode-toolbar-hoverOutline);
}
.branch-item__section > * {
margin-block: 0;
}
Expand Down Expand Up @@ -499,19 +518,47 @@ export abstract class GlBranchCardBase extends GlElement {
this.toggleExpanded(true);
}

protected renderIssues(): TemplateResult | NothingType {
private getIssues(): OverviewBranchIssue[] {
const { autolinks, issues } = this;
const issuesSource = issues?.length ? issues : autolinks;
if (!issuesSource?.length) return nothing;
const issuesMap: Record<string, OverviewBranchIssue> = {};
autolinks?.map(autolink => {
if (autolink.type !== 'issue') {
return;
}
issuesMap[autolink.url] = autolink;
});
issues?.map(issue => {
issuesMap[issue.url] = issue;
});
return Object.values(issuesMap);
}

protected renderIssues(issues: OverviewBranchIssue[]) {
if (!issues.length) return nothing;
return html`
${issuesSource.map(issue => {
${issues.map(issue => {
return html`
<p class="branch-item__grouping">
<span class="branch-item__icon">
<issue-icon state=${issue.state} issue-id=${issue.id}></issue-icon>
</span>
<a href=${issue.url} class="branch-item__name branch-item__name--secondary">${issue.title}</a>
${when(
issue.isAutolink && this.expanded,
() => html`
<gl-tooltip>
<a
class="branch-item__unplug"
href=${createCommandLink('gitlens.home.unlinkIssue', {
issue: issue,
reference: this.branch.reference,
})}
><code-icon icon="gl-unplug"></code-icon
></a>
<div slot="content">Unlink automatically linked issue</div>
</gl-tooltip>
`,
)}
<span class="branch-item__identifier">#${issue.id}</span>
</p>
`;
Expand Down Expand Up @@ -791,7 +838,7 @@ export abstract class GlBranchCardBase extends GlElement {
}

protected renderIssuesItem(): TemplateResult | NothingType {
const issues = [...(this.issues ?? []), ...(this.autolinks ?? [])];
const issues = this.getIssues();
if (!issues.length) {
if (!this.expanded) return nothing;

Expand Down Expand Up @@ -821,7 +868,7 @@ export abstract class GlBranchCardBase extends GlElement {

return html`
<gl-work-item ?expanded=${this.expanded} ?nested=${!this.branch.opened} .indicator=${indicator}>
<div class="branch-item__section">${this.renderIssues()}</div>
<div class="branch-item__section">${this.renderIssues(issues)}</div>
</gl-work-item>
`;
}
Expand Down
37 changes: 36 additions & 1 deletion src/webviews/home/homeWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { GitFileChangeShape } from '../../git/models/fileChange';
import type { Issue } from '../../git/models/issue';
import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus';
import type { PullRequest } from '../../git/models/pullRequest';
import type { GitBranchReference } from '../../git/models/reference';
import { RemoteResourceType } from '../../git/models/remoteResource';
import type { Repository, RepositoryFileSystemChangeEvent } from '../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../git/models/repository';
Expand Down Expand Up @@ -75,6 +76,7 @@ import type {
GetOverviewBranch,
IntegrationState,
OpenInGraphParams,
OverviewBranchIssue,
OverviewFilters,
OverviewRecentThreshold,
OverviewStaleThreshold,
Expand Down Expand Up @@ -332,6 +334,7 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
registerCommand('gitlens.home.continuePausedOperation', this.continuePausedOperation, this),
registerCommand('gitlens.home.abortPausedOperation', this.abortPausedOperation, this),
registerCommand('gitlens.home.openRebaseEditor', this.openRebaseEditor, this),
registerCommand('gitlens.home.unlinkIssue', this.unlinkIssue, this),
];
}

Expand Down Expand Up @@ -545,6 +548,35 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
});
}

private async unlinkIssue({ issue, reference }: { reference: GitBranchReference; issue: OverviewBranchIssue }) {
const skipPrompt = this.container.storage.get('autolinks:branches:ignore:skipPrompt') || undefined;
const item =
skipPrompt ??
(await window.showWarningMessage(
`This action will unlink the issue ${issue.url} from the branch ${reference.name} forever`,
{
modal: true,
},
`OK`,
`OK, Don't ask again`,
));
if (!item) {
return;
}
if (item === `OK, Don't ask again`) {
void this.container.storage.store('autolinks:branches:ignore:skipPrompt', true);
}
const prev = this.container.storage.get('autolinks:branches:ignore') ?? {};
const refId = reference.id ?? `${reference.repoPath}/${reference.remote}/${reference.ref}`;
await this.container.storage
.store('autolinks:branches:ignore', {
...prev,
[refId]: [...(prev[refId] ?? []), issue.url],
})
.catch();
void this.host.notify(DidChangeRepositoryWip, undefined);
}

private async createCloudPatch(ref: BranchRef) {
const status = await this.container.git.status(ref.repoPath).getStatus();
if (status == null) return;
Expand Down Expand Up @@ -1357,11 +1389,12 @@ function getOverviewBranchesCore(
for (const branch of branches) {
const wt = worktreesByBranch.get(branch.id);

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

if (isPro === true) {
prPromises.set(branch.id, getPullRequestInfo(container, branch, launchpadPromise));
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks());
autolinkPromises.set(branch.id, branch.getEnrichedAutolinks(ignored));
issuePromises.set(
branch.id,
getAssociatedIssuesForBranch(container, branch).then(issues => issues.value),
Expand Down Expand Up @@ -1472,6 +1505,8 @@ async function getAutolinkIssuesInfo(links: Map<string, EnrichedAutolink> | unde
title: issue.title,
url: issue.url,
state: issue.state,
type: issue.type,
isAutolink: true,
};
}),
);
Expand Down
20 changes: 3 additions & 17 deletions src/webviews/home/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IntegrationDescriptor } from '../../constants.integrations';
import type { GitBranchMergedStatus } from '../../git/gitProvider';
import type { GitBranchStatus, GitTrackingState } from '../../git/models/branch';
import type { Issue } from '../../git/models/issue';
import type { Issue, IssueOrPullRequestType } from '../../git/models/issue';
import type { MergeConflict } from '../../git/models/mergeConflict';
import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus';
import type { GitBranchReference } from '../../git/models/reference';
Expand Down Expand Up @@ -161,23 +161,9 @@ export interface GetOverviewBranch {
| undefined
>;

autolinks?: Promise<
{
id: string;
title: string;
url: string;
state: Omit<Issue['state'], 'merged'>;
}[]
>;
autolinks?: Promise<(OverviewBranchIssue & { type: IssueOrPullRequestType })[]>;

issues?: Promise<
{
id: string;
title: string;
url: string;
state: Omit<Issue['state'], 'merged'>;
}[]
>;
issues?: Promise<OverviewBranchIssue[]>;

worktree?: {
name: string;
Expand Down

0 comments on commit c60eda3

Please sign in to comment.