From 669de270140d0a22c39da47f004e9e68519c67e5 Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Mon, 20 Jan 2025 16:03:33 +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 | 12 +++- .../apps/plus/home/components/branch-card.ts | 61 ++++++++++++++++--- src/webviews/home/homeWebview.ts | 42 ++++++++++++- src/webviews/home/protocol.ts | 28 ++++----- 7 files changed, 128 insertions(+), 29 deletions(-) diff --git a/src/autolinks/autolinks.ts b/src/autolinks/autolinks.ts index 7aad6ac8a73ad..b7ef257df0e77 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 ec80afb20dc1c..d60fe80e680c6 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -685,6 +685,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 8ba966544b93f..b06a84f3cd3a3 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; } & { @@ -91,6 +93,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 6a13712da413d..108d1cd78a483 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -3,6 +3,7 @@ import type { Container } from '../../container'; import { formatDate, fromNow } from '../../system/date'; import { debug } from '../../system/decorators/log'; import { memoize } from '../../system/decorators/memoize'; +import { forEach } from '../../system/iterable'; import { getLoggableName } from '../../system/logger'; import { formatDetachedHeadName, @@ -127,9 +128,18 @@ 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]) { + console.log('ignored', link.url, branchAutolinks); + 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 28c2e68319eec..0ca8d19d95e27 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 { 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'; @@ -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() { + private getIssues() { 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: OverviewBranchIssue[]) { + 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() { - 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 efaa40a5f4c5c..4c5d6ee55b842 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -26,6 +26,7 @@ import type { Issue } from '../../git/models/issue'; import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus'; import type { PullRequest } from '../../git/models/pullRequest'; import { getComparisonRefsForPullRequest } from '../../git/models/pullRequest'; +import type { GitBranchReference } from '../../git/models/reference'; import { getReferenceFromBranch } from '../../git/models/reference.utils'; import { RemoteResourceType } from '../../git/models/remoteResource'; import type { Repository } from '../../git/models/repository'; @@ -68,6 +69,7 @@ import type { GetOverviewResponse, IntegrationState, OpenInGraphParams, + OverviewBranchIssue, OverviewFilters, OverviewRecentThreshold, OverviewStaleThreshold, @@ -312,6 +314,7 @@ export class HomeWebviewProvider implements WebviewProvider issues.value), @@ -1239,7 +1272,7 @@ function getOverviewBranches( if (timestamp != null && timestamp > recentThreshold) { if (options?.isPro !== false) { 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), @@ -1288,7 +1321,8 @@ function getOverviewBranches( } if (options?.isPro !== false) { - autolinkPromises.set(branch.id, branch.getEnrichedAutolinks()); + const ignored = container.storage.get('autolinks:branches:ignore')?.[branch.id]; + autolinkPromises.set(branch.id, branch.getEnrichedAutolinks(ignored)); issuePromises.set( branch.id, getAssociatedIssuesForBranch(container, branch).then(issues => issues.value), @@ -1406,6 +1440,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 54b08d3e6f107..88cefd1bcd0be 100644 --- a/src/webviews/home/protocol.ts +++ b/src/webviews/home/protocol.ts @@ -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'; @@ -67,6 +67,14 @@ export interface GetOverviewRequest { [key: string]: unknown; } +export interface OverviewBranchIssue { + id: string; + title: string; + url: string; + state: Omit; + isAutolink?: boolean; +} + export interface GetOverviewBranch { reference: GitBranchReference; @@ -166,23 +174,9 @@ export interface GetOverviewBranch { | undefined >; - autolinks?: Promise< - { - id: string; - title: string; - url: string; - state: Omit; - }[] - >; + autolinks?: Promise<(OverviewBranchIssue & { type: IssueOrPullRequestType })[]>; - issues?: Promise< - { - id: string; - title: string; - url: string; - state: Omit; - }[] - >; + issues?: Promise; worktree?: { name: string;