From 2d0caf4668884bb39a91c229635a48bad5dfa445 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 20 Feb 2025 08:41:39 -0700 Subject: [PATCH 1/3] Fixes remote and open branch matching on base ref --- src/plus/launchpad/launchpadProvider.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/plus/launchpad/launchpadProvider.ts b/src/plus/launchpad/launchpadProvider.ts index 8ed0430557783..cb4bb15b3366c 100644 --- a/src/plus/launchpad/launchpadProvider.ts +++ b/src/plus/launchpad/launchpadProvider.ts @@ -109,7 +109,7 @@ export type LaunchpadItem = LaunchpadPullRequest & { export type OpenRepository = { repo: Repository; - remote: GitRemote; + remote?: GitRemote; localBranch?: GitBranch; }; @@ -551,12 +551,16 @@ export class LaunchpadProvider implements Disposable { ): Promise { if (pr.repoIdentity.remote.url == null) return undefined; - const match = - matchingRemoteMap.get(pr.repoIdentity.remote.url) ?? - (pr.underlyingPullRequest?.refs?.base?.url - ? matchingRemoteMap.get(pr.underlyingPullRequest.refs.base.url) - : undefined); - if (match == null) return undefined; + let match = matchingRemoteMap.get(pr.repoIdentity.remote.url); + if (match == null) { + if (pr.underlyingPullRequest?.refs?.base?.url == null) return undefined; + + match = matchingRemoteMap.get(pr.underlyingPullRequest.refs.base.url); + if (match == null) return undefined; + + const [repo] = match; + return { repo: repo }; + } const [repo, remote] = match; From 4ba7f233849b4df1ff42a7cb6aac548bfa634f09 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 20 Feb 2025 13:16:58 -0700 Subject: [PATCH 2/3] Uses command message node instead of prompt for missing remote --- contributions.json | 5 +++ package.json | 10 ++++++ src/constants.commands.generated.ts | 1 + src/constants.commands.ts | 3 ++ src/git/utils/-webview/pullRequest.utils.ts | 34 +++++++++++---------- src/views/nodes/pullRequestNode.ts | 31 +++++++++++++++++-- src/views/viewCommands.ts | 14 +++++++++ 7 files changed, 79 insertions(+), 19 deletions(-) diff --git a/contributions.json b/contributions.json index 8dc1dd9dd99eb..01af947be3f96 100644 --- a/contributions.json +++ b/contributions.json @@ -4804,6 +4804,11 @@ ] } }, + "gitlens.views.addPullRequestRemote": { + "label": "Add Pull Request Remote", + "icon": "$(add)", + "enablement": "!operationInProgress" + }, "gitlens.views.addRemote": { "label": "Add Remote...", "icon": "$(add)", diff --git a/package.json b/package.json index 13d66de9f6fd6..2efa5ff18c805 100644 --- a/package.json +++ b/package.json @@ -7629,6 +7629,12 @@ "title": "Add Co-authors...", "icon": "$(person-add)" }, + { + "command": "gitlens.views.addPullRequestRemote", + "title": "Add Pull Request Remote", + "icon": "$(add)", + "enablement": "!operationInProgress" + }, { "command": "gitlens.views.addRemote", "title": "Add Remote...", @@ -11330,6 +11336,10 @@ "command": "gitlens.views.addAuthors", "when": "false" }, + { + "command": "gitlens.views.addPullRequestRemote", + "when": "false" + }, { "command": "gitlens.views.addRemote", "when": "false" diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index a6867e5663f42..c2db37bfe7949 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -176,6 +176,7 @@ export type ContributedCommands = | 'gitlens.views.addAuthor' | 'gitlens.views.addAuthor.multi' | 'gitlens.views.addAuthors' + | 'gitlens.views.addPullRequestRemote' | 'gitlens.views.addRemote' | 'gitlens.views.applyChanges' | 'gitlens.views.associateIssueWithBranch' diff --git a/src/constants.commands.ts b/src/constants.commands.ts index bff64a31de9a2..c32635f15a188 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -182,6 +182,8 @@ type InternalPlusCommands = | 'gitlens.plus.showPlans' | 'gitlens.plus.validate'; +type InternalPullRequestViewCommands = 'gitlens.views.addPullRequestRemote'; + type InternalScmGroupedViewCommands = | 'gitlens.views.scm.grouped.welcome.dismiss' | 'gitlens.views.scm.grouped.welcome.restore'; @@ -221,6 +223,7 @@ type InternalGlCommands = | InternalHomeWebviewViewCommands | InternalLaunchPadCommands | InternalPlusCommands + | InternalPullRequestViewCommands | InternalScmGroupedViewCommands | InternalSearchAndCompareViewCommands | InternalTimelineWebviewViewCommands diff --git a/src/git/utils/-webview/pullRequest.utils.ts b/src/git/utils/-webview/pullRequest.utils.ts index 75180cdd4006b..c376ef306c2cf 100644 --- a/src/git/utils/-webview/pullRequest.utils.ts +++ b/src/git/utils/-webview/pullRequest.utils.ts @@ -10,7 +10,7 @@ import { createRevisionRange } from '../revision.utils'; export async function ensurePullRequestRefs( pr: PullRequest, repo: Repository, - options?: { promptMessage?: string }, + options?: { silent?: true; promptMessage?: never } | { silent?: never; promptMessage?: string }, refs?: PullRequestComparisonRefs, ): Promise { if (pr.refs == null) return undefined; @@ -32,7 +32,7 @@ export async function ensurePullRequestRefs( export async function ensurePullRequestRemote( pr: PullRequest, repo: Repository, - options?: { promptMessage?: string }, + options?: { silent?: true; promptMessage?: never } | { silent?: never; promptMessage?: string }, ): Promise { const identity = getRepositoryIdentityForPullRequest(pr); if (identity.remote.url == null) return false; @@ -51,20 +51,22 @@ export async function ensurePullRequestRemote( const confirm = { title: 'Add Remote' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; - const result = await window.showInformationMessage( - `${ - options?.promptMessage ?? `Unable to find a remote for PR #${pr.id}.` - }\nWould you like to add a remote for '${identity.provider.repoDomain}?`, - { modal: true }, - confirm, - cancel, - ); - - if (result === confirm) { - await repo.git - .remotes() - .addRemoteWithResult?.(identity.provider.repoDomain, identity.remote.url, { fetch: true }); - return true; + if (!options?.silent) { + const result = await window.showInformationMessage( + `${ + options?.promptMessage ?? `Unable to find a remote for PR #${pr.id}.` + }\nWould you like to add a remote for '${identity.provider.repoDomain}?`, + { modal: true }, + confirm, + cancel, + ); + + if (result === confirm) { + await repo.git + .remotes() + .addRemoteWithResult?.(identity.provider.repoDomain, identity.remote.url, { fetch: true }); + return true; + } } return false; diff --git a/src/views/nodes/pullRequestNode.ts b/src/views/nodes/pullRequestNode.ts index dd6f22928dd0a..f7fa23716b5bc 100644 --- a/src/views/nodes/pullRequestNode.ts +++ b/src/views/nodes/pullRequestNode.ts @@ -7,16 +7,24 @@ import type { GitBranchReference } from '../../git/models/reference'; import type { Repository } from '../../git/models/repository'; import { getAheadBehindFilesQuery, getCommitsQuery } from '../../git/queryResults'; import { getIssueOrPullRequestMarkdownIcon, getIssueOrPullRequestThemeIcon } from '../../git/utils/-webview/icons'; -import { ensurePullRequestRefs, getOrOpenPullRequestRepository } from '../../git/utils/-webview/pullRequest.utils'; -import { getComparisonRefsForPullRequest } from '../../git/utils/pullRequest.utils'; +import { + ensurePullRequestRefs, + ensurePullRequestRemote, + getOrOpenPullRequestRepository, +} from '../../git/utils/-webview/pullRequest.utils'; +import { + getComparisonRefsForPullRequest, + getRepositoryIdentityForPullRequest, +} from '../../git/utils/pullRequest.utils'; import { createRevisionRange } from '../../git/utils/revision.utils'; +import { createCommand } from '../../system/-webview/command'; import { pluralize } from '../../system/string'; import type { ViewsWithCommits } from '../viewBase'; import { CacheableChildrenViewNode } from './abstract/cacheableChildrenViewNode'; import type { ClipboardType, ViewNode } from './abstract/viewNode'; import { ContextValues, getViewNodeId } from './abstract/viewNode'; import { CodeSuggestionsNode } from './codeSuggestionsNode'; -import { MessageNode } from './common'; +import { CommandMessageNode, MessageNode } from './common'; import { ResultsCommitsNode } from './resultsCommitsNode'; import { ResultsFilesNode } from './resultsFilesNode'; @@ -159,6 +167,23 @@ export async function getPullRequestChildren( const repoPath = repo.path; const refs = getComparisonRefsForPullRequest(repoPath, pullRequest.refs!); + const identity = getRepositoryIdentityForPullRequest(pullRequest); + if (!(await ensurePullRequestRemote(pullRequest, repo, { silent: true }))) { + return [ + new CommandMessageNode( + view, + parent, + createCommand<[ViewNode, PullRequest, Repository]>( + 'gitlens.views.addPullRequestRemote', + 'Add Pull Request Remote...', + parent, + pullRequest, + repo, + ), + `Missing remote '${identity.provider.repoDomain}'. Click to add.`, + ), + ]; + } const counts = await ensurePullRequestRefs( pullRequest, diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 8f8d84ce82280..d4b26e74772ef 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -22,7 +22,9 @@ import * as StashActions from '../git/actions/stash'; import * as TagActions from '../git/actions/tag'; import * as WorktreeActions from '../git/actions/worktree'; import { GitUri } from '../git/gitUri'; +import type { PullRequest } from '../git/models/pullRequest'; import { RemoteResourceType } from '../git/models/remoteResource'; +import type { Repository } from '../git/models/repository'; import { deletedOrMissing } from '../git/models/revision'; import { ensurePullRequestRefs, @@ -238,6 +240,8 @@ export class ViewCommands implements Disposable { registerViewCommand('gitlens.views.addAuthor', this.addAuthor, this), registerViewCommand('gitlens.views.addAuthor.multi', this.addAuthor, this, true), + registerViewCommand('gitlens.views.addPullRequestRemote', this.addPullRequestRemote, this), + registerViewCommand( 'gitlens.views.openBranchOnRemote', n => executeCommand(GlCommand.OpenBranchOnRemote, n), @@ -481,6 +485,16 @@ export class ViewCommands implements Disposable { return RemoteActions.add(getNodeRepoPath(node)); } + @log() + private async addPullRequestRemote(node: ViewNode, pr: PullRequest, repo: Repository) { + const identity = getRepositoryIdentityForPullRequest(pr); + if (identity.remote?.url == null) return; + await repo.git + .remotes() + .addRemoteWithResult?.(identity.provider.repoDomain, identity.remote.url, { fetch: true }); + return node.view.refreshNode(node, true); + } + @log() private applyChanges(node: ViewRefFileNode) { if (node.is('results-file')) { From 893da30ca11625a9d373558b0664f2b45182a1c5 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 21 Feb 2025 19:07:58 -0500 Subject: [PATCH 3/3] Updates view styling --- src/views/nodes/common.ts | 4 +++- src/views/nodes/pullRequestNode.ts | 14 ++++++++++++-- src/views/nodes/remoteNode.ts | 2 +- src/views/viewCommands.ts | 7 +++---- src/views/viewDecorationProvider.ts | 20 ++++++++++++++------ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/views/nodes/common.ts b/src/views/nodes/common.ts index 813f8034dcf64..518f4d032b0d2 100644 --- a/src/views/nodes/common.ts +++ b/src/views/nodes/common.ts @@ -51,8 +51,10 @@ export class CommandMessageNode extends MessageNode { description?: string, tooltip?: string, iconPath?: TreeItem['iconPath'], + contextValue?: string, + resourceUri?: Uri, ) { - super(view, parent, message, description, tooltip, iconPath); + super(view, parent, message, description, tooltip, iconPath, contextValue, resourceUri); } override getTreeItem(): TreeItem | Promise { diff --git a/src/views/nodes/pullRequestNode.ts b/src/views/nodes/pullRequestNode.ts index f7fa23716b5bc..12629818a7180 100644 --- a/src/views/nodes/pullRequestNode.ts +++ b/src/views/nodes/pullRequestNode.ts @@ -1,4 +1,5 @@ -import { MarkdownString, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import type { Colors } from '../../constants.colors'; import { GitUri } from '../../git/gitUri'; import { GitBranch } from '../../git/models/branch'; import type { GitCommit } from '../../git/models/commit'; @@ -20,6 +21,7 @@ import { createRevisionRange } from '../../git/utils/revision.utils'; import { createCommand } from '../../system/-webview/command'; import { pluralize } from '../../system/string'; import type { ViewsWithCommits } from '../viewBase'; +import { createViewDecorationUri } from '../viewDecorationProvider'; import { CacheableChildrenViewNode } from './abstract/cacheableChildrenViewNode'; import type { ClipboardType, ViewNode } from './abstract/viewNode'; import { ContextValues, getViewNodeId } from './abstract/viewNode'; @@ -180,7 +182,15 @@ export async function getPullRequestChildren( pullRequest, repo, ), - `Missing remote '${identity.provider.repoDomain}'. Click to add.`, + `Unable to find a remote for '${identity.provider.repoDomain}'`, + undefined, + `Click to add a remote for '${identity.provider.repoDomain}'`, + new ThemeIcon( + 'question', + new ThemeColor('gitlens.decorations.workspaceRepoMissingForegroundColor' satisfies Colors), + ), + undefined, + createViewDecorationUri('remote', { state: 'missing' }), ), ]; } diff --git a/src/views/nodes/remoteNode.ts b/src/views/nodes/remoteNode.ts index 6bbd03ac1ce96..9de399d34168e 100644 --- a/src/views/nodes/remoteNode.ts +++ b/src/views/nodes/remoteNode.ts @@ -130,7 +130,7 @@ export class RemoteNode extends ViewNode<'remote', ViewsWithRemotes> { if (this.remote.default) { item.contextValue += '+default'; } - item.resourceUri = createViewDecorationUri('remote', { default: this.remote.default }); + item.resourceUri = createViewDecorationUri('remote', { state: this.remote.default ? 'default' : undefined }); for (const { type, url } of this.remote.urls) { tooltip += `\\\n${url} (${type})`; diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index d4b26e74772ef..d69066b15c257 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -489,10 +489,9 @@ export class ViewCommands implements Disposable { private async addPullRequestRemote(node: ViewNode, pr: PullRequest, repo: Repository) { const identity = getRepositoryIdentityForPullRequest(pr); if (identity.remote?.url == null) return; - await repo.git - .remotes() - .addRemoteWithResult?.(identity.provider.repoDomain, identity.remote.url, { fetch: true }); - return node.view.refreshNode(node, true); + + await repo.git.remotes().addRemote?.(identity.provider.repoDomain, identity.remote.url, { fetch: true }); + return node.triggerChange(true); } @log() diff --git a/src/views/viewDecorationProvider.ts b/src/views/viewDecorationProvider.ts index 2fe30e796b922..4adfff69e3448 100644 --- a/src/views/viewDecorationProvider.ts +++ b/src/views/viewDecorationProvider.ts @@ -207,17 +207,25 @@ function getCommitFileStatusDecoration(uri: Uri, _token: CancellationToken): Fil } interface RemoteViewDecoration { - default?: boolean; + state?: 'default' | 'missing'; } function getRemoteDecoration(uri: Uri, _token: CancellationToken): FileDecoration | undefined { const state = getViewDecoration<'remote'>(uri); - if (state?.default) { - return { - badge: GlyphChars.Check, - tooltip: 'Default Remote', - }; + switch (state?.state) { + case 'default': + return { + badge: GlyphChars.Check, + tooltip: 'Default Remote', + }; + + case 'missing': + return { + badge: '?', + color: new ThemeColor('gitlens.decorations.workspaceRepoMissingForegroundColor' satisfies Colors), + tooltip: '', + }; } return undefined;