From 9419199e001635f9a3f1581704044cb7f8e7751b Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Mon, 3 Feb 2025 11:11:54 +0700 Subject: [PATCH] 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 --- src/autolinks/autolinks.ts | 9 ++- src/constants.commands.ts | 1 + src/constants.storage.ts | 4 ++ src/git/models/branch.ts | 11 +++- .../apps/plus/home/components/branch-card.ts | 61 ++++++++++++++++--- src/webviews/home/homeWebview.ts | 37 ++++++++++- src/webviews/home/protocol.ts | 27 ++++---- 7 files changed, 124 insertions(+), 26 deletions(-) diff --git a/src/autolinks/autolinks.ts b/src/autolinks/autolinks.ts index e7c234db5b286..5398871d2e48b 100644 --- a/src/autolinks/autolinks.ts +++ b/src/autolinks/autolinks.ts @@ -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 && @@ -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]); } diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 79bc6912724b5..f03ba6eab132e 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -688,6 +688,7 @@ export type TreeViewCommandSuffixesByViewType = Extract >; type HomeWebviewCommands = `home.${ + | 'unlinkIssue' | 'openMergeTargetComparison' | 'openPullRequestChanges' | 'openPullRequestComparison' diff --git a/src/constants.storage.ts b/src/constants.storage.ts index 49e1f3a60deaa..1742a0945dbde 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -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; } & { @@ -95,6 +97,8 @@ export type GlobalStorage = { export type StoredIntegrationConfigurations = Record; +export type IgnoredBranchesAutolinks = Record; + export interface StoredConfiguredIntegrationDescriptor { cloud: boolean; integrationId: IntegrationId; diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 448c70773c144..3821acb6cd95b 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -4,6 +4,7 @@ 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 { forEach } from '../../system/iterable'; import { getLoggableName } from '../../system/logger'; import type { MaybePausedResult } from '../../system/promise'; import { @@ -118,9 +119,17 @@ export class GitBranch implements GitBranchReference { } @memoize() - async getEnrichedAutolinks(): Promise | undefined> { + async getEnrichedAutolinks(ignoredLinks?: string[]): Promise | 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]) { + branchAutolinks.delete(key); + } + }); + } return this.container.autolinks.getEnrichedAutolinks(branchAutolinks, remote); } diff --git a/src/webviews/apps/plus/home/components/branch-card.ts b/src/webviews/apps/plus/home/components/branch-card.ts index 25719547fe40e..27af3bc4cab33 100644 --- a/src/webviews/apps/plus/home/components/branch-card.ts +++ b/src/webviews/apps/plus/home/components/branch-card.ts @@ -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 { BranchIssueLink, BranchRef, GetOverviewBranch, OpenInGraphParams } 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'; @@ -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; } @@ -499,19 +518,47 @@ export abstract class GlBranchCardBase extends GlElement { this.toggleExpanded(true); } - protected renderIssues(): TemplateResult | NothingType { + private getIssues(): BranchIssueLink[] { const { autolinks, issues } = this; - const issuesSource = issues?.length ? issues : autolinks; - if (!issuesSource?.length) return nothing; + const issuesMap: Record = {}; + 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: BranchIssueLink[]) { + if (!issues.length) return nothing; return html` - ${issuesSource.map(issue => { + ${issues.map(issue => { return html`

${issue.title} + ${when( + issue.isAutolink && this.expanded, + () => html` + + +

Unlink automatically linked issue
+ + `, + )} #${issue.id}

`; @@ -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; @@ -821,7 +868,7 @@ export abstract class GlBranchCardBase extends GlElement { return html` -
${this.renderIssues()}
+
${this.renderIssues(issues)}
`; } diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 3431fe6d5ec3b..43f9f51302f16 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -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'; @@ -67,6 +68,7 @@ import type { IpcMessage } from '../protocol'; import type { WebviewHost, WebviewProvider, WebviewShowingArgs } from '../webviewProvider'; import type { WebviewShowOptions } from '../webviewsController'; import type { + BranchIssueLink, BranchRef, CollapseSectionParams, DidChangeRepositoriesParams, @@ -332,6 +334,7 @@ export class HomeWebviewProvider implements WebviewProvider issues.value), @@ -1472,6 +1505,8 @@ async function getAutolinkIssuesInfo(links: Map | unde title: issue.title, url: issue.url, state: issue.state, + type: issue.type, + isAutolink: true, }; }), ); diff --git a/src/webviews/home/protocol.ts b/src/webviews/home/protocol.ts index d47c8fd091ef7..911dd6f1c60d3 100644 --- a/src/webviews/home/protocol.ts +++ b/src/webviews/home/protocol.ts @@ -2,6 +2,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 { IssueOrPullRequestType } from '../../git/models/issueOrPullRequest'; import type { MergeConflict } from '../../git/models/mergeConflict'; import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus'; import type { GitBranchReference } from '../../git/models/reference'; @@ -62,6 +63,14 @@ export const GetLaunchpadSummary = new IpcRequest; + isAutolink?: boolean; +} + export interface GetOverviewBranch { reference: GitBranchReference; @@ -161,23 +170,9 @@ export interface GetOverviewBranch { | undefined >; - autolinks?: Promise< - { - id: string; - title: string; - url: string; - state: Omit; - }[] - >; + autolinks?: Promise<(BranchIssueLink & { type: IssueOrPullRequestType })[]>; - issues?: Promise< - { - id: string; - title: string; - url: string; - state: Omit; - }[] - >; + issues?: Promise; worktree?: { name: string;