From 0a0db1fc38f28e9a23687909b11b61ab083abaa1 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 5 Feb 2023 14:18:48 -0500 Subject: [PATCH] Adds remote command to Git Command Palette --- CHANGELOG.md | 1 + package.json | 8 +- src/commands/git/remote.ts | 461 ++++++++++++++++++++++ src/commands/gitCommands.actions.ts | 82 ++-- src/commands/gitCommands.ts | 23 +- src/commands/gitCommands.utils.ts | 2 + src/commands/quickCommand.steps.ts | 261 +++++++++++-- src/env/node/git/git.ts | 16 +- src/env/node/git/localGitProvider.ts | 10 +- src/git/gitProvider.ts | 5 +- src/git/gitProviderService.ts | 28 +- src/git/models/repository.ts | 17 + src/plus/github/githubGitProvider.ts | 7 +- src/quickpicks/commitPicker.ts | 7 +- src/quickpicks/items/gitCommands.ts | 722 ++++++++++++++++++----------------- src/quickpicks/referencePicker.ts | 8 +- src/quickpicks/repositoryPicker.ts | 5 +- src/views/viewCommands.ts | 19 +- 18 files changed, 1211 insertions(+), 471 deletions(-) create mode 100644 src/commands/git/remote.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ed445..dbadeb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Adds a `worktrees.openAfterCreate` setting to specify how and when to open a worktree after it is created - Creates new worktrees from the "main" repo, if already in a worktree - Shows the _Worktrees_ view after creating a new worktree +- Adds a new _remote_ command to the _Git Command Palette_ to add, prune, and remove remotes ### Changed diff --git a/package.json b/package.json index 712cbf9..b3c0e75 100644 --- a/package.json +++ b/package.json @@ -5582,8 +5582,8 @@ "icon": "$(discard)" }, { - "command": "gitlens.views.terminalRemoveRemote", - "title": "Remove Remote (via Terminal)", + "command": "gitlens.views.removeRemote", + "title": "Remove Remote...", "category": "GitLens" }, { @@ -7928,7 +7928,7 @@ "when": "false" }, { - "command": "gitlens.views.terminalRemoveRemote", + "command": "gitlens.views.removeRemote", "when": "false" }, { @@ -10617,7 +10617,7 @@ "alt": "gitlens.copyRemoteBranchesUrl" }, { - "command": "gitlens.views.terminalRemoveRemote", + "command": "gitlens.views.removeRemote", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:remote\\b/", "group": "6_gitlens_terminal@1" }, diff --git a/src/commands/git/remote.ts b/src/commands/git/remote.ts new file mode 100644 index 0000000..80f2d24 --- /dev/null +++ b/src/commands/git/remote.ts @@ -0,0 +1,461 @@ +import type { QuickPickItem } from 'vscode'; +import { QuickInputButtons } from 'vscode'; +import type { Container } from '../../container'; +import type { GitRemote } from '../../git/models/remote'; +import { Repository } from '../../git/models/repository'; +import { Logger } from '../../logger'; +import { showGenericErrorMessage } from '../../messages'; +import type { QuickPickItemOfT } from '../../quickpicks/items/common'; +import { FlagsQuickPickItem } from '../../quickpicks/items/flags'; +import type { ViewsWithRepositoryFolders } from '../../views/viewBase'; +import { GitActions } from '../gitCommands.actions'; +import type { + AsyncStepResultGenerator, + PartialStepState, + QuickPickStep, + StepGenerator, + StepResultGenerator, + StepSelection, + StepState, +} from '../quickCommand'; +import { + appendReposToTitle, + inputRemoteNameStep, + inputRemoteUrlStep, + pickRemoteStep, + pickRepositoryStep, + QuickCommand, + StepResult, +} from '../quickCommand'; + +interface Context { + repos: Repository[]; + associatedView: ViewsWithRepositoryFolders; + title: string; +} + +type AddFlags = '-f'; + +interface AddState { + subcommand: 'add'; + repo: string | Repository; + name: string; + url: string; + flags: AddFlags[]; +} + +interface RemoveState { + subcommand: 'remove'; + repo: string | Repository; + remote: string | GitRemote; +} + +interface PruneState { + subcommand: 'prune'; + repo: string | Repository; + remote: string | GitRemote; +} + +type State = AddState | RemoveState | PruneState; +type RemoteStepState = SomeNonNullable, 'subcommand'>; + +type AddStepState = RemoteStepState>; +function assertStateStepAdd(state: PartialStepState): asserts state is AddStepState { + if (state.repo instanceof Repository && state.subcommand === 'add') return; + + debugger; + throw new Error('Missing repository'); +} + +type RemoveStepState = RemoteStepState>; +function assertStateStepRemove(state: PartialStepState): asserts state is RemoveStepState { + if (state.repo instanceof Repository && state.subcommand === 'remove') return; + + debugger; + throw new Error('Missing repository'); +} + +type PruneStepState = RemoteStepState>; +function assertStateStepPrune(state: PartialStepState): asserts state is PruneStepState { + if (state.repo instanceof Repository && state.subcommand === 'prune') return; + + debugger; + throw new Error('Missing repository'); +} + +function assertStateStepRemoveRemotes( + state: RemoveStepState, +): asserts state is ExcludeSome { + if (typeof state.remote !== 'string') return; + + debugger; + throw new Error('Missing remote'); +} + +function assertStateStepPruneRemotes( + state: PruneStepState, +): asserts state is ExcludeSome { + if (typeof state.remote !== 'string') return; + + debugger; + throw new Error('Missing remote'); +} + +const subcommandToTitleMap = new Map([ + ['add', 'Add'], + ['prune', 'Prune'], + ['remove', 'Remove'], +]); +function getTitle(title: string, subcommand: State['subcommand'] | undefined) { + return subcommand == null ? title : `${subcommandToTitleMap.get(subcommand)} ${title}`; +} + +export interface RemoteGitCommandArgs { + readonly command: 'remote'; + confirm?: boolean; + state?: Partial; +} + +export class RemoteGitCommand extends QuickCommand { + private subcommand: State['subcommand'] | undefined; + + constructor(container: Container, args?: RemoteGitCommandArgs) { + super(container, 'remote', 'remote', 'Remote', { + description: 'add, prune, or remove remotes', + }); + + let counter = 0; + if (args?.state?.subcommand != null) { + counter++; + + switch (args?.state.subcommand) { + case 'add': + if (args.state.name != null) { + counter++; + } + + if (args.state.url != null) { + counter++; + } + + break; + case 'prune': + case 'remove': + if (args.state.remote != null) { + counter++; + } + + break; + } + } + + if (args?.state?.repo != null) { + counter++; + } + + this.initialState = { + counter: counter, + confirm: args?.confirm, + ...args?.state, + }; + } + + override get canConfirm(): boolean { + return this.subcommand != null; + } + + override get canSkipConfirm(): boolean { + return this.subcommand === 'remove' || this.subcommand === 'prune' ? false : super.canSkipConfirm; + } + + override get skipConfirmKey() { + return `${this.key}${this.subcommand == null ? '' : `-${this.subcommand}`}:${this.pickedVia}`; + } + + protected async *steps(state: PartialStepState): StepGenerator { + const context: Context = { + repos: this.container.git.openRepositories, + associatedView: this.container.remotesView, + title: this.title, + }; + + let skippedStepTwo = false; + + while (this.canStepsContinue(state)) { + context.title = this.title; + + if (state.counter < 1 || state.subcommand == null) { + this.subcommand = undefined; + + const result = yield* this.pickSubcommandStep(state); + // Always break on the first step (so we will go back) + if (result === StepResult.Break) break; + + state.subcommand = result; + } + + this.subcommand = state.subcommand; + + context.title = getTitle(this.title, state.subcommand); + + if (state.counter < 2 || state.repo == null || typeof state.repo === 'string') { + skippedStepTwo = false; + if (context.repos.length === 1) { + skippedStepTwo = true; + state.counter++; + + state.repo = context.repos[0]; + } else { + const result = yield* pickRepositoryStep(state, context); + if (result === StepResult.Break) continue; + + state.repo = result; + } + } + + switch (state.subcommand) { + case 'add': + assertStateStepAdd(state); + yield* this.addCommandSteps(state, context); + // Clear any chosen name, since we are exiting this subcommand + state.name = undefined!; + state.url = undefined!; + break; + case 'prune': + assertStateStepPrune(state); + yield* this.pruneCommandSteps(state, context); + break; + case 'remove': + assertStateStepRemove(state); + yield* this.removeCommandSteps(state, context); + break; + default: + QuickCommand.endSteps(state); + break; + } + + // If we skipped the previous step, make sure we back up past it + if (skippedStepTwo) { + state.counter--; + } + } + + return state.counter < 0 ? StepResult.Break : undefined; + } + + private *pickSubcommandStep(state: PartialStepState): StepResultGenerator { + const step = QuickCommand.createPickStep>({ + title: this.title, + placeholder: `Choose a ${this.label} command`, + items: [ + { + label: 'add', + description: 'adds a new remote', + picked: state.subcommand === 'add', + item: 'add', + }, + { + label: 'prune', + description: 'prunes remote branches on the specified remote', + picked: state.subcommand === 'prune', + item: 'prune', + }, + { + label: 'remove', + description: 'removes the specified remote', + picked: state.subcommand === 'remove', + item: 'remove', + }, + ], + buttons: [QuickInputButtons.Back], + }); + const selection: StepSelection = yield step; + return QuickCommand.canPickStepContinue(step, state, selection) ? selection[0].item : StepResult.Break; + } + + private async *addCommandSteps(state: AddStepState, context: Context): AsyncStepResultGenerator { + if (state.flags == null) { + state.flags = ['-f']; + } + + let alreadyExists = (await state.repo.getRemotes({ filter: r => r.name === state.name })).length !== 0; + + while (this.canStepsContinue(state)) { + if (state.counter < 3 || state.name == null || alreadyExists) { + const result = yield* inputRemoteNameStep(state, context, { + placeholder: 'Please provide a name for the remote', + value: state.name, + }); + if (result === StepResult.Break) continue; + + alreadyExists = (await state.repo.getRemotes({ filter: r => r.name === result })).length !== 0; + if (alreadyExists) { + state.counter--; + continue; + } + + state.name = result; + } + + if (state.counter < 4 || state.url == null) { + const result = yield* inputRemoteUrlStep(state, context, { + placeholder: 'Please provide a URL for the remote', + value: state.url, + }); + if (result === StepResult.Break) continue; + + state.url = result; + } + + if (this.confirm(state.confirm)) { + const result = yield* this.addCommandConfirmStep(state, context); + if (result === StepResult.Break) continue; + + state.flags = result; + } + + QuickCommand.endSteps(state); + const remote = await state.repo.addRemote( + state.name, + state.url, + state.flags.includes('-f') ? { fetch: true } : undefined, + ); + queueMicrotask( + () => + void GitActions.Remote.reveal(remote, { + focus: true, + select: true, + }), + ); + } + } + + private *addCommandConfirmStep(state: AddStepState, context: Context): StepResultGenerator { + const step: QuickPickStep> = QuickCommand.createConfirmStep( + appendReposToTitle(`Confirm ${context.title}`, state, context), + [ + FlagsQuickPickItem.create(state.flags, [], { + label: context.title, + detail: `Will add remote '${state.name}' for ${state.url}`, + }), + FlagsQuickPickItem.create(state.flags, ['-f'], { + label: `${context.title} and Fetch`, + description: '-f', + detail: `Will add and fetch remote '${state.name}' for ${state.url}`, + }), + ], + context, + ); + const selection: StepSelection = yield step; + return QuickCommand.canPickStepContinue(step, state, selection) ? selection[0].item : StepResult.Break; + } + + private async *removeCommandSteps(state: RemoveStepState, context: Context): AsyncStepResultGenerator { + while (this.canStepsContinue(state)) { + if (state.remote != null) { + if (typeof state.remote === 'string') { + const [remote] = await state.repo.getRemotes({ filter: r => r.name === state.remote }); + if (remote != null) { + state.remote = remote; + } else { + state.remote = undefined!; + } + } + } + + if (state.counter < 3 || state.remote == null) { + context.title = getTitle('Remotes', state.subcommand); + + const result = yield* pickRemoteStep(state, context, { + picked: state.remote?.name, + placeholder: 'Choose remote to remove', + }); + // Always break on the first step (so we will go back) + if (result === StepResult.Break) break; + + state.remote = result; + } + + assertStateStepRemoveRemotes(state); + const result = yield* this.removeCommandConfirmStep(state, context); + if (result === StepResult.Break) continue; + + QuickCommand.endSteps(state); + try { + await state.repo.removeRemote(state.remote.name); + } catch (ex) { + Logger.error(ex); + void showGenericErrorMessage('Unable to remove remote'); + } + } + } + + private *removeCommandConfirmStep( + state: RemoveStepState>, + context: Context, + ): StepResultGenerator { + const step: QuickPickStep = QuickCommand.createConfirmStep( + appendReposToTitle(`Confirm ${context.title}`, state, context), + [ + { + label: context.title, + detail: `Will remove remote '${state.remote.name}'`, + }, + ], + context, + ); + const selection: StepSelection = yield step; + return QuickCommand.canPickStepContinue(step, state, selection) ? undefined : StepResult.Break; + } + + private async *pruneCommandSteps(state: PruneStepState, context: Context): AsyncStepResultGenerator { + while (this.canStepsContinue(state)) { + if (state.remote != null) { + if (typeof state.remote === 'string') { + const [remote] = await state.repo.getRemotes({ filter: r => r.name === state.remote }); + if (remote != null) { + state.remote = remote; + } else { + state.remote = undefined!; + } + } + } + + if (state.counter < 3 || state.remote == null) { + const result = yield* pickRemoteStep(state, context, { + picked: state.remote?.name, + placeholder: 'Choose a remote to prune', + }); + // Always break on the first step (so we will go back) + if (result === StepResult.Break) break; + + state.remote = result; + } + + assertStateStepPruneRemotes(state); + const result = yield* this.pruneCommandConfirmStep(state, context); + if (result === StepResult.Break) continue; + + QuickCommand.endSteps(state); + void state.repo.pruneRemote(state.remote.name); + } + } + + private *pruneCommandConfirmStep( + state: PruneStepState>, + context: Context, + ): StepResultGenerator { + const step: QuickPickStep = QuickCommand.createConfirmStep( + appendReposToTitle(`Confirm ${context.title}`, state, context), + [ + { + label: context.title, + detail: `Will prune remote '${state.remote.name}'`, + }, + ], + context, + ); + const selection: StepSelection = yield step; + return QuickCommand.canPickStepContinue(step, state, selection) ? undefined : StepResult.Break; + } +} diff --git a/src/commands/gitCommands.actions.ts b/src/commands/gitCommands.actions.ts index d0f3b62..1ed3282 100644 --- a/src/commands/gitCommands.actions.ts +++ b/src/commands/gitCommands.actions.ts @@ -6,6 +6,7 @@ import type { DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, GitCommandsCommandArgs, + GitCommandsCommandArgsWithCompletion, OpenFileOnRemoteCommandArgs, OpenWorkingFileCommandArgs, ShowQuickCommitCommandArgs, @@ -30,22 +31,21 @@ import type { GitRemote } from '../git/models/remote'; import type { Repository } from '../git/models/repository'; import type { GitWorktree } from '../git/models/worktree'; import type { ShowInCommitGraphCommandArgs } from '../plus/webviews/graph/graphWebview'; -import { RepositoryPicker } from '../quickpicks/repositoryPicker'; import { ensure } from '../system/array'; import { executeCommand, executeCoreCommand, executeEditorCommand } from '../system/command'; +import { defer } from '../system/promise'; import type { OpenWorkspaceLocation } from '../system/utils'; import { findOrOpenEditor, findOrOpenEditors, openWorkspace } from '../system/utils'; import type { ViewsWithRepositoryFolders } from '../views/viewBase'; import type { ResetGitCommandArgs } from './git/reset'; export async function executeGitCommand(args: GitCommandsCommandArgs): Promise { - void (await executeCommand(Commands.GitCommands, args)); -} - -function ensureRepo(repo: string | Repository): Repository { - const repository = typeof repo === 'string' ? Container.instance.git.getRepository(repo) : repo; - if (repository == null) throw new Error('Repository not found'); - return repository; + const deferred = defer(); + void (await executeCommand(Commands.GitCommands, { + ...args, + completion: deferred, + })); + return deferred.promise; } export namespace GitActions { @@ -822,38 +822,17 @@ export namespace GitActions { } export namespace Remote { - export async function add(repo?: string | Repository) { - if (repo == null) { - repo = Container.instance.git.highlander; - - if (repo == null) { - const pick = await RepositoryPicker.show(undefined, 'Choose a repository to add a remote to'); - repo = pick?.item; - if (repo == null) return undefined; - } - } - - const name = await window.showInputBox({ - prompt: 'Please provide a name for the remote', - placeHolder: 'Remote name', - value: undefined, - ignoreFocusOut: true, - }); - if (name == null || name.length === 0) return undefined; - - const url = await window.showInputBox({ - prompt: 'Please provide the repository url for the remote', - placeHolder: 'Remote repository url', - value: undefined, - ignoreFocusOut: true, + export function add(repo?: string | Repository, name?: string, url?: string, options?: { fetch?: boolean }) { + return executeGitCommand({ + command: 'remote', + state: { + repo: repo, + subcommand: 'add', + name: name, + url: url, + flags: options?.fetch ? ['-f'] : undefined, + }, }); - if (url == null || url.length === 0) return undefined; - - repo = ensureRepo(repo); - await Container.instance.git.addRemote(repo.path, name, url); - await repo.fetch({ remote: name }); - - return name; } export async function fetch(repo: string | Repository, remote: string) { @@ -868,11 +847,21 @@ export namespace GitActions { } export async function prune(repo: string | Repository, remote: string) { - await Container.instance.git.pruneRemote(typeof repo === 'string' ? repo : repo.path, remote); + return executeGitCommand({ + command: 'remote', + state: { repo: repo, subcommand: 'prune', remote: remote }, + }); + } + + export async function remove(repo: string | Repository, remote: string) { + return executeGitCommand({ + command: 'remote', + state: { repo: repo, subcommand: 'remove', remote: remote }, + }); } export async function reveal( - remote: GitRemote, + remote: GitRemote | undefined, options?: { select?: boolean; focus?: boolean; @@ -880,9 +869,12 @@ export namespace GitActions { }, ) { const view = Container.instance.remotesView; - const node = view.canReveal - ? await view.revealRemote(remote, options) - : await Container.instance.repositoriesView.revealRemote(remote, options); + const node = + remote != null + ? view.canReveal + ? await view.revealRemote(remote, options) + : await Container.instance.repositoriesView.revealRemote(remote, options) + : undefined; if (node == null) { void view.show({ preserveFocus: !options?.focus }); } @@ -890,7 +882,7 @@ export namespace GitActions { } } - export namespace Repository { + export namespace Repo { export async function reveal( repoPath: string, view?: ViewsWithRepositoryFolders, diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index 8492899..a015b10 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -1,5 +1,5 @@ import type { Disposable, InputBox, QuickInputButton, QuickPick, QuickPickItem } from 'vscode'; -import { QuickInputButtons, window } from 'vscode'; +import { InputBoxValidationSeverity, QuickInputButtons, window } from 'vscode'; import { configuration } from '../configuration'; import { Commands } from '../constants'; import { Container } from '../container'; @@ -7,6 +7,7 @@ import type { KeyMapping } from '../keyboard'; import { Directive, DirectiveQuickPickItem } from '../quickpicks/items/directive'; import { command } from '../system/command'; import { log } from '../system/decorators/log'; +import type { Deferred } from '../system/promise'; import { isPromise } from '../system/promise'; import type { CommandContext } from './base'; import { Command } from './base'; @@ -19,6 +20,7 @@ import type { MergeGitCommandArgs } from './git/merge'; import type { PullGitCommandArgs } from './git/pull'; import type { PushGitCommandArgs } from './git/push'; import type { RebaseGitCommandArgs } from './git/rebase'; +import type { RemoteGitCommandArgs } from './git/remote'; import type { ResetGitCommandArgs } from './git/reset'; import type { RevertGitCommandArgs } from './git/revert'; import type { SearchGitCommandArgs } from './git/search'; @@ -46,6 +48,7 @@ export type GitCommandsCommandArgs = | PullGitCommandArgs | PushGitCommandArgs | RebaseGitCommandArgs + | RemoteGitCommandArgs | ResetGitCommandArgs | RevertGitCommandArgs | SearchGitCommandArgs @@ -56,6 +59,8 @@ export type GitCommandsCommandArgs = | TagGitCommandArgs | WorktreeGitCommandArgs; +export type GitCommandsCommandArgsWithCompletion = GitCommandsCommandArgs & { completion?: Deferred }; + @command() export class GitCommandsCommand extends Command { private startedWith: 'menu' | 'command' = 'menu'; @@ -75,7 +80,7 @@ export class GitCommandsCommand extends Command { ]); } - protected override preExecute(context: CommandContext, args?: GitCommandsCommandArgs) { + protected override preExecute(context: CommandContext, args?: GitCommandsCommandArgsWithCompletion) { switch (context.command) { case Commands.GitCommandsBranch: args = { command: 'branch' }; @@ -110,7 +115,7 @@ export class GitCommandsCommand extends Command { } @log({ args: false, scoped: true, singleLine: true, timed: false }) - async execute(args?: GitCommandsCommandArgs) { + async execute(args?: GitCommandsCommandArgsWithCompletion) { const commandsStep = new PickCommandStep(this.container, args); const command = args?.command != null ? commandsStep.find(args.command) : undefined; @@ -170,6 +175,8 @@ export class GitCommandsCommand extends Command { break; } + + args?.completion?.fulfill(); } private async showLoadingIfNeeded( @@ -322,7 +329,8 @@ export class GitCommandsCommand extends Command { const disposables: Disposable[] = []; try { - return await new Promise(resolve => { + // eslint-disable-next-line no-async-promise-executor + return await new Promise(async resolve => { const goBack = async () => { input.value = ''; if (commandsStep.command != null) { @@ -407,6 +415,13 @@ export class GitCommandsCommand extends Command { input.prompt = step.prompt; if (step.value != null) { input.value = step.value; + + if (step.validate != null) { + const [valid, message] = await step.validate(step.value); + if (!valid && message != null) { + input.validationMessage = { severity: InputBoxValidationSeverity.Error, message: message }; + } + } } // If we are starting over clear the previously active command diff --git a/src/commands/gitCommands.utils.ts b/src/commands/gitCommands.utils.ts index 2cf9da2..5cf610a 100644 --- a/src/commands/gitCommands.utils.ts +++ b/src/commands/gitCommands.utils.ts @@ -13,6 +13,7 @@ import { MergeGitCommand } from './git/merge'; import { PullGitCommand } from './git/pull'; import { PushGitCommand } from './git/push'; import { RebaseGitCommand } from './git/rebase'; +import { RemoteGitCommand } from './git/remote'; import { ResetGitCommand } from './git/reset'; import { RevertGitCommand } from './git/revert'; import { SearchGitCommand } from './git/search'; @@ -74,6 +75,7 @@ export class PickCommandStep implements QuickPickStep { readonly ? undefined : new PullGitCommand(container, args?.command === 'pull' ? args : undefined), readonly ? undefined : new PushGitCommand(container, args?.command === 'push' ? args : undefined), readonly ? undefined : new RebaseGitCommand(container, args?.command === 'rebase' ? args : undefined), + readonly ? undefined : new RemoteGitCommand(container, args?.command === 'remote' ? args : undefined), readonly ? undefined : new ResetGitCommand(container, args?.command === 'reset' ? args : undefined), readonly ? undefined : new RevertGitCommand(container, args?.command === 'revert' ? args : undefined), new SearchGitCommand(container, args?.command === 'search' || args?.command === 'grep' ? args : undefined), diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index edf32ad..dae944c 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -48,16 +48,26 @@ import { } from '../quickpicks/items/commits'; import { CommandQuickPickItem, QuickPickSeparator } from '../quickpicks/items/common'; import { Directive, DirectiveQuickPickItem } from '../quickpicks/items/directive'; -import { +import type { BranchQuickPickItem, CommitQuickPickItem, ContributorQuickPickItem, - GitCommandQuickPickItem, - RefQuickPickItem, + RemoteQuickPickItem, RepositoryQuickPickItem, TagQuickPickItem, WorktreeQuickPickItem, } from '../quickpicks/items/gitCommands'; +import { + createBranchQuickPickItem, + createCommitQuickPickItem, + createContributorQuickPickItem, + createRefQuickPickItem, + createRemoteQuickPickItem, + createRepositoryQuickPickItem, + createTagQuickPickItem, + createWorktreeQuickPickItem, + GitCommandQuickPickItem, +} from '../quickpicks/items/gitCommands'; import type { ReferencesQuickPickItem } from '../quickpicks/referencePicker'; import { CopyRemoteResourceCommandQuickPickItem, @@ -123,6 +133,29 @@ export async function getBranches( }) as Promise; } +export async function getRemotes( + repo: Repository, + options: { + buttons?: QuickInputButton[]; + filter?: (b: GitRemote) => boolean; + picked?: string | string[]; + }, +): Promise { + if (repo == null) return []; + + const remotes = (await repo.getRemotes(options?.filter != null ? { filter: options.filter } : undefined)).map(r => + createRemoteQuickPickItem( + r, + options?.picked != null && + (typeof options.picked === 'string' ? r.name === options.picked : options.picked.includes(r.name)), + { + buttons: options?.buttons, + }, + ), + ); + return remotes; +} + export async function getTags( repos: Repository | Repository[], options?: { @@ -159,7 +192,7 @@ export async function getWorktrees( ...worktrees .filter(w => filter == null || filter(w)) .map(async w => - WorktreeQuickPickItem.create( + createWorktreeQuickPickItem( w, picked != null && (typeof picked === 'string' ? w.uri.toString() === picked : picked.includes(w.uri.toString())), @@ -266,7 +299,7 @@ export async function getBranchesAndOrTags( branches .filter(b => !b.remote) .map(b => - BranchQuickPickItem.create( + createBranchQuickPickItem( b, picked != null && (typeof picked === 'string' ? b.ref === picked : picked.includes(b.ref)), { @@ -284,7 +317,7 @@ export async function getBranchesAndOrTags( branches .filter(b => b.remote) .map(b => - BranchQuickPickItem.create( + createBranchQuickPickItem( b, picked != null && (typeof picked === 'string' ? b.ref === picked : picked.includes(b.ref)), { @@ -302,7 +335,7 @@ export async function getBranchesAndOrTags( if (tags != null && tags.length !== 0 && (branches == null || branches.length === 0)) { return tags.map(t => - TagQuickPickItem.create( + createTagQuickPickItem( t, picked != null && (typeof picked === 'string' ? t.ref === picked : picked.includes(t.ref)), { @@ -320,7 +353,7 @@ export async function getBranchesAndOrTags( branches! .filter(b => !b.remote) .map(b => - BranchQuickPickItem.create( + createBranchQuickPickItem( b, picked != null && (typeof picked === 'string' ? b.ref === picked : picked.includes(b.ref)), { @@ -334,7 +367,7 @@ export async function getBranchesAndOrTags( )), QuickPickSeparator.create('Tags'), ...tags!.map(t => - TagQuickPickItem.create( + createTagQuickPickItem( t, picked != null && (typeof picked === 'string' ? t.ref === picked : picked.includes(t.ref)), { @@ -350,7 +383,7 @@ export async function getBranchesAndOrTags( branches! .filter(b => b.remote) .map(b => - BranchQuickPickItem.create( + createBranchQuickPickItem( b, picked != null && (typeof picked === 'string' ? b.ref === picked : picked.includes(b.ref)), { @@ -386,7 +419,7 @@ export function getValidateGitReferenceFn( if (inRefMode && options?.ranges && GitRevision.isRange(value)) { quickpick.items = [ - RefQuickPickItem.create(value, repos.path, true, { + createRefQuickPickItem(value, repos.path, true, { alwaysShow: true, buttons: options?.buttons, ref: false, @@ -421,7 +454,7 @@ export function getValidateGitReferenceFn( const commit = await Container.instance.git.getCommit(repos.path, value); quickpick.items = [ - CommitQuickPickItem.create(commit!, true, { + createCommitQuickPickItem(commit!, true, { alwaysShow: true, buttons: options?.buttons, compact: true, @@ -480,6 +513,85 @@ export async function* inputBranchNameStep< return value; } +export async function* inputRemoteNameStep< + State extends PartialStepState & ({ repo: Repository } | { repos: Repository[] }), + Context extends { repos: Repository[]; title: string }, +>( + state: State, + context: Context, + options: { placeholder: string; titleContext?: string; value?: string }, +): AsyncStepResultGenerator { + const step = QuickCommand.createInputStep({ + title: appendReposToTitle(`${context.title}${options.titleContext ?? ''}`, state, context), + placeholder: options.placeholder, + value: options.value, + prompt: 'Enter remote name', + validate: async (value: string | undefined): Promise<[boolean, string | undefined]> => { + if (value == null) return [false, undefined]; + + value = value.trim(); + if (value.length === 0) return [false, 'Please enter a valid remote name']; + + const valid = !/[^a-zA-Z0-9-_.]/.test(value); + if (!valid) return [false, `'${value}' isn't a valid remote name`]; + + if ('repo' in state) { + const alreadyExists = (await state.repo.getRemotes({ filter: r => r.name === value })).length !== 0; + if (alreadyExists) { + return [false, `Remote named '${value}' already exists`]; + } + } + + return [true, undefined]; + }, + }); + + const value: StepSelection = yield step; + if ( + !QuickCommand.canStepContinue(step, state, value) || + !(await QuickCommand.canInputStepContinue(step, state, value)) + ) { + return StepResult.Break; + } + + return value; +} + +export async function* inputRemoteUrlStep< + State extends PartialStepState & ({ repo: Repository } | { repos: Repository[] }), + Context extends { repos: Repository[]; title: string }, +>( + state: State, + context: Context, + options: { placeholder: string; titleContext?: string; value?: string }, +): AsyncStepResultGenerator { + const step = QuickCommand.createInputStep({ + title: appendReposToTitle(`${context.title}${options.titleContext ?? ''}`, state, context), + placeholder: options.placeholder, + value: options.value, + prompt: 'Enter remote URL', + validate: (value: string | undefined): [boolean, string | undefined] => { + if (value == null) return [false, undefined]; + + value = value.trim(); + if (value.length === 0) return [false, 'Please enter a valid remote URL']; + + const valid = /^(https?|git|ssh|rsync):\/\//.test(value); + return [valid, valid ? undefined : `'${value}' isn't a valid remote URL`]; + }, + }); + + const value: StepSelection = yield step; + if ( + !QuickCommand.canStepContinue(step, state, value) || + !(await QuickCommand.canInputStepContinue(step, state, value)) + ) { + return StepResult.Break; + } + + return value; +} + export async function* inputTagNameStep< State extends PartialStepState & ({ repo: Repository } | { repos: Repository[] }), Context extends { repos: Repository[]; showTags?: boolean; title: string }, @@ -899,7 +1011,7 @@ export async function* pickCommitStep< ? [DirectiveQuickPickItem.create(Directive.Back, true), DirectiveQuickPickItem.create(Directive.Cancel)] : [ ...map(log.commits.values(), commit => - CommitQuickPickItem.create( + createCommitQuickPickItem( commit, picked != null && (typeof picked === 'string' ? commit.ref === picked : picked.includes(commit.ref)), @@ -1024,7 +1136,7 @@ export function* pickCommitsStep< ? [DirectiveQuickPickItem.create(Directive.Back, true), DirectiveQuickPickItem.create(Directive.Cancel)] : [ ...map(log.commits.values(), commit => - CommitQuickPickItem.create( + createCommitQuickPickItem( commit, picked != null && (typeof picked === 'string' ? commit.ref === picked : picked.includes(commit.ref)), @@ -1111,7 +1223,7 @@ export async function* pickContributorsStep< placeholder: placeholder, matchOnDescription: true, items: (await Container.instance.git.getContributors(state.repo.path)).map(c => - ContributorQuickPickItem.create(c, message?.includes(c.getCoauthor()), { + createContributorQuickPickItem(c, message?.includes(c.getCoauthor()), { buttons: [QuickCommandButtons.RevealInSideBar], }), ), @@ -1135,6 +1247,111 @@ export async function* pickContributorsStep< return QuickCommand.canPickStepContinue(step, state, selection) ? selection.map(i => i.item) : StepResult.Break; } +export async function* pickRemoteStep< + State extends PartialStepState & { repo: Repository }, + Context extends { repos: Repository[]; title: string }, +>( + state: State, + context: Context, + { + filter, + picked, + placeholder, + titleContext, + }: { + filter?: (r: GitRemote) => boolean; + picked?: string | string[]; + placeholder: string; + titleContext?: string; + }, +): AsyncStepResultGenerator { + const remotes = await getRemotes(state.repo, { + buttons: [QuickCommandButtons.RevealInSideBar], + filter: filter, + picked: picked, + }); + + const step = QuickCommand.createPickStep({ + title: appendReposToTitle(`${context.title}${titleContext ?? ''}`, state, context), + placeholder: remotes.length === 0 ? `No remotes found in ${state.repo.formattedName}` : placeholder, + matchOnDetail: true, + items: + remotes.length === 0 + ? [DirectiveQuickPickItem.create(Directive.Back, true), DirectiveQuickPickItem.create(Directive.Cancel)] + : remotes, + onDidClickItemButton: (quickpick, button, { item }) => { + if (button === QuickCommandButtons.RevealInSideBar) { + void GitActions.Remote.reveal(item, { select: true, focus: false, expand: true }); + } + }, + keys: ['right', 'alt+right', 'ctrl+right'], + onDidPressKey: async quickpick => { + if (quickpick.activeItems.length === 0) return; + + await GitActions.Remote.reveal(quickpick.activeItems[0].item, { + select: true, + focus: false, + expand: true, + }); + }, + }); + const selection: StepSelection = yield step; + return QuickCommand.canPickStepContinue(step, state, selection) ? selection[0].item : StepResult.Break; +} + +export async function* pickRemotesStep< + State extends PartialStepState & { repo: Repository }, + Context extends { repos: Repository[]; title: string }, +>( + state: State, + context: Context, + { + filter, + picked, + placeholder, + titleContext, + }: { + filter?: (b: GitRemote) => boolean; + picked?: string | string[]; + placeholder: string; + titleContext?: string; + }, +): AsyncStepResultGenerator { + const remotes = await getRemotes(state.repo, { + buttons: [QuickCommandButtons.RevealInSideBar], + filter: filter, + picked: picked, + }); + + const step = QuickCommand.createPickStep({ + multiselect: remotes.length !== 0, + title: appendReposToTitle(`${context.title}${titleContext ?? ''}`, state, context), + placeholder: remotes.length === 0 ? `No remotes found in ${state.repo.formattedName}` : placeholder, + matchOnDetail: true, + items: + remotes.length === 0 + ? [DirectiveQuickPickItem.create(Directive.Back, true), DirectiveQuickPickItem.create(Directive.Cancel)] + : remotes, + onDidClickItemButton: (quickpick, button, { item }) => { + if (button === QuickCommandButtons.RevealInSideBar) { + void GitActions.Remote.reveal(item, { select: true, focus: false, expand: true }); + } + }, + keys: ['right', 'alt+right', 'ctrl+right'], + onDidPressKey: async quickpick => { + if (quickpick.activeItems.length === 0) return; + + await GitActions.Remote.reveal(quickpick.activeItems[0].item, { + select: true, + focus: false, + expand: true, + }); + }, + }); + const selection: StepSelection = yield step; + return QuickCommand.canPickStepContinue(step, state, selection) ? selection.map(i => i.item) : StepResult.Break; +} + export async function* pickRepositoryStep< State extends PartialStepState & { repo?: string | Repository }, Context extends { repos: Repository[]; title: string; associatedView: ViewsWithRepositoryFolders }, @@ -1153,7 +1370,7 @@ export async function* pickRepositoryStep< ? [DirectiveQuickPickItem.create(Directive.Cancel)] : await Promise.all( context.repos.map(r => - RepositoryQuickPickItem.create(r, r.id === active?.id, { + createRepositoryQuickPickItem(r, r.id === active?.id, { branch: true, buttons: [QuickCommandButtons.RevealInSideBar], fetched: true, @@ -1163,7 +1380,7 @@ export async function* pickRepositoryStep< ), onDidClickItemButton: (quickpick, button, { item }) => { if (button === QuickCommandButtons.RevealInSideBar) { - void GitActions.Repository.reveal(item.path, context.associatedView, { + void GitActions.Repo.reveal(item.path, context.associatedView, { select: true, focus: false, expand: true, @@ -1174,7 +1391,7 @@ export async function* pickRepositoryStep< onDidPressKey: quickpick => { if (quickpick.activeItems.length === 0) return; - void GitActions.Repository.reveal(quickpick.activeItems[0].item.path, context.associatedView, { + void GitActions.Repo.reveal(quickpick.activeItems[0].item.path, context.associatedView, { select: true, focus: false, expand: true, @@ -1219,7 +1436,7 @@ export async function* pickRepositoriesStep< ? [DirectiveQuickPickItem.create(Directive.Cancel)] : await Promise.all( context.repos.map(repo => - RepositoryQuickPickItem.create( + createRepositoryQuickPickItem( repo, actives.some(r => r.id === repo.id), { @@ -1233,7 +1450,7 @@ export async function* pickRepositoriesStep< ), onDidClickItemButton: (quickpick, button, { item }) => { if (button === QuickCommandButtons.RevealInSideBar) { - void GitActions.Repository.reveal(item.path, context.associatedView, { + void GitActions.Repo.reveal(item.path, context.associatedView, { select: true, focus: false, expand: true, @@ -1244,7 +1461,7 @@ export async function* pickRepositoriesStep< onDidPressKey: quickpick => { if (quickpick.activeItems.length === 0) return; - void GitActions.Repository.reveal(quickpick.activeItems[0].item.path, context.associatedView, { + void GitActions.Repo.reveal(quickpick.activeItems[0].item.path, context.associatedView, { select: true, focus: false, expand: true, @@ -1286,7 +1503,7 @@ export function* pickStashStep< ? [DirectiveQuickPickItem.create(Directive.Back, true), DirectiveQuickPickItem.create(Directive.Cancel)] : [ ...map(stash.commits.values(), commit => - CommitQuickPickItem.create( + createCommitQuickPickItem( commit, picked != null && (typeof picked === 'string' ? commit.ref === picked : picked.includes(commit.ref)), diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index b0304aa..42a80ec 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -1332,12 +1332,20 @@ export class Git { return this.git({ cwd: repoPath }, 'remote', '-v'); } - remote__add(repoPath: string, name: string, url: string) { - return this.git({ cwd: repoPath }, 'remote', 'add', name, url); + remote__add(repoPath: string, name: string, url: string, options?: { fetch?: boolean }) { + const params = ['remote', 'add']; + if (options?.fetch) { + params.push('-f'); + } + return this.git({ cwd: repoPath }, ...params, name, url); + } + + remote__prune(repoPath: string, name: string) { + return this.git({ cwd: repoPath }, 'remote', 'prune', name); } - remote__prune(repoPath: string, remoteName: string) { - return this.git({ cwd: repoPath }, 'remote', 'prune', remoteName); + remote__remove(repoPath: string, name: string) { + return this.git({ cwd: repoPath }, 'remote', 'remove', name); } remote__get_url(repoPath: string, remote: string): Promise { diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index f795e4c..d399d87 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -847,8 +847,8 @@ export class LocalGitProvider implements GitProvider, Disposable { } @log() - async addRemote(repoPath: string, name: string, url: string): Promise { - await this.git.remote__add(repoPath, name, url); + async addRemote(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise { + await this.git.remote__add(repoPath, name, url, options); this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['remotes'] }); } @@ -859,6 +859,12 @@ export class LocalGitProvider implements GitProvider, Disposable { } @log() + async removeRemote(repoPath: string, name: string): Promise { + await this.git.remote__remove(repoPath, name); + this.container.events.fire('git:cache:reset', { repoPath: repoPath, caches: ['remotes'] }); + } + + @log() async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string) { const scope = getLogScope(); diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 02f5a38..17e3ab3 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -132,8 +132,9 @@ export interface GitProvider extends Disposable { // getRootUri(pathOrUri: string | Uri): Uri; getWorkingUri(repoPath: string, uri: Uri): Promise; - addRemote(repoPath: string, name: string, url: string): Promise; - pruneRemote(repoPath: string, remoteName: string): Promise; + addRemote(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise; + pruneRemote(repoPath: string, name: string): Promise; + removeRemote(repoPath: string, name: string): Promise; applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise; checkout( repoPath: string, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 9c867b9..e325040 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -996,35 +996,37 @@ export class GitProviderService implements Disposable { } @log() - async getWorkingUri(repoPath: string | Uri, uri: Uri) { + getWorkingUri(repoPath: string | Uri, uri: Uri) { const { provider, path } = this.getProvider(repoPath); return provider.getWorkingUri(path, uri); } @log() - addRemote(repoPath: string | Uri, name: string, url: string): Promise { + addRemote(repoPath: string | Uri, name: string, url: string, options?: { fetch?: boolean }): Promise { const { provider, path } = this.getProvider(repoPath); - return provider.addRemote(path, name, url); + return provider.addRemote(path, name, url, options); } @log() - pruneRemote(repoPath: string | Uri, remoteName: string): Promise { + pruneRemote(repoPath: string | Uri, name: string): Promise { const { provider, path } = this.getProvider(repoPath); - return provider.pruneRemote(path, remoteName); + return provider.pruneRemote(path, name); } @log() - async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise { + removeRemote(repoPath: string | Uri, name: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.removeRemote(path, name); + } + + @log() + applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise { const { provider } = this.getProvider(uri); return provider.applyChangesToWorkingFile(uri, ref1, ref2); } @log() - async checkout( - repoPath: string, - ref: string, - options?: { createBranch?: string } | { path?: string }, - ): Promise { + checkout(repoPath: string, ref: string, options?: { createBranch?: string } | { path?: string }): Promise { const { provider, path } = this.getProvider(repoPath); return provider.checkout(path, ref, options); } @@ -1039,14 +1041,14 @@ export class GitProviderService implements Disposable { } @log({ args: { 1: uris => uris.length } }) - async excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise { + excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise { const { provider, path } = this.getProvider(repoPath); return provider.excludeIgnoredUris(path, uris); } @gate() @log() - async fetch( + fetch( repoPath: string, options?: { all?: boolean; branch?: GitBranchReference; prune?: boolean; pull?: boolean; remote?: string }, ): Promise { diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index db730cd..93b863e 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -459,6 +459,23 @@ export class Repository implements Disposable { } @log() + async addRemote(name: string, url: string, options?: { fetch?: boolean }): Promise { + await this.container.git.addRemote(this.path, name, url, options); + const [remote] = await this.getRemotes({ filter: r => r.url === url }); + return remote; + } + + @log() + pruneRemote(name: string): Promise { + return this.container.git.pruneRemote(this.path, name); + } + + @log() + removeRemote(name: string): Promise { + return this.container.git.removeRemote(this.path, name); + } + + @log() branch(...args: string[]) { this.runTerminalCommand('branch', ...args); } diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index 56c0ff0..fb96727 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -358,10 +358,13 @@ export class GitHubGitProvider implements GitProvider, Disposable { } @log() - async addRemote(_repoPath: string, _name: string, _url: string): Promise {} + async addRemote(_repoPath: string, _name: string, _url: string, _options?: { fetch?: boolean }): Promise {} @log() - async pruneRemote(_repoPath: string, _remoteName: string): Promise {} + async pruneRemote(_repoPath: string, _name: string): Promise {} + + @log() + async removeRemote(_repoPath: string, _name: string): Promise {} @log() async applyChangesToWorkingFile(_uri: GitUri, _ref1?: string, _ref2?: string): Promise {} diff --git a/src/quickpicks/commitPicker.ts b/src/quickpicks/commitPicker.ts index 4032cf9..a8f0a97 100644 --- a/src/quickpicks/commitPicker.ts +++ b/src/quickpicks/commitPicker.ts @@ -11,7 +11,8 @@ import { isPromise } from '../system/promise'; import { getQuickPickIgnoreFocusOut } from '../system/utils'; import { CommandQuickPickItem } from './items/common'; import { Directive, DirectiveQuickPickItem } from './items/directive'; -import { CommitQuickPickItem } from './items/gitCommands'; +import type { CommitQuickPickItem } from './items/gitCommands'; +import { createCommitQuickPickItem } from './items/gitCommands'; export namespace CommitPicker { export async function show( @@ -56,7 +57,7 @@ export namespace CommitPicker { : [ ...(options?.showOtherReferences ?? []), ...map(log.commits.values(), commit => - CommitQuickPickItem.create(commit, options?.picked === commit.ref, { + createCommitQuickPickItem(commit, options?.picked === commit.ref, { compact: true, icon: true, }), @@ -215,7 +216,7 @@ export namespace StashPicker { ...map( options?.filter != null ? filter(stash.commits.values(), options.filter) : stash.commits.values(), commit => - CommitQuickPickItem.create(commit, options?.picked === commit.ref, { + createCommitQuickPickItem(commit, options?.picked === commit.ref, { compact: true, icon: true, }), diff --git a/src/quickpicks/items/gitCommands.ts b/src/quickpicks/items/gitCommands.ts index d377bb2..c835854 100644 --- a/src/quickpicks/items/gitCommands.ts +++ b/src/quickpicks/items/gitCommands.ts @@ -9,6 +9,7 @@ import type { GitCommit } from '../../git/models/commit'; import { isStash } from '../../git/models/commit'; import type { GitContributor } from '../../git/models/contributor'; import { GitReference, GitRevision } from '../../git/models/reference'; +import type { GitRemote } from '../../git/models/remote'; import { GitRemoteType } from '../../git/models/remote'; import type { Repository } from '../../git/models/repository'; import type { GitStatus } from '../../git/models/status'; @@ -37,105 +38,103 @@ export interface BranchQuickPickItem extends QuickPickItemOfT { readonly remote: boolean; } -export namespace BranchQuickPickItem { - export async function create( - branch: GitBranch, - picked?: boolean, - options?: { - alwaysShow?: boolean; - buttons?: QuickInputButton[]; - checked?: boolean; - current?: boolean | 'checkmark'; - ref?: boolean; - status?: boolean; - type?: boolean | 'remote'; - }, - ): Promise { - let description = ''; - if (options?.type === true) { - if (options.current === true && branch.current) { - description = 'current branch'; - } else { - description = 'branch'; - } - } else if (options?.type === 'remote') { - if (branch.remote) { - description = 'remote branch'; - } - } else if (options?.current === true && branch.current) { +export async function createBranchQuickPickItem( + branch: GitBranch, + picked?: boolean, + options?: { + alwaysShow?: boolean; + buttons?: QuickInputButton[]; + checked?: boolean; + current?: boolean | 'checkmark'; + ref?: boolean; + status?: boolean; + type?: boolean | 'remote'; + }, +): Promise { + let description = ''; + if (options?.type === true) { + if (options.current === true && branch.current) { description = 'current branch'; + } else { + description = 'branch'; } + } else if (options?.type === 'remote') { + if (branch.remote) { + description = 'remote branch'; + } + } else if (options?.current === true && branch.current) { + description = 'current branch'; + } - if (options?.status && !branch.remote && branch.upstream != null) { - let arrows = GlyphChars.Dash; + if (options?.status && !branch.remote && branch.upstream != null) { + let arrows = GlyphChars.Dash; - if (!branch.upstream.missing) { - const remote = await branch.getRemote(); - if (remote != null) { - let left; - let right; - for (const { type } of remote.urls) { - if (type === GitRemoteType.Fetch) { - left = true; + if (!branch.upstream.missing) { + const remote = await branch.getRemote(); + if (remote != null) { + let left; + let right; + for (const { type } of remote.urls) { + if (type === GitRemoteType.Fetch) { + left = true; - if (right) break; - } else if (type === GitRemoteType.Push) { - right = true; + if (right) break; + } else if (type === GitRemoteType.Push) { + right = true; - if (left) break; - } + if (left) break; } + } - if (left && right) { - arrows = GlyphChars.ArrowsRightLeft; - } else if (right) { - arrows = GlyphChars.ArrowRight; - } else if (left) { - arrows = GlyphChars.ArrowLeft; - } + if (left && right) { + arrows = GlyphChars.ArrowsRightLeft; + } else if (right) { + arrows = GlyphChars.ArrowRight; + } else if (left) { + arrows = GlyphChars.ArrowLeft; } - } else { - arrows = GlyphChars.Warning; } - - const status = `${branch.getTrackingStatus({ suffix: `${GlyphChars.Space} ` })}${arrows}${ - GlyphChars.Space - } ${branch.upstream.name}`; - description = `${description ? `${description}${GlyphChars.Space.repeat(2)}${status}` : status}`; + } else { + arrows = GlyphChars.Warning; } - if (options?.ref) { - if (branch.sha) { - description = description - ? `${description} $(git-commit)${GlyphChars.Space}${GitRevision.shorten(branch.sha)}` - : `$(git-commit)${GlyphChars.Space}${GitRevision.shorten(branch.sha)}`; - } + const status = `${branch.getTrackingStatus({ suffix: `${GlyphChars.Space} ` })}${arrows}${GlyphChars.Space} ${ + branch.upstream.name + }`; + description = `${description ? `${description}${GlyphChars.Space.repeat(2)}${status}` : status}`; + } - if (branch.date !== undefined) { - description = description - ? `${description}${pad(GlyphChars.Dot, 2, 2)}${branch.formattedDate}` - : branch.formattedDate; - } + if (options?.ref) { + if (branch.sha) { + description = description + ? `${description} $(git-commit)${GlyphChars.Space}${GitRevision.shorten(branch.sha)}` + : `$(git-commit)${GlyphChars.Space}${GitRevision.shorten(branch.sha)}`; } - const checked = - options?.checked || (options?.checked == null && options?.current === 'checkmark' && branch.current); - const item: BranchQuickPickItem = { - label: `$(git-branch)${GlyphChars.Space}${branch.starred ? `$(star-full)${GlyphChars.Space}` : ''}${ - branch.name - }${checked ? pad('$(check)', 2) : ''}`, - description: description, - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked ?? branch.current, - item: branch, - current: branch.current, - ref: branch.name, - remote: branch.remote, - }; - - return item; + if (branch.date !== undefined) { + description = description + ? `${description}${pad(GlyphChars.Dot, 2, 2)}${branch.formattedDate}` + : branch.formattedDate; + } } + + const checked = + options?.checked || (options?.checked == null && options?.current === 'checkmark' && branch.current); + const item: BranchQuickPickItem = { + label: `$(git-branch)${GlyphChars.Space}${branch.starred ? `$(star-full)${GlyphChars.Space}` : ''}${ + branch.name + }${checked ? pad('$(check)', 2) : ''}`, + description: description, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked ?? branch.current, + item: branch, + current: branch.current, + ref: branch.name, + remote: branch.remote, + }; + + return item; } export class CommitLoadMoreQuickPickItem implements QuickPickItem { @@ -145,98 +144,94 @@ export class CommitLoadMoreQuickPickItem implements QuickPickItem { export type CommitQuickPickItem = QuickPickItemOfT; -export namespace CommitQuickPickItem { - export function create( - commit: T, - picked?: boolean, - options?: { alwaysShow?: boolean; buttons?: QuickInputButton[]; compact?: boolean; icon?: boolean }, - ) { - if (isStash(commit)) { - const number = commit.number == null ? '' : `${commit.number}: `; - - if (options?.compact) { - const item: CommitQuickPickItem = { - label: `${options.icon ? `$(archive)${GlyphChars.Space}` : ''}${number}${commit.summary}`, - description: `${commit.formattedDate}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ - compact: true, - })}`, - alwaysShow: options.alwaysShow, - buttons: options.buttons, - picked: picked, - item: commit, - }; - - return item; - } - - const item: CommitQuickPickItem = { - label: `${options?.icon ? `$(archive)${GlyphChars.Space}` : ''}${number}${commit.summary}`, - description: '', - detail: `${GlyphChars.Space.repeat(2)}${commit.formattedDate}${pad( - GlyphChars.Dot, - 2, - 2, - )}${commit.formatStats({ compact: true })}`, - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: commit, - }; - - return item; - } +export function createCommitQuickPickItem( + commit: T, + picked?: boolean, + options?: { alwaysShow?: boolean; buttons?: QuickInputButton[]; compact?: boolean; icon?: boolean }, +) { + if (isStash(commit)) { + const number = commit.number == null ? '' : `${commit.number}: `; if (options?.compact) { const item: CommitQuickPickItem = { - label: `${options.icon ? `$(git-commit)${GlyphChars.Space}` : ''}${commit.summary}`, - description: `${commit.author.name}, ${commit.formattedDate}${pad('$(git-commit)', 2, 1)}${ - commit.shortSha - }${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ compact: true })}`, + label: `${options.icon ? `$(archive)${GlyphChars.Space}` : ''}${number}${commit.summary}`, + description: `${commit.formattedDate}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ + compact: true, + })}`, alwaysShow: options.alwaysShow, buttons: options.buttons, picked: picked, item: commit, }; + return item; } const item: CommitQuickPickItem = { - label: `${options?.icon ? `$(git-commit)${GlyphChars.Space}` : ''}${commit.summary}`, + label: `${options?.icon ? `$(archive)${GlyphChars.Space}` : ''}${number}${commit.summary}`, description: '', - detail: `${GlyphChars.Space.repeat(2)}${commit.author.name}, ${commit.formattedDate}${pad( - '$(git-commit)', + detail: `${GlyphChars.Space.repeat(2)}${commit.formattedDate}${pad( + GlyphChars.Dot, 2, - 1, - )}${commit.shortSha}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ - compact: true, - })}`, + 2, + )}${commit.formatStats({ compact: true })}`, alwaysShow: options?.alwaysShow, buttons: options?.buttons, picked: picked, item: commit, }; + return item; } -} - -export type ContributorQuickPickItem = QuickPickItemOfT; -export namespace ContributorQuickPickItem { - export function create( - contributor: GitContributor, - picked?: boolean, - options?: { alwaysShow?: boolean; buttons?: QuickInputButton[] }, - ): ContributorQuickPickItem { - const item: ContributorQuickPickItem = { - label: contributor.label, - description: contributor.email, - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, + if (options?.compact) { + const item: CommitQuickPickItem = { + label: `${options.icon ? `$(git-commit)${GlyphChars.Space}` : ''}${commit.summary}`, + description: `${commit.author.name}, ${commit.formattedDate}${pad('$(git-commit)', 2, 1)}${ + commit.shortSha + }${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ compact: true })}`, + alwaysShow: options.alwaysShow, + buttons: options.buttons, picked: picked, - item: contributor, + item: commit, }; return item; } + + const item: CommitQuickPickItem = { + label: `${options?.icon ? `$(git-commit)${GlyphChars.Space}` : ''}${commit.summary}`, + description: '', + detail: `${GlyphChars.Space.repeat(2)}${commit.author.name}, ${commit.formattedDate}${pad( + '$(git-commit)', + 2, + 1, + )}${commit.shortSha}${pad(GlyphChars.Dot, 2, 2)}${commit.formatStats({ + compact: true, + })}`, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: commit, + }; + return item; +} + +export type ContributorQuickPickItem = QuickPickItemOfT; + +export function createContributorQuickPickItem( + contributor: GitContributor, + picked?: boolean, + options?: { alwaysShow?: boolean; buttons?: QuickInputButton[] }, +): ContributorQuickPickItem { + const item: ContributorQuickPickItem = { + label: contributor.label, + description: contributor.email, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: contributor, + }; + return item; } export interface RefQuickPickItem extends QuickPickItemOfT { @@ -245,66 +240,52 @@ export interface RefQuickPickItem extends QuickPickItemOfT { readonly remote: boolean; } -export namespace RefQuickPickItem { - export function create( - ref: string | GitReference, - repoPath: string, - picked?: boolean, - options?: { alwaysShow?: boolean; buttons?: QuickInputButton[]; icon?: boolean; ref?: boolean }, - ): RefQuickPickItem { - if (ref === '') { - return { - label: `${options?.icon ? `$(file-directory)${GlyphChars.Space}` : ''}Working Tree`, - description: '', - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: GitReference.create(ref, repoPath, { refType: 'revision', name: 'Working Tree' }), - current: false, - ref: ref, - remote: false, - }; - } - - if (ref === 'HEAD') { - return { - label: `${options?.icon ? `$(git-branch)${GlyphChars.Space}` : ''}HEAD`, - description: '', - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: GitReference.create(ref, repoPath, { refType: 'revision', name: 'HEAD' }), - current: false, - ref: ref, - remote: false, - }; - } +export function createRefQuickPickItem( + ref: string | GitReference, + repoPath: string, + picked?: boolean, + options?: { alwaysShow?: boolean; buttons?: QuickInputButton[]; icon?: boolean; ref?: boolean }, +): RefQuickPickItem { + if (ref === '') { + return { + label: `${options?.icon ? `$(file-directory)${GlyphChars.Space}` : ''}Working Tree`, + description: '', + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: GitReference.create(ref, repoPath, { refType: 'revision', name: 'Working Tree' }), + current: false, + ref: ref, + remote: false, + }; + } - let gitRef; - if (typeof ref === 'string') { - gitRef = GitReference.create(ref, repoPath); - } else { - gitRef = ref; - ref = gitRef.ref; - } + if (ref === 'HEAD') { + return { + label: `${options?.icon ? `$(git-branch)${GlyphChars.Space}` : ''}HEAD`, + description: '', + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: GitReference.create(ref, repoPath, { refType: 'revision', name: 'HEAD' }), + current: false, + ref: ref, + remote: false, + }; + } - if (GitRevision.isRange(ref)) { - return { - label: `Range ${gitRef.name}`, - description: '', - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: gitRef, - current: false, - ref: ref, - remote: false, - }; - } + let gitRef; + if (typeof ref === 'string') { + gitRef = GitReference.create(ref, repoPath); + } else { + gitRef = ref; + ref = gitRef.ref; + } - const item: RefQuickPickItem = { - label: `Commit ${gitRef.name}`, - description: options?.ref ? `$(git-commit)${GlyphChars.Space}${ref}` : '', + if (GitRevision.isRange(ref)) { + return { + label: `Range ${gitRef.name}`, + description: '', alwaysShow: options?.alwaysShow, buttons: options?.buttons, picked: picked, @@ -313,76 +294,115 @@ export namespace RefQuickPickItem { ref: ref, remote: false, }; + } - return item; + const item: RefQuickPickItem = { + label: `Commit ${gitRef.name}`, + description: options?.ref ? `$(git-commit)${GlyphChars.Space}${ref}` : '', + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: gitRef, + current: false, + ref: ref, + remote: false, + }; + + return item; +} + +export type RemoteQuickPickItem = QuickPickItemOfT; + +export function createRemoteQuickPickItem( + remote: GitRemote, + picked?: boolean, + options?: { + alwaysShow?: boolean; + buttons?: QuickInputButton[]; + checked?: boolean; + type?: boolean; + }, +) { + let description = ''; + if (options?.type) { + description = 'remote'; } + + const item: RemoteQuickPickItem = { + label: `$(cloud)${GlyphChars.Space}${remote.name}${options?.checked ? pad('$(check)', 2) : ''}`, + description: description, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: remote, + }; + + return item; } export interface RepositoryQuickPickItem extends QuickPickItemOfT { readonly repoPath: string; } -export namespace RepositoryQuickPickItem { - export async function create( - repository: Repository, - picked?: boolean, - options?: { - alwaysShow?: boolean; - branch?: boolean; - buttons?: QuickInputButton[]; - fetched?: boolean; - status?: boolean; - }, - ) { - let repoStatus; - if (options?.branch || options?.status) { - repoStatus = await repository.getStatus(); - } - - let description = ''; - if (options?.branch && repoStatus != null) { - description = repoStatus.branch; - } +export async function createRepositoryQuickPickItem( + repository: Repository, + picked?: boolean, + options?: { + alwaysShow?: boolean; + branch?: boolean; + buttons?: QuickInputButton[]; + fetched?: boolean; + status?: boolean; + }, +) { + let repoStatus; + if (options?.branch || options?.status) { + repoStatus = await repository.getStatus(); + } - if (options?.status && repoStatus != null) { - let workingStatus = ''; - if (repoStatus.files.length !== 0) { - workingStatus = repoStatus.getFormattedDiffStatus({ - compact: true, - prefix: pad(GlyphChars.Dot, 2, 2), - }); - } + let description = ''; + if (options?.branch && repoStatus != null) { + description = repoStatus.branch; + } - const upstreamStatus = repoStatus.getUpstreamStatus({ - prefix: description ? `${GlyphChars.Space} ` : '', + if (options?.status && repoStatus != null) { + let workingStatus = ''; + if (repoStatus.files.length !== 0) { + workingStatus = repoStatus.getFormattedDiffStatus({ + compact: true, + prefix: pad(GlyphChars.Dot, 2, 2), }); - - const status = `${upstreamStatus}${workingStatus}`; - if (status) { - description = `${description ? `${description}${status}` : status}`; - } } - if (options?.fetched) { - const lastFetched = await repository.getLastFetched(); - if (lastFetched !== 0) { - const fetched = `Last fetched ${fromNow(new Date(lastFetched))}`; - description = `${description ? `${description}${pad(GlyphChars.Dot, 2, 2)}${fetched}` : fetched}`; - } - } + const upstreamStatus = repoStatus.getUpstreamStatus({ + prefix: description ? `${GlyphChars.Space} ` : '', + }); - const item: RepositoryQuickPickItem = { - label: repository.formattedName, - description: description, - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: repository, - repoPath: repository.path, - }; + const status = `${upstreamStatus}${workingStatus}`; + if (status) { + description = `${description ? `${description}${status}` : status}`; + } + } - return item; + if (options?.fetched) { + const lastFetched = await repository.getLastFetched(); + if (lastFetched !== 0) { + const fetched = `Last fetched ${fromNow(new Date(lastFetched))}`; + description = `${description ? `${description}${pad(GlyphChars.Dot, 2, 2)}${fetched}` : fetched}`; + } } + + const item: RepositoryQuickPickItem = { + label: repository.formattedName, + description: description, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: repository, + repoPath: repository.path, + }; + + return item; } export interface TagQuickPickItem extends QuickPickItemOfT { @@ -391,51 +411,47 @@ export interface TagQuickPickItem extends QuickPickItemOfT { readonly remote: boolean; } -export namespace TagQuickPickItem { - export function create( - tag: GitTag, - picked?: boolean, - options?: { - alwaysShow?: boolean; - buttons?: QuickInputButton[]; - checked?: boolean; - message?: boolean; - ref?: boolean; - type?: boolean; - }, - ) { - let description = ''; - if (options?.type) { - description = 'tag'; - } - - if (options?.ref) { - description = `${description}${pad('$(git-commit)', description ? 2 : 0, 1)}${GitRevision.shorten( - tag.sha, - )}`; - - description = `${description ? `${description}${pad(GlyphChars.Dot, 2, 2)}` : ''}${tag.formattedDate}`; - } +export function createTagQuickPickItem( + tag: GitTag, + picked?: boolean, + options?: { + alwaysShow?: boolean; + buttons?: QuickInputButton[]; + checked?: boolean; + message?: boolean; + ref?: boolean; + type?: boolean; + }, +) { + let description = ''; + if (options?.type) { + description = 'tag'; + } - if (options?.message) { - const message = emojify(tag.message); - description = description ? `${description}${pad(GlyphChars.Dot, 2, 2)}${message}` : message; - } + if (options?.ref) { + description = `${description}${pad('$(git-commit)', description ? 2 : 0, 1)}${GitRevision.shorten(tag.sha)}`; - const item: TagQuickPickItem = { - label: `$(tag)${GlyphChars.Space}${tag.name}${options?.checked ? pad('$(check)', 2) : ''}`, - description: description, - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: tag, - current: false, - ref: tag.name, - remote: false, - }; + description = `${description ? `${description}${pad(GlyphChars.Dot, 2, 2)}` : ''}${tag.formattedDate}`; + } - return item; + if (options?.message) { + const message = emojify(tag.message); + description = description ? `${description}${pad(GlyphChars.Dot, 2, 2)}${message}` : message; } + + const item: TagQuickPickItem = { + label: `$(tag)${GlyphChars.Space}${tag.name}${options?.checked ? pad('$(check)', 2) : ''}`, + description: description, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: tag, + current: false, + ref: tag.name, + remote: false, + }; + + return item; } export interface WorktreeQuickPickItem extends QuickPickItemOfT { @@ -443,60 +459,58 @@ export interface WorktreeQuickPickItem extends QuickPickItemOfT { readonly hasChanges: boolean | undefined; } -export namespace WorktreeQuickPickItem { - export function create( - worktree: GitWorktree, - picked?: boolean, - options?: { - alwaysShow?: boolean; - buttons?: QuickInputButton[]; - checked?: boolean; - message?: boolean; - path?: boolean; - type?: boolean; - status?: GitStatus; - }, - ) { - let description = ''; - if (options?.type) { - description = 'worktree'; - } - - if (options?.status != null) { - description += options.status.hasChanges - ? pad(`Uncommited changes (${options.status.getFormattedDiffStatus()})`, description ? 2 : 0, 0) - : pad('No changes', description ? 2 : 0, 0); - } - - let icon; - let label; - switch (worktree.type) { - case 'bare': - label = '(bare)'; - icon = '$(folder)'; - break; - case 'branch': - label = worktree.branch!; - icon = '$(git-branch)'; - break; - case 'detached': - label = GitRevision.shorten(worktree.sha); - icon = '$(git-commit)'; - break; - } +export function createWorktreeQuickPickItem( + worktree: GitWorktree, + picked?: boolean, + options?: { + alwaysShow?: boolean; + buttons?: QuickInputButton[]; + checked?: boolean; + message?: boolean; + path?: boolean; + type?: boolean; + status?: GitStatus; + }, +) { + let description = ''; + if (options?.type) { + description = 'worktree'; + } - const item: WorktreeQuickPickItem = { - label: `${icon}${GlyphChars.Space}${label}${options?.checked ? pad('$(check)', 2) : ''}`, - description: description, - detail: options?.path ? `In $(folder) ${worktree.friendlyPath}` : undefined, - alwaysShow: options?.alwaysShow, - buttons: options?.buttons, - picked: picked, - item: worktree, - opened: worktree.opened, - hasChanges: options?.status?.hasChanges, - }; + if (options?.status != null) { + description += options.status.hasChanges + ? pad(`Uncommited changes (${options.status.getFormattedDiffStatus()})`, description ? 2 : 0, 0) + : pad('No changes', description ? 2 : 0, 0); + } - return item; + let icon; + let label; + switch (worktree.type) { + case 'bare': + label = '(bare)'; + icon = '$(folder)'; + break; + case 'branch': + label = worktree.branch!; + icon = '$(git-branch)'; + break; + case 'detached': + label = GitRevision.shorten(worktree.sha); + icon = '$(git-commit)'; + break; } + + const item: WorktreeQuickPickItem = { + label: `${icon}${GlyphChars.Space}${label}${options?.checked ? pad('$(check)', 2) : ''}`, + description: description, + detail: options?.path ? `In $(folder) ${worktree.friendlyPath}` : undefined, + alwaysShow: options?.alwaysShow, + buttons: options?.buttons, + picked: picked, + item: worktree, + opened: worktree.opened, + hasChanges: options?.status?.hasChanges, + }; + + return item; } diff --git a/src/quickpicks/referencePicker.ts b/src/quickpicks/referencePicker.ts index 11a3637..9ab1148 100644 --- a/src/quickpicks/referencePicker.ts +++ b/src/quickpicks/referencePicker.ts @@ -9,8 +9,8 @@ import { GitReference } from '../git/models/reference'; import type { GitTag, TagSortOptions } from '../git/models/tag'; import type { KeyboardScope, Keys } from '../keyboard'; import { getQuickPickIgnoreFocusOut } from '../system/utils'; -import type { BranchQuickPickItem, TagQuickPickItem } from './items/gitCommands'; -import { RefQuickPickItem } from './items/gitCommands'; +import type { BranchQuickPickItem, RefQuickPickItem, TagQuickPickItem } from './items/gitCommands'; +import { createRefQuickPickItem } from './items/gitCommands'; export type ReferencesQuickPickItem = BranchQuickPickItem | TagQuickPickItem | RefQuickPickItem; @@ -190,11 +190,11 @@ export namespace ReferencePicker { } if (include & ReferencesQuickPickIncludes.HEAD) { - items.splice(0, 0, RefQuickPickItem.create('HEAD', repoPath, undefined, { icon: true })); + items.splice(0, 0, createRefQuickPickItem('HEAD', repoPath, undefined, { icon: true })); } if (include & ReferencesQuickPickIncludes.WorkingTree) { - items.splice(0, 0, RefQuickPickItem.create('', repoPath, undefined, { icon: true })); + items.splice(0, 0, createRefQuickPickItem('', repoPath, undefined, { icon: true })); } return items; diff --git a/src/quickpicks/repositoryPicker.ts b/src/quickpicks/repositoryPicker.ts index 253975d..c937720 100644 --- a/src/quickpicks/repositoryPicker.ts +++ b/src/quickpicks/repositoryPicker.ts @@ -5,7 +5,8 @@ import type { Repository } from '../git/models/repository'; import { map } from '../system/iterable'; import { getQuickPickIgnoreFocusOut } from '../system/utils'; import { CommandQuickPickItem } from './items/common'; -import { RepositoryQuickPickItem } from './items/gitCommands'; +import type { RepositoryQuickPickItem } from './items/gitCommands'; +import { createRepositoryQuickPickItem } from './items/gitCommands'; export namespace RepositoryPicker { export async function getBestRepositoryOrShow( @@ -50,7 +51,7 @@ export namespace RepositoryPicker { ): Promise { const items = await Promise.all>([ ...map(repositories ?? Container.instance.git.openRepositories, r => - RepositoryQuickPickItem.create(r, undefined, { branch: true, status: true }), + createRepositoryQuickPickItem(r, undefined, { branch: true, status: true }), ), ]); diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 14a7711..7cae2b0 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -26,7 +26,6 @@ import { import { debug } from '../system/decorators/log'; import { sequentialize } from '../system/function'; import { OpenWorkspaceLocation } from '../system/utils'; -import { runGitCommandInTerminal } from '../terminal'; import type { BranchesNode } from './nodes/branchesNode'; import { BranchNode } from './nodes/branchNode'; import { BranchTrackingStatusNode } from './nodes/branchTrackingStatusNode'; @@ -219,6 +218,7 @@ export class ViewCommands { registerViewCommand('gitlens.views.switchToTag', this.switchTo, this); registerViewCommand('gitlens.views.addRemote', this.addRemote, this); registerViewCommand('gitlens.views.pruneRemote', this.pruneRemote, this); + registerViewCommand('gitlens.views.removeRemote', this.removeRemote, this); registerViewCommand('gitlens.views.stageDirectory', this.stageDirectory, this); registerViewCommand('gitlens.views.stageFile', this.stageFile, this); @@ -271,8 +271,6 @@ export class ViewCommands { registerViewCommand('gitlens.views.revert', this.revert, this); registerViewCommand('gitlens.views.undoCommit', this.undoCommit, this); - registerViewCommand('gitlens.views.terminalRemoveRemote', this.terminalRemoveRemote, this); - registerViewCommand('gitlens.views.createPullRequest', this.createPullRequest, this); registerViewCommand('gitlens.views.openPullRequest', this.openPullRequest, this); @@ -570,13 +568,20 @@ export class ViewCommands { } @debug() - private async pruneRemote(node: RemoteNode) { + private pruneRemote(node: RemoteNode) { if (!(node instanceof RemoteNode)) return Promise.resolve(); return GitActions.Remote.prune(node.repo, node.remote.name); } @debug() + private async removeRemote(node: RemoteNode) { + if (!(node instanceof RemoteNode)) return Promise.resolve(); + + return GitActions.Remote.remove(node.repo, node.remote.name); + } + + @debug() private publishBranch(node: BranchNode | BranchTrackingStatusNode) { if (node instanceof BranchNode || node instanceof BranchTrackingStatusNode) { return GitActions.push(node.repoPath, undefined, node.branch); @@ -1247,10 +1252,4 @@ export class ViewCommands { return GitActions.Commit.openFilesAtRevision(node.commit); } - - private terminalRemoveRemote(node: RemoteNode) { - if (!(node instanceof RemoteNode)) return; - - runGitCommandInTerminal('remote', `remove ${node.remote.name}`, node.remote.repoPath); - } }