From f7a74506290d32857912561c7e3b58463a17566c Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Mon, 21 Oct 2024 10:39:07 -0700 Subject: [PATCH 01/17] Adds option to get body on GitHub issues, uses in getMyIssues (#3621, #3698) --- src/git/models/issue.ts | 3 +++ src/plus/integrations/providers/github.ts | 1 + .../integrations/providers/github/github.ts | 23 +++++++++++++++---- .../integrations/providers/github/models.ts | 2 ++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/git/models/issue.ts b/src/git/models/issue.ts index a7ed52499d3f3..a4fc475ffd1f4 100644 --- a/src/git/models/issue.ts +++ b/src/git/models/issue.ts @@ -52,6 +52,7 @@ export interface IssueShape extends IssueOrPullRequest { assignees: IssueMember[]; repository?: IssueRepository; labels?: IssueLabel[]; + body?: string; } export interface SearchedIssue { @@ -232,6 +233,7 @@ export function serializeIssue(value: IssueShape): IssueShape { })), commentsCount: value.commentsCount, thumbsUpCount: value.thumbsUpCount, + body: value.body, }; return serialized; } @@ -256,5 +258,6 @@ export class Issue implements IssueShape { public readonly labels?: IssueLabel[], public readonly commentsCount?: number, public readonly thumbsUpCount?: number, + public readonly body?: string, ) {} } diff --git a/src/plus/integrations/providers/github.ts b/src/plus/integrations/providers/github.ts index fcc556c28d1e6..4145d3617b4c2 100644 --- a/src/plus/integrations/providers/github.ts +++ b/src/plus/integrations/providers/github.ts @@ -201,6 +201,7 @@ abstract class GitHubIntegrationBase extends { repos: repos?.map(r => `${r.owner}/${r.name}`), baseUrl: this.apiBaseUrl, + includeBody: true, }, cancellation, ); diff --git a/src/plus/integrations/providers/github/github.ts b/src/plus/integrations/providers/github/github.ts index 50aa36f73b523..6746a22afc44e 100644 --- a/src/plus/integrations/providers/github/github.ts +++ b/src/plus/integrations/providers/github/github.ts @@ -2933,7 +2933,14 @@ export class GitHubApi implements Disposable { async searchMyIssues( provider: Provider, token: string, - options?: { search?: string; user?: string; repos?: string[]; baseUrl?: string; avatarSize?: number }, + options?: { + search?: string; + user?: string; + repos?: string[]; + baseUrl?: string; + avatarSize?: number; + includeBody?: boolean; + }, cancellation?: CancellationToken, ): Promise { const scope = getLogScope(); @@ -2950,6 +2957,14 @@ export class GitHubApi implements Disposable { }; } + const issueFragement = `${gqIssueFragment}${ + options?.includeBody + ? ` + body + ` + : '' + }`; + const query = `query searchMyIssues( $authored: String! $assigned: String! @@ -2959,21 +2974,21 @@ export class GitHubApi implements Disposable { authored: search(first: 100, query: $authored, type: ISSUE) { nodes { ... on Issue { - ${gqIssueFragment} + ${issueFragement} } } } assigned: search(first: 100, query: $assigned, type: ISSUE) { nodes { ... on Issue { - ${gqIssueFragment} + ${issueFragement} } } } mentioned: search(first: 100, query: $mentioned, type: ISSUE) { nodes { ... on Issue { - ${gqIssueFragment} + ${issueFragement} } } } diff --git a/src/plus/integrations/providers/github/models.ts b/src/plus/integrations/providers/github/models.ts index 3979832407619..569bd521fdcfd 100644 --- a/src/plus/integrations/providers/github/models.ts +++ b/src/plus/integrations/providers/github/models.ts @@ -134,6 +134,7 @@ export interface GitHubIssue extends Omit Date: Tue, 22 Oct 2024 07:50:13 -0700 Subject: [PATCH 02/17] Barebones 'start work' command for hacking/experimenting (#3621, #3698) --- package.json | 10 ++ src/commands/quickWizard.ts | 8 +- src/commands/quickWizard.utils.ts | 5 + src/constants.commands.ts | 1 + src/plus/startWork/startWork.ts | 174 ++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/plus/startWork/startWork.ts diff --git a/package.json b/package.json index 6cdf5af080007..0cce9791b2cad 100644 --- a/package.json +++ b/package.json @@ -5892,6 +5892,12 @@ "category": "GitLens", "icon": "$(rocket)" }, + { + "command": "gitlens.startWork", + "title": "Start Work", + "category": "GitLens", + "icon": "$(rocket)" + }, { "command": "gitlens.showLaunchpadView", "title": "Show Launchpad View", @@ -10350,6 +10356,10 @@ "command": "gitlens.showLaunchpad", "when": "gitlens:enabled" }, + { + "command": "gitlens.startWork", + "when": "gitlens:enabled" + }, { "command": "gitlens.showLaunchpadView", "when": "gitlens:enabled" diff --git a/src/commands/quickWizard.ts b/src/commands/quickWizard.ts index 4ac108aa2574f..2e993fd17ec79 100644 --- a/src/commands/quickWizard.ts +++ b/src/commands/quickWizard.ts @@ -1,17 +1,18 @@ import { Commands } from '../constants.commands'; import type { Container } from '../container'; import type { LaunchpadCommandArgs } from '../plus/launchpad/launchpad'; +import type { StartWorkCommandArgs } from '../plus/startWork/startWork'; import { command } from '../system/vscode/command'; import type { CommandContext } from './base'; import type { QuickWizardCommandArgsWithCompletion } from './quickWizard.base'; import { QuickWizardCommandBase } from './quickWizard.base'; -export type QuickWizardCommandArgs = LaunchpadCommandArgs; +export type QuickWizardCommandArgs = LaunchpadCommandArgs | StartWorkCommandArgs; @command() export class QuickWizardCommand extends QuickWizardCommandBase { constructor(container: Container) { - super(container, [Commands.ShowLaunchpad]); + super(container, [Commands.ShowLaunchpad, Commands.StartWork]); } protected override preExecute( @@ -22,6 +23,9 @@ export class QuickWizardCommand extends QuickWizardCommandBase { case Commands.ShowLaunchpad: return this.execute({ command: 'launchpad', ...args }); + case Commands.StartWork: + return this.execute({ command: 'startWork', ...args }); + default: return this.execute(args); } diff --git a/src/commands/quickWizard.utils.ts b/src/commands/quickWizard.utils.ts index 859369b03f96b..40a4b8f13b110 100644 --- a/src/commands/quickWizard.utils.ts +++ b/src/commands/quickWizard.utils.ts @@ -1,6 +1,7 @@ import type { StoredRecentUsage } from '../constants.storage'; import type { Container } from '../container'; import { LaunchpadCommand } from '../plus/launchpad/launchpad'; +import { StartWorkCommand } from '../plus/startWork/startWork'; import { configuration } from '../system/vscode/configuration'; import { getContext } from '../system/vscode/context'; import { BranchGitCommand } from './git/branch'; @@ -112,6 +113,10 @@ export class QuickWizardRootStep implements QuickPickStep { if (args?.command === 'launchpad') { this.hiddenItems.push(new LaunchpadCommand(container, args)); } + + if (args?.command === 'startWork') { + this.hiddenItems.push(new StartWorkCommand(container)); + } } private _command: QuickCommand | undefined; diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 64c1a51f417a1..e778ce93411d8 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -223,6 +223,7 @@ export const enum Commands { ShowTimelineView = 'gitlens.showTimelineView', ShowWorktreesView = 'gitlens.showWorktreesView', ShowWorkspacesView = 'gitlens.showWorkspacesView', + StartWork = 'gitlens.startWork', StashApply = 'gitlens.stashApply', StashSave = 'gitlens.stashSave', StashSaveFiles = 'gitlens.stashSaveFiles', diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts new file mode 100644 index 0000000000000..8ecc64652f3a8 --- /dev/null +++ b/src/plus/startWork/startWork.ts @@ -0,0 +1,174 @@ +import { Uri } from 'vscode'; +import type { + PartialStepState, + StepGenerator, + StepResultGenerator, + StepSelection, + StepState, +} from '../../commands/quickCommand'; +import { + canPickStepContinue, + createPickStep, + endSteps, + QuickCommand, + StepResultBreak, +} from '../../commands/quickCommand'; +import { proBadge } from '../../constants'; +import { HostingIntegrationId } from '../../constants.integrations'; +import type { Container } from '../../container'; +import type { SearchedIssue } from '../../git/models/issue'; +import type { QuickPickItemOfT } from '../../quickpicks/items/common'; +import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive'; +import { fromNow } from '../../system/date'; + +export type StartWorkItem = { + item: SearchedIssue; +}; + +export type StartWorkResult = { items: StartWorkItem[] }; + +interface Context { + result: StartWorkResult; + title: string; +} + +interface State { + item?: StartWorkItem; + action?: StartWorkAction; +} + +type StartWorkStepState = RequireSome, 'item'>; + +export type StartWorkAction = 'start'; + +export interface StartWorkCommandArgs { + readonly command: 'startWork'; +} + +function assertsStartWorkStepState(state: StepState): asserts state is StartWorkStepState { + if (state.item != null) return; + + debugger; + throw new Error('Missing item'); +} + +export class StartWorkCommand extends QuickCommand { + constructor(container: Container) { + super(container, 'startWork', 'startWork', `Start Work\u00a0\u00a0${proBadge}`, { + description: 'Start work on an issue', + }); + + this.initialState = { + counter: 0, + }; + } + + protected async *steps(state: PartialStepState): StepGenerator { + if (this.container.git.isDiscoveringRepositories) { + await this.container.git.isDiscoveringRepositories; + } + + const context: Context = { + result: { items: [] }, + title: this.title, + }; + + while (this.canStepsContinue(state)) { + context.title = this.title; + + await updateContextItems(this.container, context); + + if (state.counter < 1 || state.item == null) { + const result = yield* this.pickIssueStep(state, context); + if (result === StepResultBreak) continue; + state.item = result; + } + + assertsStartWorkStepState(state); + state.action = 'start'; + + if (typeof state.action === 'string') { + switch (state.action) { + case 'start': + startWork(state.item.item); + break; + } + } + + endSteps(state); + } + + return state.counter < 0 ? StepResultBreak : undefined; + } + + private *pickIssueStep(state: StepState, context: Context): StepResultGenerator { + const buildIssueItem = (i: StartWorkItem) => { + return { + label: + i.item.issue.title.length > 60 ? `${i.item.issue.title.substring(0, 60)}...` : i.item.issue.title, + // description: `${i.repoAndOwner}#${i.id}, by @${i.author}`, + description: `\u00a0 ${i.item.issue.repository?.owner ?? ''}/${i.item.issue.repository?.repo ?? ''}#${ + i.item.issue.id + } \u00a0`, + detail: ` ${fromNow(i.item.issue.updatedDate)} by @${i.item.issue.author.name}`, + iconPath: i.item.issue.author?.avatarUrl != null ? Uri.parse(i.item.issue.author.avatarUrl) : undefined, + item: i, + picked: i.item.issue.id === state.item?.item?.issue.id, + }; + }; + + const getItems = (result: StartWorkResult) => { + const items: QuickPickItemOfT[] = []; + + if (result.items?.length) { + items.push(...result.items.map(buildIssueItem)); + } + + return items; + }; + + function getItemsAndPlaceholder() { + if (!context.result.items.length) { + return { + placeholder: 'All done! Take a vacation', + items: [createDirectiveQuickPickItem(Directive.Cancel, undefined, { label: 'OK' })], + }; + } + + return { + placeholder: 'Choose an item to focus on', + items: getItems(context.result), + }; + } + + const { items, placeholder } = getItemsAndPlaceholder(); + + const step = createPickStep({ + title: context.title, + placeholder: placeholder, + matchOnDescription: true, + matchOnDetail: true, + items: items, + }); + + const selection: StepSelection = yield step; + if (!canPickStepContinue(step, state, selection)) { + return StepResultBreak; + } + const element = selection[0]; + return { ...element.item }; + } +} + +async function updateContextItems(container: Container, context: Context) { + context.result = { + items: + (await container.integrations.getMyIssues([HostingIntegrationId.GitHub]))?.map(i => ({ + item: i, + })) ?? [], + }; +} + +function startWork(_issue: SearchedIssue) { + // TODO: Hack here +} From 9812660153f7fc35db9ff9c1b8eeb87df6969b56 Mon Sep 17 00:00:00 2001 From: Chivorotkiv Date: Tue, 12 Nov 2024 17:55:47 +0100 Subject: [PATCH 03/17] Adds "Start Work" shortcut to the "Home" view (#3621, #3698) --- src/webviews/apps/plus/home/components/launchpad.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/webviews/apps/plus/home/components/launchpad.ts b/src/webviews/apps/plus/home/components/launchpad.ts index db9adcb535cc9..b51d64995c3fc 100644 --- a/src/webviews/apps/plus/home/components/launchpad.ts +++ b/src/webviews/apps/plus/home/components/launchpad.ts @@ -3,9 +3,9 @@ import { SignalWatcher } from '@lit-labs/signals'; import type { TemplateResult } from 'lit'; import { css, html, LitElement, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import type { BranchGitCommandArgs } from '../../../../../commands/git/branch'; import { Commands } from '../../../../../constants.commands'; import type { LaunchpadCommandArgs } from '../../../../../plus/launchpad/launchpad'; +import type { StartWorkCommandArgs } from '../../../../../plus/startWork/startWork'; import { createCommandLink } from '../../../../../system/commands'; import { pluralize } from '../../../../../system/string'; import type { GetLaunchpadSummaryResponse, State } from '../../../../home/protocol'; @@ -108,13 +108,7 @@ export class GlLaunchpad extends SignalWatcher(LitElement) { }); get startWorkCommand() { - return createCommandLink(Commands.GitCommandsBranch, { - state: { - subcommand: 'create', - }, - command: 'branch', - confirm: true, - }); + return createCommandLink(Commands.StartWork, { command: 'startWork' }); } override connectedCallback() { From 231df163e47e25b58b601254489efe226ed4e40e Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 24 Oct 2024 15:51:51 +0200 Subject: [PATCH 04/17] Connects integrations if they are not connected (#3621, #3698) --- docs/telemetry-events.md | 2 +- src/constants.telemetry.ts | 1 + src/plus/startWork/startWork.ts | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index f8e4028e27a69..cad716bc8ca1d 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -1331,7 +1331,7 @@ void 'repository.visibility': 'private' | 'public' | 'local', 'repoPrivacy': 'private' | 'public' | 'local', 'filesChanged': number, - 'source': 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'code-suggest' | 'account' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'trial-indicator' | 'scm-input' | 'subscription' | 'walkthrough' | 'worktrees' + 'source': 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'code-suggest' | 'account' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'startWork' | 'trial-indicator' | 'scm-input' | 'subscription' | 'walkthrough' | 'worktrees' } ``` diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 1d9be449af62f..c6d8bff849e16 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -620,6 +620,7 @@ export type Sources = | 'quick-wizard' | 'remoteProvider' | 'settings' + | 'startWork' | 'timeline' | 'trial-indicator' | 'scm-input' diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 8ecc64652f3a8..84a71508460fa 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -1,5 +1,7 @@ +import type { QuickPick } from 'vscode'; import { Uri } from 'vscode'; import type { + AsyncStepResultGenerator, PartialStepState, StepGenerator, StepResultGenerator, @@ -10,16 +12,20 @@ import { canPickStepContinue, createPickStep, endSteps, + freezeStep, QuickCommand, StepResultBreak, } from '../../commands/quickCommand'; import { proBadge } from '../../constants'; +import type { IntegrationId } from '../../constants.integrations'; import { HostingIntegrationId } from '../../constants.integrations'; import type { Container } from '../../container'; import type { SearchedIssue } from '../../git/models/issue'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; +import { createQuickPickItemOfT } from '../../quickpicks/items/common'; import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive'; import { fromNow } from '../../system/date'; +import { some } from '../../system/iterable'; export type StartWorkItem = { item: SearchedIssue; @@ -30,6 +36,7 @@ export type StartWorkResult = { items: StartWorkItem[] }; interface Context { result: StartWorkResult; title: string; + connectedIntegrations: Map; } interface State { @@ -52,6 +59,8 @@ function assertsStartWorkStepState(state: StepState): asserts state is St throw new Error('Missing item'); } +export const supportedStartWorkIntegrations = [HostingIntegrationId.GitHub]; + export class StartWorkCommand extends QuickCommand { constructor(container: Container) { super(container, 'startWork', 'startWork', `Start Work\u00a0\u00a0${proBadge}`, { @@ -71,11 +80,20 @@ export class StartWorkCommand extends QuickCommand { const context: Context = { result: { items: [] }, title: this.title, + connectedIntegrations: await this.getConnectedIntegrations(), }; while (this.canStepsContinue(state)) { context.title = this.title; + const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c); + if (!hasConnectedIntegrations) { + const result = yield* this.confirmCloudIntegrationsConnectStep(state, context); + if (result === StepResultBreak) { + return result; + } + } + await updateContextItems(this.container, context); if (state.counter < 1 || state.item == null) { @@ -101,6 +119,65 @@ export class StartWorkCommand extends QuickCommand { return state.counter < 0 ? StepResultBreak : undefined; } + private async *confirmCloudIntegrationsConnectStep( + state: StepState, + context: Context, + ): AsyncStepResultGenerator<{ connected: boolean | IntegrationId; resume: () => void }> { + // TODO: This step is almost an exact copy of the similar one from launchpad.ts. Do we want to do anything about it? Maybe to move it to an util function with ability to parameterize labels? + const hasConnectedIntegration = some(context.connectedIntegrations.values(), c => c); + const step = this.createConfirmStep( + `${this.title} \u00a0\u2022\u00a0 Connect an ${hasConnectedIntegration ? 'Additional ' : ''}Integration`, + [ + createQuickPickItemOfT( + { + label: `Connect an ${hasConnectedIntegration ? 'Additional ' : ''}Integration...`, + detail: hasConnectedIntegration + ? 'Connect additional integrations to view their issues in Start Work' + : 'Connect an integration to accelerate your work', + picked: true, + }, + true, + ), + ], + createDirectiveQuickPickItem(Directive.Cancel, false, { label: 'Cancel' }), + { + placeholder: hasConnectedIntegration + ? 'Connect additional integrations to Start Work' + : 'Connect an integration to get started with Start Work', + buttons: [], + ignoreFocusOut: true, + }, + ); + + // Note: This is a hack to allow the quickpick to stay alive after the user finishes connecting the integration. + // Otherwise it disappears. + let freeze!: () => Disposable; + let quickpick!: QuickPick; + step.onDidActivate = qp => { + quickpick = qp; + freeze = () => freezeStep(step, qp); + }; + + const selection: StepSelection = yield step; + + if (canPickStepContinue(step, state, selection)) { + const previousPlaceholder = quickpick.placeholder; + quickpick.placeholder = 'Connecting integrations...'; + quickpick.ignoreFocusOut = true; + const resume = freeze(); + const connected = await this.container.integrations.connectCloudIntegrations( + { integrationIds: supportedStartWorkIntegrations }, + { + source: 'startWork', + }, + ); + quickpick.placeholder = previousPlaceholder; + return { connected: connected, resume: () => resume[Symbol.dispose]() }; + } + + return StepResultBreak; + } + private *pickIssueStep(state: StepState, context: Context): StepResultGenerator { const buildIssueItem = (i: StartWorkItem) => { return { @@ -158,6 +235,18 @@ export class StartWorkCommand extends QuickCommand { const element = selection[0]; return { ...element.item }; } + + private async getConnectedIntegrations(): Promise> { + const connected = new Map(); + await Promise.allSettled( + supportedStartWorkIntegrations.map(async integrationId => { + const integration = await this.container.integrations.get(integrationId); + connected.set(integrationId, integration.maybeConnected ?? (await integration.isConnected())); + }), + ); + + return connected; + } } async function updateContextItems(container: Container, context: Context) { From f7a02423cb94a78ee779df1b0c37ec13354b6182 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 24 Oct 2024 16:52:38 +0200 Subject: [PATCH 05/17] Connects using a different flow when cloudIntegrations are disabled. (#3621, #3698) --- src/plus/startWork/startWork.ts | 72 ++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 84a71508460fa..e6992de56919b 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -26,6 +26,7 @@ import { createQuickPickItemOfT } from '../../quickpicks/items/common'; import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive'; import { fromNow } from '../../system/date'; import { some } from '../../system/iterable'; +import { configuration } from '../../system/vscode/configuration'; export type StartWorkItem = { item: SearchedIssue; @@ -88,7 +89,10 @@ export class StartWorkCommand extends QuickCommand { const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c); if (!hasConnectedIntegrations) { - const result = yield* this.confirmCloudIntegrationsConnectStep(state, context); + const isUsingCloudIntegrations = configuration.get('cloudIntegrations.enabled', undefined, false); + const result = isUsingCloudIntegrations + ? yield* this.confirmCloudIntegrationsConnectStep(state, context) + : yield* this.confirmLocalIntegrationConnectStep(state, context); if (result === StepResultBreak) { return result; } @@ -119,6 +123,72 @@ export class StartWorkCommand extends QuickCommand { return state.counter < 0 ? StepResultBreak : undefined; } + private async *confirmLocalIntegrationConnectStep( + state: StepState, + context: Context, + ): AsyncStepResultGenerator<{ connected: boolean | IntegrationId; resume: () => void }> { + const confirmations: (QuickPickItemOfT | DirectiveQuickPickItem)[] = []; + + for (const integration of supportedStartWorkIntegrations) { + if (context.connectedIntegrations.get(integration)) { + continue; + } + switch (integration) { + case HostingIntegrationId.GitHub: + confirmations.push( + createQuickPickItemOfT( + { + label: 'Connect to GitHub...', + detail: 'Will connect to GitHub to provide access your pull requests and issues', + }, + integration, + ), + ); + break; + default: + break; + } + } + + const step = this.createConfirmStep( + `${this.title} \u00a0\u2022\u00a0 Connect an Integration`, + confirmations, + createDirectiveQuickPickItem(Directive.Cancel, false, { label: 'Cancel' }), + { + placeholder: 'Connect an integration to view their issues in Start Work', + buttons: [], + ignoreFocusOut: false, + }, + ); + + // Note: This is a hack to allow the quickpick to stay alive after the user finishes connecting the integration. + // Otherwise it disappears. + let freeze!: () => Disposable; + step.onDidActivate = qp => { + freeze = () => freezeStep(step, qp); + }; + + const selection: StepSelection = yield step; + if (canPickStepContinue(step, state, selection)) { + const resume = freeze(); + const chosenIntegrationId = selection[0].item; + const connected = await this.ensureIntegrationConnected(chosenIntegrationId); + return { connected: connected ? chosenIntegrationId : false, resume: () => resume[Symbol.dispose]() }; + } + + return StepResultBreak; + } + + private async ensureIntegrationConnected(id: IntegrationId) { + const integration = await this.container.integrations.get(id); + let connected = integration.maybeConnected ?? (await integration.isConnected()); + if (!connected) { + connected = await integration.connect('startWork'); + } + + return connected; + } + private async *confirmCloudIntegrationsConnectStep( state: StepState, context: Context, From 1467dea3b8fb6c71d7bf89e78bcd12cc2474d370 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 24 Oct 2024 16:04:37 +0200 Subject: [PATCH 06/17] Adds some telemetry to start work flow (#3621, #3698) --- docs/telemetry-events.md | 32 ++++++++++++++++++++++++++++++++ src/constants.telemetry.ts | 19 +++++++++++++++++++ src/plus/startWork/startWork.ts | 28 +++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index cad716bc8ca1d..61b0a00bf283f 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -1321,6 +1321,38 @@ void } ``` +### startWork/open + +> Sent when the user opens Start Work; use `instance` to correlate a StartWork "session" + +```typescript +{ + 'instance': number +} +``` + +### startWork/opened + +> Sent when the launchpad is opened; use `instance` to correlate a StartWork "session" + +```typescript +{ + 'instance': number, + 'connected': false | true +} +``` + +### startWork/steps/connect + +> Sent when the Start Work has "reloaded" (while open, e.g. user refreshed or back button) and is disconnected; use `instance` to correlate a Start Work "session" + +```typescript +{ + 'instance': number, + 'connected': false | true +} +``` + ### openReviewMode > Sent when a PR review was started in the inspect overview diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index c6d8bff849e16..4f63a757c39e0 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -311,6 +311,17 @@ export type TelemetryEvents = { duration: number; }; + /** Sent when the user opens Start Work; use `instance` to correlate a StartWork "session" */ + 'startWork/open': StartWorkEventDataBase; + /** Sent when the launchpad is opened; use `instance` to correlate a StartWork "session" */ + 'startWork/opened': StartWorkEventData & { + connected: boolean; + }; + /** Sent when the Start Work has "reloaded" (while open, e.g. user refreshed or back button) and is disconnected; use `instance` to correlate a Start Work "session" */ + 'startWork/steps/connect': StartWorkEventData & { + connected: boolean; + }; + /** Sent when a PR review was started in the inspect overview */ openReviewMode: { provider: string; @@ -451,6 +462,14 @@ export type CommandEventData = webview?: string; }; +export type StartWorkTelemetryContext = StartWorkEventDataBase; + +type StartWorkEventDataBase = { + instance: number; +}; + +type StartWorkEventData = StartWorkEventDataBase; + export type LaunchpadTelemetryContext = LaunchpadEventData; type LaunchpadEventDataBase = { diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index e6992de56919b..f415fb559fb78 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -19,11 +19,13 @@ import { import { proBadge } from '../../constants'; import type { IntegrationId } from '../../constants.integrations'; import { HostingIntegrationId } from '../../constants.integrations'; +import type { Source, Sources, StartWorkTelemetryContext } from '../../constants.telemetry'; import type { Container } from '../../container'; import type { SearchedIssue } from '../../git/models/issue'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickItemOfT } from '../../quickpicks/items/common'; import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive'; +import { getScopedCounter } from '../../system/counter'; import { fromNow } from '../../system/date'; import { some } from '../../system/iterable'; import { configuration } from '../../system/vscode/configuration'; @@ -37,6 +39,7 @@ export type StartWorkResult = { items: StartWorkItem[] }; interface Context { result: StartWorkResult; title: string; + telemetryContext: StartWorkTelemetryContext | undefined; connectedIntegrations: Map; } @@ -51,6 +54,7 @@ export type StartWorkAction = 'start'; export interface StartWorkCommandArgs { readonly command: 'startWork'; + source?: Sources; } function assertsStartWorkStepState(state: StepState): asserts state is StartWorkStepState { @@ -61,13 +65,23 @@ function assertsStartWorkStepState(state: StepState): asserts state is St } export const supportedStartWorkIntegrations = [HostingIntegrationId.GitHub]; +const instanceCounter = getScopedCounter(); export class StartWorkCommand extends QuickCommand { - constructor(container: Container) { + private readonly source: Source; + private readonly telemetryContext: StartWorkTelemetryContext | undefined; + constructor(container: Container, args?: StartWorkCommandArgs) { super(container, 'startWork', 'startWork', `Start Work\u00a0\u00a0${proBadge}`, { description: 'Start work on an issue', }); + this.source = { source: args?.source ?? 'commandPalette' }; + + if (this.container.telemetry.enabled) { + this.telemetryContext = { instance: instanceCounter.next() }; + this.container.telemetry.sendEvent('startWork/open', { ...this.telemetryContext }, this.source); + } + this.initialState = { counter: 0, }; @@ -81,14 +95,26 @@ export class StartWorkCommand extends QuickCommand { const context: Context = { result: { items: [] }, title: this.title, + telemetryContext: this.telemetryContext, connectedIntegrations: await this.getConnectedIntegrations(), }; + const opened = false; while (this.canStepsContinue(state)) { context.title = this.title; const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c); if (!hasConnectedIntegrations) { + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent( + opened ? 'startWork/steps/connect' : 'startWork/opened', + { + ...context.telemetryContext!, + connected: false, + }, + this.source, + ); + } const isUsingCloudIntegrations = configuration.get('cloudIntegrations.enabled', undefined, false); const result = isUsingCloudIntegrations ? yield* this.confirmCloudIntegrationsConnectStep(state, context) From b332a16e23036f8136e0cc134d0feb19999c3f4b Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Wed, 23 Oct 2024 04:09:35 +0200 Subject: [PATCH 07/17] Creates branch suggesting a name based on selected issue (#3621, #3698) --- src/plus/startWork/startWork.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index f415fb559fb78..cbc5889fb8361 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -16,6 +16,7 @@ import { QuickCommand, StepResultBreak, } from '../../commands/quickCommand'; +import { getSteps } from '../../commands/quickWizard.utils'; import { proBadge } from '../../constants'; import type { IntegrationId } from '../../constants.integrations'; import { HostingIntegrationId } from '../../constants.integrations'; @@ -24,6 +25,7 @@ import type { Container } from '../../container'; import type { SearchedIssue } from '../../git/models/issue'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickItemOfT } from '../../quickpicks/items/common'; +import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive'; import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive'; import { getScopedCounter } from '../../system/counter'; import { fromNow } from '../../system/date'; @@ -138,7 +140,19 @@ export class StartWorkCommand extends QuickCommand { if (typeof state.action === 'string') { switch (state.action) { case 'start': - startWork(state.item.item); + yield* getSteps( + this.container, + { + command: 'branch', + state: { + subcommand: 'create', + repo: undefined, + name: `${state.item.item.issue.id}-${state.item.item.issue.title}`, + suggestNameOnly: true, + }, + }, + this.pickedVia, + ); break; } } @@ -332,6 +346,13 @@ export class StartWorkCommand extends QuickCommand { return { ...element.item }; } + private startWork(state: PartialStepState, item?: StartWorkItem) { + state.action = 'start'; + if (item != null) { + state.item = item; + } + } + private async getConnectedIntegrations(): Promise> { const connected = new Map(); await Promise.allSettled( @@ -353,7 +374,3 @@ async function updateContextItems(container: Container, context: Context) { })) ?? [], }; } - -function startWork(_issue: SearchedIssue) { - // TODO: Hack here -} From 679b1fd28d1fdc409016182dc97e9e2a5aedd7e4 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 24 Oct 2024 18:49:23 +0200 Subject: [PATCH 08/17] Uses `slug` to convert issue title and id to branch name (#3621, #3698) --- package.json | 6 +- pnpm-lock.yaml | 111 ++++++++++++++++++-------------- src/plus/startWork/startWork.ts | 3 +- 3 files changed, 70 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 0cce9791b2cad..c79a374498c4e 100644 --- a/package.json +++ b/package.json @@ -19861,7 +19861,7 @@ "@gitkraken/provider-apis": "0.24.2", "@gitkraken/shared-web-components": "0.1.1-rc.15", "@gk-nzaytsev/fast-string-truncated-width": "1.1.0", - "@lit-labs/signals": "^0.1.1", + "@lit-labs/signals": "0.1.1", "@lit/context": "1.1.3", "@lit/react": "1.0.6", "@lit/task": "1.0.1", @@ -19887,7 +19887,8 @@ "path-browserify": "1.0.1", "react": "16.8.4", "react-dom": "16.8.4", - "signal-utils": "^0.20.0", + "signal-utils": "0.20.0", + "slug": "10.0.0", "sortablejs": "1.15.0" }, "devDependencies": { @@ -19901,6 +19902,7 @@ "@types/node": "18.15.0", "@types/react": "17.0.82", "@types/react-dom": "17.0.21", + "@types/slug": "5.0.9", "@types/sortablejs": "1.15.8", "@types/vscode": "1.82.0", "@typescript-eslint/parser": "8.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65fa6be42f35f..df075c885e8e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: specifier: 1.1.0 version: 1.1.0 '@lit-labs/signals': - specifier: ^0.1.1 + specifier: 0.1.1 version: 0.1.1 '@lit/context': specifier: 1.1.3 @@ -105,15 +105,18 @@ importers: specifier: 16.8.4 version: 16.8.4(react@16.8.4) signal-utils: - specifier: ^0.20.0 + specifier: 0.20.0 version: 0.20.0(signal-polyfill@0.2.1) + slug: + specifier: 10.0.0 + version: 10.0.0 sortablejs: specifier: 1.15.0 version: 1.15.0 devDependencies: '@eamodio/eslint-lite-webpack-plugin': specifier: 0.1.0 - version: 0.1.0(@swc/core@1.9.1)(esbuild@0.24.0)(eslint@9.14.0)(webpack-cli@5.1.4)(webpack@5.96.1) + version: 0.1.0(@swc/core@1.9.1)(esbuild@0.24.0)(eslint@9.14.0)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) '@eslint/js': specifier: 9.14.0 version: 9.14.0 @@ -141,6 +144,9 @@ importers: '@types/react-dom': specifier: 17.0.21 version: 17.0.21 + '@types/slug': + specifier: 5.0.9 + version: 5.0.9 '@types/sortablejs': specifier: 1.15.8 version: 1.15.8 @@ -167,22 +173,22 @@ importers: version: 1.0.0-rc.12 circular-dependency-plugin: specifier: 5.2.2 - version: 5.2.2(webpack@5.96.1) + version: 5.2.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) clean-webpack-plugin: specifier: 4.0.0 - version: 4.0.0(webpack@5.96.1) + version: 4.0.0(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) copy-webpack-plugin: specifier: 12.0.2 - version: 12.0.2(webpack@5.96.1) + version: 12.0.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) csp-html-webpack-plugin: specifier: 5.1.0 - version: 5.1.0(html-webpack-plugin@5.6.3(webpack@5.96.1))(webpack@5.96.1) + version: 5.1.0(html-webpack-plugin@5.6.3(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) css-loader: specifier: 7.1.2 - version: 7.1.2(webpack@5.96.1) + version: 7.1.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) css-minimizer-webpack-plugin: specifier: 7.0.0 - version: 7.0.0(esbuild@0.24.0)(webpack@5.96.1) + version: 7.0.0(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) cssnano-preset-advanced: specifier: 7.0.6 version: 7.0.6(postcss@8.4.47) @@ -191,7 +197,7 @@ importers: version: 0.24.0 esbuild-loader: specifier: 4.2.2 - version: 4.2.2(webpack@5.96.1) + version: 4.2.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) esbuild-node-externals: specifier: 1.15.0 version: 1.15.0(esbuild@0.24.0) @@ -218,7 +224,7 @@ importers: version: 2.2.0(eslint@9.14.0) fork-ts-checker-webpack-plugin: specifier: 6.5.3 - version: 6.5.3(eslint@9.14.0)(typescript@5.7.1-rc)(webpack@5.96.1) + version: 6.5.3(eslint@9.14.0)(typescript@5.7.1-rc)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) glob: specifier: 11.0.0 version: 11.0.0 @@ -227,13 +233,13 @@ importers: version: 15.12.0 html-loader: specifier: 5.1.0 - version: 5.1.0(webpack@5.96.1) + version: 5.1.0(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) html-webpack-plugin: specifier: 5.6.3 - version: 5.6.3(webpack@5.96.1) + version: 5.6.3(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) image-minimizer-webpack-plugin: specifier: 4.1.0 - version: 4.1.0(sharp@0.32.6)(svgo@3.3.2)(webpack@5.96.1) + version: 4.1.0(sharp@0.32.6)(svgo@3.3.2)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) license-checker-rseidelsohn: specifier: 4.4.2 version: 4.4.2 @@ -242,7 +248,7 @@ importers: version: 1.5.0 mini-css-extract-plugin: specifier: 2.9.2 - version: 2.9.2(webpack@5.96.1) + version: 2.9.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) mocha: specifier: 10.8.2 version: 10.8.2 @@ -260,7 +266,7 @@ importers: version: 1.80.6 sass-loader: specifier: 16.0.3 - version: 16.0.3(sass-embedded@1.77.8)(sass@1.80.6)(webpack@5.96.1) + version: 16.0.3(sass-embedded@1.77.8)(sass@1.80.6)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) schema-utils: specifier: 4.2.0 version: 4.2.0 @@ -272,10 +278,10 @@ importers: version: 3.3.2 terser-webpack-plugin: specifier: 5.3.10 - version: 5.3.10(@swc/core@1.9.1)(esbuild@0.24.0)(webpack@5.96.1) + version: 5.3.10(@swc/core@1.9.1)(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) ts-loader: specifier: 9.5.1 - version: 9.5.1(typescript@5.7.1-rc)(webpack@5.96.1) + version: 9.5.1(typescript@5.7.1-rc)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) typescript: specifier: 5.7.1-rc version: 5.7.1-rc @@ -1088,6 +1094,9 @@ packages: '@types/scheduler@0.16.8': resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + '@types/slug@5.0.9': + resolution: {integrity: sha512-6Yp8BSplP35Esa/wOG1wLNKiqXevpQTEF/RcL/NV6BBQaMmZh4YlDwCgrrFSoUE4xAGvnKd5c+lkQJmPrBAzfQ==} + '@types/sortablejs@1.15.8': resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} @@ -4627,6 +4636,10 @@ packages: slide@1.1.6: resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} + slug@10.0.0: + resolution: {integrity: sha512-M8s2PWOUeSCdD4S1NH5lCzXg2zFV1fozrtfr0FSKl65x+EF1rUowj+/vyFlnHgxPxWzT+DL0VXKfYc1DHJoymg==} + hasBin: true + slugify@1.6.6: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} @@ -5373,10 +5386,10 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@eamodio/eslint-lite-webpack-plugin@0.1.0(@swc/core@1.9.1)(esbuild@0.24.0)(eslint@9.14.0)(webpack-cli@5.1.4)(webpack@5.96.1)': + '@eamodio/eslint-lite-webpack-plugin@0.1.0(@swc/core@1.9.1)(esbuild@0.24.0)(eslint@9.14.0)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: '@types/eslint': 9.6.1 - '@types/webpack': 5.28.5(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) + '@types/webpack': 5.28.5(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1)) eslint: 9.14.0 fast-glob: 3.3.2 minimatch: 10.0.1 @@ -6011,13 +6024,15 @@ snapshots: '@types/scheduler@0.16.8': {} + '@types/slug@5.0.9': {} + '@types/sortablejs@1.15.8': {} '@types/trusted-types@2.0.7': {} '@types/vscode@1.82.0': {} - '@types/webpack@5.28.5(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)': + '@types/webpack@5.28.5(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))': dependencies: '@types/node': 18.15.0 tapable: 2.2.1 @@ -6305,17 +6320,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.96.1)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.96.1)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.96.1)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1) @@ -6776,7 +6791,7 @@ snapshots: ci-info@3.9.0: {} - circular-dependency-plugin@5.2.2(webpack@5.96.1): + circular-dependency-plugin@5.2.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -6788,7 +6803,7 @@ snapshots: clean-stack@2.2.0: {} - clean-webpack-plugin@4.0.0(webpack@5.96.1): + clean-webpack-plugin@4.0.0(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: del: 4.1.1 webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -6893,7 +6908,7 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - copy-webpack-plugin@12.0.2(webpack@5.96.1): + copy-webpack-plugin@12.0.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: fast-glob: 3.3.2 glob-parent: 6.0.2 @@ -6925,10 +6940,10 @@ snapshots: dependencies: custom-event: 1.0.0 - csp-html-webpack-plugin@5.1.0(html-webpack-plugin@5.6.3(webpack@5.96.1))(webpack@5.96.1): + csp-html-webpack-plugin@5.1.0(html-webpack-plugin@5.6.3(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: cheerio: 1.0.0-rc.12 - html-webpack-plugin: 5.6.3(webpack@5.96.1) + html-webpack-plugin: 5.6.3(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) lodash: 4.17.21 webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -6936,7 +6951,7 @@ snapshots: dependencies: postcss: 8.4.47 - css-loader@7.1.2(webpack@5.96.1): + css-loader@7.1.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: icss-utils: 5.1.0(postcss@8.4.47) postcss: 8.4.47 @@ -6949,7 +6964,7 @@ snapshots: optionalDependencies: webpack: 5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4) - css-minimizer-webpack-plugin@7.0.0(esbuild@0.24.0)(webpack@5.96.1): + css-minimizer-webpack-plugin@7.0.0(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 cssnano: 7.0.6(postcss@8.4.47) @@ -7449,7 +7464,7 @@ snapshots: is-symbol: 1.0.4 optional: true - esbuild-loader@4.2.2(webpack@5.96.1): + esbuild-loader@4.2.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: esbuild: 0.24.0 get-tsconfig: 4.8.1 @@ -7520,7 +7535,7 @@ snapshots: debug: 4.3.7(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 9.14.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-plugin-import-x@4.4.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-plugin-import@2.29.1)(eslint@9.14.0))(eslint@9.14.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -7534,7 +7549,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-plugin-import-x@4.4.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-plugin-import@2.29.1)(eslint@9.14.0))(eslint@9.14.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -7576,7 +7591,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-plugin-import-x@4.4.0(eslint@9.14.0)(typescript@5.7.1-rc))(eslint-plugin-import@2.29.1)(eslint@9.14.0))(eslint@9.14.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7760,7 +7775,7 @@ snapshots: cross-spawn: 7.0.5 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@6.5.3(eslint@9.14.0)(typescript@5.7.1-rc)(webpack@5.96.1): + fork-ts-checker-webpack-plugin@6.5.3(eslint@9.14.0)(typescript@5.7.1-rc)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@babel/code-frame': 7.26.2 '@types/json-schema': 7.0.15 @@ -8010,7 +8025,7 @@ snapshots: html-escaper@2.0.2: {} - html-loader@5.1.0(webpack@5.96.1): + html-loader@5.1.0(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: html-minifier-terser: 7.2.0 parse5: 7.2.1 @@ -8036,7 +8051,7 @@ snapshots: relateurl: 0.2.7 terser: 5.36.0 - html-webpack-plugin@5.6.3(webpack@5.96.1): + html-webpack-plugin@5.6.3(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -8135,7 +8150,7 @@ snapshots: ignore@5.3.2: {} - image-minimizer-webpack-plugin@4.1.0(sharp@0.32.6)(svgo@3.3.2)(webpack@5.96.1): + image-minimizer-webpack-plugin@4.1.0(sharp@0.32.6)(svgo@3.3.2)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: schema-utils: 4.2.0 serialize-javascript: 6.0.2 @@ -8774,7 +8789,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.2(webpack@5.96.1): + mini-css-extract-plugin@2.9.2(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: schema-utils: 4.2.0 tapable: 2.2.1 @@ -9844,7 +9859,7 @@ snapshots: sass-embedded-win32-ia32: 1.77.8 sass-embedded-win32-x64: 1.77.8 - sass-loader@16.0.3(sass-embedded@1.77.8)(sass@1.80.6)(webpack@5.96.1): + sass-loader@16.0.3(sass-embedded@1.77.8)(sass@1.80.6)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -9984,6 +9999,8 @@ snapshots: slide@1.1.6: {} + slug@10.0.0: {} + slugify@1.6.6: {} smart-buffer@4.2.0: {} @@ -10233,7 +10250,7 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - terser-webpack-plugin@5.3.10(@swc/core@1.9.1)(esbuild@0.24.0)(webpack@5.96.1): + terser-webpack-plugin@5.3.10(@swc/core@1.9.1)(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -10289,7 +10306,7 @@ snapshots: dependencies: typescript: 5.7.1-rc - ts-loader@9.5.1(typescript@5.7.1-rc)(webpack@5.96.1): + ts-loader@9.5.1(typescript@5.7.1-rc)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.17.1 @@ -10509,9 +10526,9 @@ snapshots: webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.96.1) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.96.1) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.96.1) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.5 @@ -10566,7 +10583,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.9.1)(esbuild@0.24.0)(webpack@5.96.1) + terser-webpack-plugin: 5.3.10(@swc/core@1.9.1)(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.9.1)(esbuild@0.24.0)(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index cbc5889fb8361..784019c6574cf 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -1,3 +1,4 @@ +import slug from 'slug'; import type { QuickPick } from 'vscode'; import { Uri } from 'vscode'; import type { @@ -147,7 +148,7 @@ export class StartWorkCommand extends QuickCommand { state: { subcommand: 'create', repo: undefined, - name: `${state.item.item.issue.id}-${state.item.item.issue.title}`, + name: slug(`${state.item.item.issue.id}-${state.item.item.issue.title}`), suggestNameOnly: true, }, }, From f104e5b89be6b2354d76325e683114a94dcb687b Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 7 Nov 2024 14:52:44 +0100 Subject: [PATCH 09/17] Fixes unit tests by uncoupling dependencies: It has been broken by adding 'slug' dependency that pulled to the test assembly through Container, so we need to uncouple. (#3621, #3698) --- ...uest.test.ts => pullRequest.utils.test.ts} | 2 +- src/git/models/pullRequest.ts | 40 +----------------- src/git/models/pullRequest.utils.ts | 42 +++++++++++++++++++ src/plus/launchpad/launchpad.ts | 6 +-- src/plus/launchpad/launchpadProvider.ts | 2 +- 5 files changed, 47 insertions(+), 45 deletions(-) rename src/git/models/__tests__/{pullRequest.test.ts => pullRequest.utils.test.ts} (99%) create mode 100644 src/git/models/pullRequest.utils.ts diff --git a/src/git/models/__tests__/pullRequest.test.ts b/src/git/models/__tests__/pullRequest.utils.test.ts similarity index 99% rename from src/git/models/__tests__/pullRequest.test.ts rename to src/git/models/__tests__/pullRequest.utils.test.ts index a2da2e0606edb..adeaec4f240b8 100644 --- a/src/git/models/__tests__/pullRequest.test.ts +++ b/src/git/models/__tests__/pullRequest.utils.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import { suite, test } from 'mocha'; -import { getPullRequestIdentityValuesFromSearch } from '../pullRequest'; +import { getPullRequestIdentityValuesFromSearch } from '../pullRequest.utils'; suite('Test GitHub PR URL parsing to identity: getPullRequestIdentityValuesFromSearch()', () => { function t(message: string, query: string, prNumber: string | undefined, ownerAndRepo?: string) { diff --git a/src/git/models/pullRequest.ts b/src/git/models/pullRequest.ts index 637f9eb14b52b..06caee34082fe 100644 --- a/src/git/models/pullRequest.ts +++ b/src/git/models/pullRequest.ts @@ -7,6 +7,7 @@ import { formatDate, fromNow } from '../../system/date'; import { memoize } from '../../system/decorators/memoize'; import type { LeftRightCommitCountResult } from '../gitProvider'; import type { IssueOrPullRequest, IssueRepository, IssueOrPullRequestState as PullRequestState } from './issue'; +import type { PullRequestURLIdentity } from './pullRequest.utils'; import { createRevisionRange, shortenRevision } from './reference'; import type { ProviderReference } from './remoteProvider'; import type { Repository } from './repository'; @@ -417,45 +418,6 @@ export async function getOpenedPullRequestRepo( return repo; } -export type PullRequestURLIdentity = { - ownerAndRepo?: string; - prNumber?: string; -}; - -export function getPullRequestIdentityValuesFromSearch(search: string): PullRequestURLIdentity { - let ownerAndRepo: string | undefined = undefined; - let prNumber: string | undefined = undefined; - - let match = search.match(/([^/]+\/[^/]+)\/pull\/(\d+)/); // with org and rep name - if (match != null) { - ownerAndRepo = match[1]; - prNumber = match[2]; - } - - if (prNumber == null) { - match = search.match(/(?:\/|^)pull\/(\d+)/); // without repo name - if (match != null) { - prNumber = match[1]; - } - } - - if (prNumber == null) { - match = search.match(/(?:\/)(\d+)/); // any number starting with "/" - if (match != null) { - prNumber = match[1]; - } - } - - if (prNumber == null) { - match = search.match(/^#?(\d+)$/); // just a number or with a leading "#" - if (match != null) { - prNumber = match[1]; - } - } - - return { ownerAndRepo: ownerAndRepo, prNumber: prNumber }; -} - export function doesPullRequestSatisfyRepositoryURLIdentity( pr: EnrichablePullRequest | undefined, { ownerAndRepo, prNumber }: PullRequestURLIdentity, diff --git a/src/git/models/pullRequest.utils.ts b/src/git/models/pullRequest.utils.ts new file mode 100644 index 0000000000000..0f6bf01d819b3 --- /dev/null +++ b/src/git/models/pullRequest.utils.ts @@ -0,0 +1,42 @@ +// pullRequest.ts pulls many dependencies through Container and some of them break the unit tests. +// To avoid this file has been created that can collect more simple functions which +// don't require Container and can be tested. + +export type PullRequestURLIdentity = { + ownerAndRepo?: string; + prNumber?: string; +}; + +export function getPullRequestIdentityValuesFromSearch(search: string): PullRequestURLIdentity { + let ownerAndRepo: string | undefined = undefined; + let prNumber: string | undefined = undefined; + + let match = search.match(/([^/]+\/[^/]+)\/pull\/(\d+)/); // with org and rep name + if (match != null) { + ownerAndRepo = match[1]; + prNumber = match[2]; + } + + if (prNumber == null) { + match = search.match(/(?:\/|^)pull\/(\d+)/); // without repo name + if (match != null) { + prNumber = match[1]; + } + } + + if (prNumber == null) { + match = search.match(/(?:\/)(\d+)/); // any number starting with "/" + if (match != null) { + prNumber = match[1]; + } + } + + if (prNumber == null) { + match = search.match(/^#?(\d+)$/); // just a number or with a leading "#" + if (match != null) { + prNumber = match[1]; + } + } + + return { ownerAndRepo: ownerAndRepo, prNumber: prNumber }; +} diff --git a/src/plus/launchpad/launchpad.ts b/src/plus/launchpad/launchpad.ts index f4751e7505aa7..25115f0079320 100644 --- a/src/plus/launchpad/launchpad.ts +++ b/src/plus/launchpad/launchpad.ts @@ -42,10 +42,8 @@ import { HostingIntegrationId, SelfHostedIntegrationId } from '../../constants.i import type { LaunchpadTelemetryContext, Source, Sources, TelemetryEvents } from '../../constants.telemetry'; import type { Container } from '../../container'; import { PlusFeatures } from '../../features'; -import { - doesPullRequestSatisfyRepositoryURLIdentity, - getPullRequestIdentityValuesFromSearch, -} from '../../git/models/pullRequest'; +import { doesPullRequestSatisfyRepositoryURLIdentity } from '../../git/models/pullRequest'; +import { getPullRequestIdentityValuesFromSearch } from '../../git/models/pullRequest.utils'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickItemOfT, createQuickPickSeparator } from '../../quickpicks/items/common'; import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive'; diff --git a/src/plus/launchpad/launchpadProvider.ts b/src/plus/launchpad/launchpadProvider.ts index ca58018f53c96..cdabd88c22db3 100644 --- a/src/plus/launchpad/launchpadProvider.ts +++ b/src/plus/launchpad/launchpadProvider.ts @@ -19,9 +19,9 @@ import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequ import { getComparisonRefsForPullRequest, getOrOpenPullRequestRepository, - getPullRequestIdentityValuesFromSearch, getRepositoryIdentityForPullRequest, } from '../../git/models/pullRequest'; +import { getPullRequestIdentityValuesFromSearch } from '../../git/models/pullRequest.utils'; import type { GitRemote } from '../../git/models/remote'; import type { Repository } from '../../git/models/repository'; import type { CodeSuggestionCounts, Draft } from '../../gk/models/drafts'; From 16eaf2e809d12fd82a92df27cf1c4376d7902d3b Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 25 Oct 2024 12:54:17 +0200 Subject: [PATCH 10/17] Substitutes repository of the selected issue (#3621, #3698) --- src/commands/git/branch.ts | 16 ++++- src/git/models/issue.ts | 72 ++++++++++++++++++- .../integrations/providers/github/github.ts | 1 + .../integrations/providers/github/models.ts | 2 + src/plus/startWork/startWork.ts | 9 ++- 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/commands/git/branch.ts b/src/commands/git/branch.ts index 9cd2e93b5a417..d46aed1d84190 100644 --- a/src/commands/git/branch.ts +++ b/src/commands/git/branch.ts @@ -63,6 +63,11 @@ interface CreateState { flags: CreateFlags[]; suggestNameOnly?: boolean; + suggestRepoOnly?: boolean; +} + +function isCreateState(state: Partial | undefined): state is Partial { + return state?.subcommand === 'create'; } type DeleteFlags = '--force' | '--remotes'; @@ -172,6 +177,10 @@ export class BranchGitCommand extends QuickCommand { counter++; } + if (args.state.suggestRepoOnly && args.state.repo != null) { + counter--; + } + break; case 'delete': case 'prune': @@ -251,7 +260,12 @@ export class BranchGitCommand extends QuickCommand { state.subcommand, ); - if (state.counter < 2 || state.repo == null || typeof state.repo === 'string') { + if ( + state.counter < 2 || + state.repo == null || + typeof state.repo === 'string' || + (isCreateState(state) && state.suggestRepoOnly) + ) { skippedStepTwo = false; if (context.repos.length === 1) { skippedStepTwo = true; diff --git a/src/git/models/issue.ts b/src/git/models/issue.ts index a4fc475ffd1f4..9359d98268187 100644 --- a/src/git/models/issue.ts +++ b/src/git/models/issue.ts @@ -1,6 +1,10 @@ -import { ColorThemeKind, ThemeColor, ThemeIcon, window } from 'vscode'; +import { ColorThemeKind, ThemeColor, ThemeIcon, Uri, window } from 'vscode'; +import { Schemes } from '../../constants'; import type { Colors } from '../../constants.colors'; +import type { Container } from '../../container'; +import type { RepositoryIdentityDescriptor } from '../../gk/models/repositoryIdentities'; import type { ProviderReference } from './remoteProvider'; +import type { Repository } from './repository'; export type IssueOrPullRequestType = 'issue' | 'pullrequest'; export type IssueOrPullRequestState = 'opened' | 'closed' | 'merged'; @@ -45,6 +49,7 @@ export interface IssueRepository { owner: string; repo: string; accessLevel?: RepositoryAccessLevel; + url?: string; } export interface IssueShape extends IssueOrPullRequest { @@ -217,6 +222,7 @@ export function serializeIssue(value: IssueShape): IssueShape { : { owner: value.repository.owner, repo: value.repository.repo, + url: value.repository.url, }, assignees: value.assignees.map(assignee => ({ id: assignee.id, @@ -261,3 +267,67 @@ export class Issue implements IssueShape { public readonly body?: string, ) {} } + +export type IssueRepositoryIdentityDescriptor = RequireSomeWithProps< + RequireSome, 'provider'>, + 'provider', + 'id' | 'domain' | 'repoDomain' | 'repoName' +> & + RequireSomeWithProps, 'remote'>, 'remote', 'domain'>; + +export function getRepositoryIdentityForIssue(issue: IssueShape | Issue): IssueRepositoryIdentityDescriptor { + if (issue.repository == null) throw new Error('Missing repository'); + + return { + remote: { + url: issue.repository.url, + domain: issue.provider.domain, + }, + name: `${issue.repository.owner}/${issue.repository.repo}`, + provider: { + id: issue.provider.id, + domain: issue.provider.domain, + repoDomain: issue.repository.owner, + repoName: issue.repository.repo, + repoOwnerDomain: issue.repository.owner, + }, + }; +} + +export function getVirtualUriForIssue(issue: IssueShape | Issue): Uri | undefined { + if (issue.repository == null) throw new Error('Missing repository'); + if (issue.provider.id !== 'github') return undefined; + + const uri = Uri.parse(issue.repository.url ?? issue.url); + return uri.with({ scheme: Schemes.Virtual, authority: 'github', path: uri.path }); +} + +export async function getOrOpenIssueRepository( + container: Container, + issue: IssueShape | Issue, + options?: { promptIfNeeded?: boolean; skipVirtual?: boolean }, +): Promise { + const identity = getRepositoryIdentityForIssue(issue); + let repo = await container.repositoryIdentity.getRepository(identity, { + openIfNeeded: true, + keepOpen: false, + prompt: false, + }); + + if (repo == null && !options?.skipVirtual) { + const virtualUri = getVirtualUriForIssue(issue); + if (virtualUri != null) { + repo = await container.git.getOrOpenRepository(virtualUri, { closeOnOpen: true, detectNested: false }); + } + } + + if (repo == null && options?.promptIfNeeded) { + repo = await container.repositoryIdentity.getRepository(identity, { + openIfNeeded: true, + keepOpen: false, + prompt: true, + }); + } + + return repo; +} diff --git a/src/plus/integrations/providers/github/github.ts b/src/plus/integrations/providers/github/github.ts index 6746a22afc44e..7e1d867a8bab4 100644 --- a/src/plus/integrations/providers/github/github.ts +++ b/src/plus/integrations/providers/github/github.ts @@ -190,6 +190,7 @@ repository { login } viewerPermission + url } `; diff --git a/src/plus/integrations/providers/github/models.ts b/src/plus/integrations/providers/github/models.ts index 569bd521fdcfd..47017a55c1f5f 100644 --- a/src/plus/integrations/providers/github/models.ts +++ b/src/plus/integrations/providers/github/models.ts @@ -133,6 +133,7 @@ export interface GitHubIssue extends Omit ({ id: assignee.login, diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 784019c6574cf..43d158db51f94 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -24,6 +24,7 @@ import { HostingIntegrationId } from '../../constants.integrations'; import type { Source, Sources, StartWorkTelemetryContext } from '../../constants.telemetry'; import type { Container } from '../../container'; import type { SearchedIssue } from '../../git/models/issue'; +import { getOrOpenIssueRepository } from '../../git/models/issue'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickItemOfT } from '../../quickpicks/items/common'; import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive'; @@ -138,6 +139,9 @@ export class StartWorkCommand extends QuickCommand { assertsStartWorkStepState(state); state.action = 'start'; + const issue = state.item.item.issue; + const repo = await getOrOpenIssueRepository(this.container, issue); + if (typeof state.action === 'string') { switch (state.action) { case 'start': @@ -147,9 +151,10 @@ export class StartWorkCommand extends QuickCommand { command: 'branch', state: { subcommand: 'create', - repo: undefined, - name: slug(`${state.item.item.issue.id}-${state.item.item.issue.title}`), + repo: repo, + name: slug(`${issue.id}-${issue.title}`), suggestNameOnly: true, + suggestRepoOnly: true, }, }, this.pickedVia, From 85791856d8404b9b92f9c73b6ae772e8cfb53f9b Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 7 Nov 2024 17:41:22 +0100 Subject: [PATCH 11/17] Adds a start work action step that lets user to choose whether they want just to create a branch or start working on an issue (#3621, #3698) --- src/plus/startWork/startWork.ts | 55 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 43d158db51f94..e9951d364094f 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -52,8 +52,6 @@ interface State { action?: StartWorkAction; } -type StartWorkStepState = RequireSome, 'item'>; - export type StartWorkAction = 'start'; export interface StartWorkCommandArgs { @@ -61,13 +59,6 @@ export interface StartWorkCommandArgs { source?: Sources; } -function assertsStartWorkStepState(state: StepState): asserts state is StartWorkStepState { - if (state.item != null) return; - - debugger; - throw new Error('Missing item'); -} - export const supportedStartWorkIntegrations = [HostingIntegrationId.GitHub]; const instanceCounter = getScopedCounter(); @@ -128,47 +119,69 @@ export class StartWorkCommand extends QuickCommand { } } - await updateContextItems(this.container, context); + if (state.counter < 1) { + const result = yield* this.selectCommandStep(state); + if (result === StepResultBreak) continue; + state.action = result.action; + } - if (state.counter < 1 || state.item == null) { + if (state.counter < 2 && !state.action) { + await updateContextItems(this.container, context); const result = yield* this.pickIssueStep(state, context); if (result === StepResultBreak) continue; state.item = result; + state.action = 'start'; } - assertsStartWorkStepState(state); - state.action = 'start'; - - const issue = state.item.item.issue; - const repo = await getOrOpenIssueRepository(this.container, issue); + const issue = state.item?.item?.issue; + const repo = issue && (await getOrOpenIssueRepository(this.container, issue)); if (typeof state.action === 'string') { switch (state.action) { - case 'start': - yield* getSteps( + case 'start': { + const result = yield* getSteps( this.container, { command: 'branch', state: { subcommand: 'create', repo: repo, - name: slug(`${issue.id}-${issue.title}`), + name: issue ? slug(`${issue.id}-${issue.title}`) : undefined, suggestNameOnly: true, suggestRepoOnly: true, }, }, this.pickedVia, ); + if (result === StepResultBreak) { + endSteps(state); + } else { + state.counter--; + state.action = undefined; + } break; + } } } - - endSteps(state); } return state.counter < 0 ? StepResultBreak : undefined; } + private *selectCommandStep(state: StepState): StepResultGenerator<{ action?: StartWorkAction }> { + const step = createPickStep({ + placeholder: 'Start work by creating a new branch', + items: [ + createQuickPickItemOfT('Create a Branch', { + action: 'start', + }), + createQuickPickItemOfT('Create a Branch from an Issue', {}), + ], + }); + const selection: StepSelection = yield step; + return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak; + } + private async *confirmLocalIntegrationConnectStep( state: StepState, context: Context, From 47f13c846be263155cb394bc5ba3a37a28e1eef9 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 7 Nov 2024 17:51:32 +0100 Subject: [PATCH 12/17] Selects if user wants to create a branch on a worktree or not (#3621, #3698) --- src/commands/git/branch.ts | 2 +- src/plus/startWork/startWork.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/commands/git/branch.ts b/src/commands/git/branch.ts index d46aed1d84190..c70f675cf9caa 100644 --- a/src/commands/git/branch.ts +++ b/src/commands/git/branch.ts @@ -373,7 +373,7 @@ export class BranchGitCommand extends QuickCommand { state.reference = result; } - if (state.counter < 4 || state.name == null) { + if (state.counter < 4 || state.name == null || state.suggestNameOnly) { const result = yield* inputBranchNameStep(state, context, { titleContext: ` from ${getReferenceLabel(state.reference, { capitalize: true, diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index e9951d364094f..bff6e0edc2469 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -50,6 +50,7 @@ interface Context { interface State { item?: StartWorkItem; action?: StartWorkAction; + inWorktree?: boolean; } export type StartWorkAction = 'start'; @@ -123,6 +124,7 @@ export class StartWorkCommand extends QuickCommand { const result = yield* this.selectCommandStep(state); if (result === StepResultBreak) continue; state.action = result.action; + state.inWorktree = result.inWorktree; } if (state.counter < 2 && !state.action) { @@ -149,7 +151,9 @@ export class StartWorkCommand extends QuickCommand { name: issue ? slug(`${issue.id}-${issue.title}`) : undefined, suggestNameOnly: true, suggestRepoOnly: true, + flags: state.inWorktree ? ['--worktree'] : ['--switch'], }, + confirm: false, }, this.pickedVia, ); @@ -168,14 +172,18 @@ export class StartWorkCommand extends QuickCommand { return state.counter < 0 ? StepResultBreak : undefined; } - private *selectCommandStep(state: StepState): StepResultGenerator<{ action?: StartWorkAction }> { + private *selectCommandStep( + state: StepState, + ): StepResultGenerator<{ action?: StartWorkAction; inWorktree?: boolean }> { const step = createPickStep({ placeholder: 'Start work by creating a new branch', items: [ createQuickPickItemOfT('Create a Branch', { action: 'start', }), + createQuickPickItemOfT('Create a Branch in a Worktree', { action: 'start', inWorktree: true }), createQuickPickItemOfT('Create a Branch from an Issue', {}), + createQuickPickItemOfT('Create a Branch from an Issue in a Worktree', { inWorktree: true }), ], }); const selection: StepSelection = yield step; From 1c6daf70e9fc66c7b9d4475ad4271493546acf7f Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Wed, 23 Oct 2024 02:18:04 +0200 Subject: [PATCH 13/17] Fixes a message when no issues found (#3621, #3698) --- src/plus/startWork/startWork.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index bff6e0edc2469..a1fd082e1a14e 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -131,8 +131,12 @@ export class StartWorkCommand extends QuickCommand { await updateContextItems(this.container, context); const result = yield* this.pickIssueStep(state, context); if (result === StepResultBreak) continue; - state.item = result; - state.action = 'start'; + if (typeof result !== 'string') { + state.item = result; + state.action = 'start'; + } else { + state.action = result; + } } const issue = state.item?.item?.issue; @@ -315,7 +319,10 @@ export class StartWorkCommand extends QuickCommand { return StepResultBreak; } - private *pickIssueStep(state: StepState, context: Context): StepResultGenerator { + private *pickIssueStep( + state: StepState, + context: Context, + ): StepResultGenerator { const buildIssueItem = (i: StartWorkItem) => { return { label: @@ -341,11 +348,19 @@ export class StartWorkCommand extends QuickCommand { return items; }; - function getItemsAndPlaceholder() { + function getItemsAndPlaceholder(): { + placeholder: string; + items: QuickPickItemOfT[]; + } { if (!context.result.items.length) { return { - placeholder: 'All done! Take a vacation', - items: [createDirectiveQuickPickItem(Directive.Cancel, undefined, { label: 'OK' })], + placeholder: 'No issues found. Start work anyway.', + items: [ + createQuickPickItemOfT( + state.inWorktree ? 'Create a branch on a worktree' : 'Create a branch', + 'start', + ), + ], }; } @@ -357,7 +372,7 @@ export class StartWorkCommand extends QuickCommand { const { items, placeholder } = getItemsAndPlaceholder(); - const step = createPickStep({ + const step = createPickStep<(typeof items)[0]>({ title: context.title, placeholder: placeholder, matchOnDescription: true, @@ -370,7 +385,7 @@ export class StartWorkCommand extends QuickCommand { return StepResultBreak; } const element = selection[0]; - return { ...element.item }; + return typeof element.item === 'string' ? element.item : { ...element.item }; } private startWork(state: PartialStepState, item?: StartWorkItem) { From 9079be6e6a3b2d34c57dd9d3f41727afb4a8cfea Mon Sep 17 00:00:00 2001 From: Chivorotkiv Date: Fri, 8 Nov 2024 20:22:29 +0100 Subject: [PATCH 14/17] Adds a QuickInputButton that lets user to open an issue on web (#3621, #3698) --- src/plus/startWork/startWork.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index a1fd082e1a14e..8c1fe5bedd0a2 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -17,6 +17,7 @@ import { QuickCommand, StepResultBreak, } from '../../commands/quickCommand'; +import { OpenOnGitHubQuickInputButton } from '../../commands/quickCommand.buttons'; import { getSteps } from '../../commands/quickWizard.utils'; import { proBadge } from '../../constants'; import type { IntegrationId } from '../../constants.integrations'; @@ -33,6 +34,7 @@ import { getScopedCounter } from '../../system/counter'; import { fromNow } from '../../system/date'; import { some } from '../../system/iterable'; import { configuration } from '../../system/vscode/configuration'; +import { openUrl } from '../../system/vscode/utils'; export type StartWorkItem = { item: SearchedIssue; @@ -324,6 +326,7 @@ export class StartWorkCommand extends QuickCommand { context: Context, ): StepResultGenerator { const buildIssueItem = (i: StartWorkItem) => { + const buttons = i.item.issue.url ? [OpenOnGitHubQuickInputButton] : []; return { label: i.item.issue.title.length > 60 ? `${i.item.issue.title.substring(0, 60)}...` : i.item.issue.title, @@ -335,6 +338,7 @@ export class StartWorkCommand extends QuickCommand { iconPath: i.item.issue.author?.avatarUrl != null ? Uri.parse(i.item.issue.author.avatarUrl) : undefined, item: i, picked: i.item.issue.id === state.item?.item?.issue.id, + buttons: buttons, }; }; @@ -378,6 +382,13 @@ export class StartWorkCommand extends QuickCommand { matchOnDescription: true, matchOnDetail: true, items: items, + onDidClickItemButton: (_quickpick, button, { item }) => { + if (button === OpenOnGitHubQuickInputButton && typeof item !== 'string') { + this.open(item); + return true; + } + return false; + }, }); const selection: StepSelection = yield step; @@ -395,6 +406,11 @@ export class StartWorkCommand extends QuickCommand { } } + private open(item: StartWorkItem): void { + if (item.item.issue.url == null) return; + void openUrl(item.item.issue.url); + } + private async getConnectedIntegrations(): Promise> { const connected = new Map(); await Promise.allSettled( From 420fde80103c72f522e8a72ca5b2bbb79dcb21ca Mon Sep 17 00:00:00 2001 From: Chivorotkiv Date: Fri, 8 Nov 2024 20:54:28 +0100 Subject: [PATCH 15/17] Supports Jira issues in Start Work flow (#3621, #3698) --- src/plus/integrations/integrationService.ts | 2 +- src/plus/startWork/startWork.ts | 38 +++++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index 2f29ceeef016d..9c7774e9feebf 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -554,7 +554,7 @@ export class IntegrationService implements Disposable { args: { 0: integrationIds => (integrationIds?.length ? integrationIds.join(',') : ''), 1: false }, }) async getMyIssues( - integrationIds?: HostingIntegrationId[], + integrationIds?: (SupportedHostingIntegrationIds | SupportedIssueIntegrationIds)[], cancellation?: CancellationToken, ): Promise { const integrations: Map = new Map(); diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 8c1fe5bedd0a2..2e8887f2eec5f 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -21,11 +21,12 @@ import { OpenOnGitHubQuickInputButton } from '../../commands/quickCommand.button import { getSteps } from '../../commands/quickWizard.utils'; import { proBadge } from '../../constants'; import type { IntegrationId } from '../../constants.integrations'; -import { HostingIntegrationId } from '../../constants.integrations'; +import { HostingIntegrationId, IssueIntegrationId } from '../../constants.integrations'; import type { Source, Sources, StartWorkTelemetryContext } from '../../constants.telemetry'; import type { Container } from '../../container'; -import type { SearchedIssue } from '../../git/models/issue'; +import type { Issue, IssueShape, SearchedIssue } from '../../git/models/issue'; import { getOrOpenIssueRepository } from '../../git/models/issue'; +import type { Repository } from '../../git/models/repository'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickItemOfT } from '../../quickpicks/items/common'; import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive'; @@ -46,7 +47,7 @@ interface Context { result: StartWorkResult; title: string; telemetryContext: StartWorkTelemetryContext | undefined; - connectedIntegrations: Map; + connectedIntegrations: Map; } interface State { @@ -62,7 +63,8 @@ export interface StartWorkCommandArgs { source?: Sources; } -export const supportedStartWorkIntegrations = [HostingIntegrationId.GitHub]; +export const supportedStartWorkIntegrations = [HostingIntegrationId.GitHub, IssueIntegrationId.Jira]; +export type SupportedStartWorkIntegrationIds = (typeof supportedStartWorkIntegrations)[number]; const instanceCounter = getScopedCounter(); export class StartWorkCommand extends QuickCommand { @@ -142,7 +144,7 @@ export class StartWorkCommand extends QuickCommand { } const issue = state.item?.item?.issue; - const repo = issue && (await getOrOpenIssueRepository(this.container, issue)); + const repo = issue && (await this.getIssueRepositoryIfExists(issue)); if (typeof state.action === 'string') { switch (state.action) { @@ -154,7 +156,9 @@ export class StartWorkCommand extends QuickCommand { state: { subcommand: 'create', repo: repo, - name: issue ? slug(`${issue.id}-${issue.title}`) : undefined, + name: issue + ? `${slug(issue.id, { lower: false })}-${slug(issue.title)}` + : undefined, suggestNameOnly: true, suggestRepoOnly: true, flags: state.inWorktree ? ['--worktree'] : ['--switch'], @@ -196,6 +200,14 @@ export class StartWorkCommand extends QuickCommand { return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak; } + private async getIssueRepositoryIfExists(issue: IssueShape | Issue): Promise { + try { + return await getOrOpenIssueRepository(this.container, issue); + } catch { + return undefined; + } + } + private async *confirmLocalIntegrationConnectStep( state: StepState, context: Context, @@ -411,12 +423,14 @@ export class StartWorkCommand extends QuickCommand { void openUrl(item.item.issue.url); } - private async getConnectedIntegrations(): Promise> { - const connected = new Map(); + private async getConnectedIntegrations(): Promise> { + const connected = new Map(); await Promise.allSettled( supportedStartWorkIntegrations.map(async integrationId => { const integration = await this.container.integrations.get(integrationId); - connected.set(integrationId, integration.maybeConnected ?? (await integration.isConnected())); + const isConnected = integration.maybeConnected ?? (await integration.isConnected()); + const hasAccess = isConnected && (await integration.access()); + connected.set(integrationId, hasAccess); }), ); @@ -425,9 +439,13 @@ export class StartWorkCommand extends QuickCommand { } async function updateContextItems(container: Container, context: Context) { + const connectedIntegrationsMap = context.connectedIntegrations; + const connectedIntegrations = [...connectedIntegrationsMap.keys()].filter(integrationId => + Boolean(connectedIntegrationsMap.get(integrationId)), + ); context.result = { items: - (await container.integrations.getMyIssues([HostingIntegrationId.GitHub]))?.map(i => ({ + (await container.integrations.getMyIssues(connectedIntegrations))?.map(i => ({ item: i, })) ?? [], }; From 22a38b72624f502bf673f3d92e16aeedd5ec8b76 Mon Sep 17 00:00:00 2001 From: Chivorotkiv Date: Tue, 12 Nov 2024 19:16:12 +0100 Subject: [PATCH 16/17] Shows "Connect integration" only if we selected creating from a branch. If user chooses to create a branch without an issue it doesn't require connection. (#3621, #3698) --- src/plus/startWork/startWork.ts | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 2e8887f2eec5f..f7bb303693336 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -103,27 +103,6 @@ export class StartWorkCommand extends QuickCommand { while (this.canStepsContinue(state)) { context.title = this.title; - const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c); - if (!hasConnectedIntegrations) { - if (this.container.telemetry.enabled) { - this.container.telemetry.sendEvent( - opened ? 'startWork/steps/connect' : 'startWork/opened', - { - ...context.telemetryContext!, - connected: false, - }, - this.source, - ); - } - const isUsingCloudIntegrations = configuration.get('cloudIntegrations.enabled', undefined, false); - const result = isUsingCloudIntegrations - ? yield* this.confirmCloudIntegrationsConnectStep(state, context) - : yield* this.confirmLocalIntegrationConnectStep(state, context); - if (result === StepResultBreak) { - return result; - } - } - if (state.counter < 1) { const result = yield* this.selectCommandStep(state); if (result === StepResultBreak) continue; @@ -132,6 +111,27 @@ export class StartWorkCommand extends QuickCommand { } if (state.counter < 2 && !state.action) { + const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c); + if (!hasConnectedIntegrations) { + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent( + opened ? 'startWork/steps/connect' : 'startWork/opened', + { + ...context.telemetryContext!, + connected: false, + }, + this.source, + ); + } + const isUsingCloudIntegrations = configuration.get('cloudIntegrations.enabled', undefined, false); + const result = isUsingCloudIntegrations + ? yield* this.confirmCloudIntegrationsConnectStep(state, context) + : yield* this.confirmLocalIntegrationConnectStep(state, context); + if (result === StepResultBreak) { + return result; + } + } + await updateContextItems(this.container, context); const result = yield* this.pickIssueStep(state, context); if (result === StepResultBreak) continue; From 2b9948190c6b627f98091eeab1699c6cddf719ba Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 12 Nov 2024 12:24:18 -0700 Subject: [PATCH 17/17] Restricts to open repos when they exist --- src/plus/integrations/integrationService.ts | 51 +++++++++++++++++++-- src/plus/integrations/providers/models.ts | 9 ++++ src/plus/startWork/startWork.ts | 8 ++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index 9c7774e9feebf..9afd51586bf4f 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -42,7 +42,7 @@ import type { SupportedIssueIntegrationIds, SupportedSelfHostedIntegrationIds, } from './integration'; -import { isSelfHostedIntegrationId } from './providers/models'; +import { isHostingIntegrationId, isSelfHostedIntegrationId } from './providers/models'; import type { ProvidersApi } from './providers/providersApi'; export interface ConnectionStateChangeEvent { @@ -555,18 +555,59 @@ export class IntegrationService implements Disposable { }) async getMyIssues( integrationIds?: (SupportedHostingIntegrationIds | SupportedIssueIntegrationIds)[], - cancellation?: CancellationToken, + options?: { openRepositoriesOnly?: boolean; cancellation?: CancellationToken }, ): Promise { const integrations: Map = new Map(); - for (const integrationId of integrationIds?.length ? integrationIds : Object.values(HostingIntegrationId)) { + const hostingIntegrationIds = integrationIds?.filter( + id => id in HostingIntegrationId, + ) as SupportedHostingIntegrationIds[]; + const openRemotesByIntegrationId = new Map(); + for (const repository of this.container.git.openRepositories) { + const remotes = await repository.git.getRemotes(); + if (remotes.length === 0) continue; + for (const remote of remotes) { + const remoteIntegration = await remote.getIntegration(); + if (remoteIntegration == null) continue; + for (const integrationId of hostingIntegrationIds?.length + ? hostingIntegrationIds + : Object.values(HostingIntegrationId)) { + if ( + remoteIntegration.id === integrationId && + remote.provider?.owner != null && + remote.provider?.repoName != null + ) { + const descriptor = { + key: `${remote.provider.owner}/${remote.provider.repoName}`, + owner: remote.provider.owner, + name: remote.provider.repoName, + }; + if (openRemotesByIntegrationId.has(integrationId)) { + openRemotesByIntegrationId.get(integrationId)?.push(descriptor); + } else { + openRemotesByIntegrationId.set(integrationId, [descriptor]); + } + } + } + } + } + for (const integrationId of integrationIds?.length + ? integrationIds + : [...Object.values(HostingIntegrationId), ...Object.values(IssueIntegrationId)]) { const integration = await this.get(integrationId); if (integration == null) continue; - integrations.set(integration, undefined); + integrations.set( + integration, + options?.openRepositoriesOnly && + isHostingIntegrationId(integrationId) && + openRemotesByIntegrationId.has(integrationId) + ? openRemotesByIntegrationId.get(integrationId) + : undefined, + ); } if (integrations.size === 0) return undefined; - return this.getMyIssuesCore(integrations, cancellation); + return this.getMyIssuesCore(integrations, options?.cancellation); } private async getMyIssuesCore( diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts index bd3ce2858201a..47160355aa4eb 100644 --- a/src/plus/integrations/providers/models.ts +++ b/src/plus/integrations/providers/models.ts @@ -90,6 +90,15 @@ export function isSelfHostedIntegrationId(id: IntegrationId): id is SelfHostedIn return selfHostedIntegrationIds.includes(id as SelfHostedIntegrationId); } +export function isHostingIntegrationId(id: IntegrationId): id is HostingIntegrationId { + return [ + HostingIntegrationId.GitHub, + HostingIntegrationId.GitLab, + HostingIntegrationId.Bitbucket, + HostingIntegrationId.AzureDevOps, + ].includes(id as HostingIntegrationId); +} + export enum PullRequestFilter { Author = 'author', Assignee = 'assignee', diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index f7bb303693336..3dd27b2a362c8 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -445,8 +445,10 @@ async function updateContextItems(container: Container, context: Context) { ); context.result = { items: - (await container.integrations.getMyIssues(connectedIntegrations))?.map(i => ({ - item: i, - })) ?? [], + (await container.integrations.getMyIssues(connectedIntegrations, { openRepositoriesOnly: true }))?.map( + i => ({ + item: i, + }), + ) ?? [], }; }