diff --git a/CHANGELOG.md b/CHANGELOG.md index 84815f0..82ed445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Improves tooltips in the graph: - Author and avatar tooltips now also show the contributor's email address, if available - Date tooltips now always show both the absolute date and relative date +- Improves the Worktree creation experience + - 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 ### Changed diff --git a/package.json b/package.json index 30853c3..712cbf9 100644 --- a/package.json +++ b/package.json @@ -1665,6 +1665,27 @@ "scope": "resource", "order": 11 }, + "gitlens.worktrees.openAfterCreate": { + "type": "string", + "default": "prompt", + "enum": [ + "always", + "alwaysNewWindow", + "onlyWhenEmpty", + "never", + "prompt" + ], + "enumDescriptions": [ + "Always open the new worktree in the current window", + "Always open the new worktree in a new window", + "Only open the new worktree in the current window when no folder is opened", + "Never open the new worktree", + "Always prompt to open the new worktree" + ], + "markdownDescription": "Specifies how and when to open a worktree after it is created", + "scope": "resource", + "order": 12 + }, "gitlens.views.worktrees.showBranchComparison": { "type": [ "boolean", diff --git a/src/commands/git/worktree.ts b/src/commands/git/worktree.ts index 285e3d9..6fbdc7a 100644 --- a/src/commands/git/worktree.ts +++ b/src/commands/git/worktree.ts @@ -1,5 +1,6 @@ import type { MessageItem } from 'vscode'; import { QuickInputButtons, Uri, window } from 'vscode'; +import type { Config } from '../../configuration'; import { configuration } from '../../configuration'; import type { Container } from '../../container'; import { PlusFeatures } from '../../features'; @@ -206,6 +207,9 @@ export class WorktreeGitCommand extends QuickCommand { } } + // Ensure we use the "main" repository if we are in a worktree already + state.repo = await state.repo.getMainRepository(); + const result = yield* ensureAccessStep(state as any, context, PlusFeatures.Worktrees); if (result === StepResult.Break) break; @@ -365,12 +369,62 @@ export class WorktreeGitCommand extends QuickCommand { ); try { - await state.repo.createWorktree(uri, { + const worktree = await state.repo.createWorktree(uri, { commitish: state.reference?.name, createBranch: state.flags.includes('-b') ? state.createBranch : undefined, detach: state.flags.includes('--detach'), force: state.flags.includes('--force'), }); + + void GitActions.Worktree.reveal(worktree, { + select: true, + focus: true, + }); + + if (worktree == null) return; + + type OpenAction = Config['worktrees']['openAfterCreate'] | 'addToWorkspace'; + let action: OpenAction = configuration.get('worktrees.openAfterCreate'); + if (action === 'never') return; + + queueMicrotask(async () => { + if (action === 'prompt') { + type ActionMessageItem = MessageItem & { action: OpenAction }; + const open: ActionMessageItem = { title: 'Open', action: 'always' }; + const openNewWindow: ActionMessageItem = { + title: 'Open in New Window', + action: 'alwaysNewWindow', + }; + const addToWorkspace: ActionMessageItem = { + title: 'Add to Workspace', + action: 'addToWorkspace', + }; + const cancel: ActionMessageItem = { title: 'Cancel', isCloseAffordance: true, action: 'never' }; + + const result = await window.showInformationMessage( + `Would you like to open the new worktree, or add it to the current workspace?`, + { modal: true }, + open, + openNewWindow, + addToWorkspace, + cancel, + ); + + action = result?.action ?? 'never'; + } + + switch (action) { + case 'always': + GitActions.Worktree.open(worktree, { location: OpenWorkspaceLocation.CurrentWindow }); + break; + case 'alwaysNewWindow': + GitActions.Worktree.open(worktree, { location: OpenWorkspaceLocation.NewWindow }); + break; + case 'addToWorkspace': + GitActions.Worktree.open(worktree, { location: OpenWorkspaceLocation.AddToWorkspace }); + break; + } + }); } catch (ex) { if ( WorktreeCreateError.is(ex, WorktreeCreateErrorReason.AlreadyCheckedOut) && @@ -505,6 +559,8 @@ export class WorktreeGitCommand extends QuickCommand { 60, ); + const isRemoteBranch = state.reference?.refType === 'branch' && state.reference?.remote; + const step: QuickPickStep> = QuickCommand.createConfirmStep( appendReposToTitle(`Confirm ${context.title}`, state, context), [ @@ -512,7 +568,7 @@ export class WorktreeGitCommand extends QuickCommand { state.flags, [], { - label: context.title, + label: isRemoteBranch ? 'Create Local Branch and Worktree' : context.title, description: ` for ${GitReference.toString(state.reference)}`, detail: `Will create worktree in $(folder) ${recommendedFriendlyPath}`, }, @@ -522,7 +578,9 @@ export class WorktreeGitCommand extends QuickCommand { state.flags, ['-b'], { - label: 'Create New Branch and Worktree', + label: isRemoteBranch + ? 'Create New Local Branch and Worktree' + : 'Create New Branch and Worktree', description: ` from ${GitReference.toString(state.reference)}`, detail: `Will create worktree in $(folder) ${recommendedNewBranchFriendlyPath}`, }, @@ -535,7 +593,9 @@ export class WorktreeGitCommand extends QuickCommand { state.flags, ['--direct'], { - label: `${context.title} (directly in folder)`, + label: `${ + isRemoteBranch ? 'Create Local Branch and Worktree' : context.title + } (directly in folder)`, description: ` for ${GitReference.toString(state.reference)}`, detail: `Will create worktree directly in $(folder) ${pickedFriendlyPath}`, }, @@ -545,7 +605,11 @@ export class WorktreeGitCommand extends QuickCommand { state.flags, ['-b', '--direct'], { - label: 'Create New Branch and Worktree (directly in folder)', + label: `${ + isRemoteBranch + ? 'Create New Local Branch and Worktree' + : 'Create New Branch and Worktree' + } (directly in folder)`, description: ` from ${GitReference.toString(state.reference)}`, detail: `Will create worktree directly in $(folder) ${pickedFriendlyPath}`, }, diff --git a/src/commands/gitCommands.actions.ts b/src/commands/gitCommands.actions.ts index 757a776..d0f3b62 100644 --- a/src/commands/gitCommands.actions.ts +++ b/src/commands/gitCommands.actions.ts @@ -169,6 +169,10 @@ export namespace GitActions { const node = view.canReveal ? await view.revealBranch(branch, options) : await Container.instance.repositoriesView.revealBranch(branch, options); + + if (node == null) { + void view.show({ preserveFocus: !options?.focus }); + } return node; } } @@ -745,6 +749,7 @@ export namespace GitActions { if (node != null) return node; } + void views[0].show({ preserveFocus: !options?.focus }); return undefined; } @@ -809,6 +814,9 @@ export namespace GitActions { const node = view.canReveal ? await view.revealContributor(contributor, options) : await Container.instance.repositoriesView.revealContributor(contributor, options); + if (node == null) { + void view.show({ preserveFocus: !options?.focus }); + } return node; } } @@ -875,6 +883,9 @@ export namespace GitActions { const node = view.canReveal ? await view.revealRemote(remote, options) : await Container.instance.repositoriesView.revealRemote(remote, options); + if (node == null) { + void view.show({ preserveFocus: !options?.focus }); + } return node; } } @@ -892,6 +903,9 @@ export namespace GitActions { const node = view?.canReveal ? await view.revealRepository(repoPath, options) : await Container.instance.repositoriesView.revealRepository(repoPath, options); + if (node == null) { + void (view ?? Container.instance.repositoriesView).show({ preserveFocus: !options?.focus }); + } return node; } } @@ -943,6 +957,9 @@ export namespace GitActions { const node = view.canReveal ? await view.revealStash(stash, options) : await Container.instance.repositoriesView.revealStash(stash, options); + if (node == null) { + void view.show({ preserveFocus: !options?.focus }); + } return node; } @@ -990,6 +1007,9 @@ export namespace GitActions { const node = view.canReveal ? await view.revealTag(tag, options) : await Container.instance.repositoriesView.revealTag(tag, options); + if (node == null) { + void view.show({ preserveFocus: !options?.focus }); + } return node; } } @@ -1014,13 +1034,19 @@ export namespace GitActions { } export async function reveal( - worktree: GitWorktree, + worktree: GitWorktree | undefined, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }, ) { const view = Container.instance.worktreesView; - const node = view.canReveal - ? await view.revealWorktree(worktree, options) - : await Container.instance.repositoriesView.revealWorktree(worktree, options); + const node = + worktree != null + ? view.canReveal + ? await view.revealWorktree(worktree, options) + : await Container.instance.repositoriesView.revealWorktree(worktree, options) + : undefined; + if (node == null) { + void view.show({ preserveFocus: !options?.focus }); + } return node; } diff --git a/src/config.ts b/src/config.ts index 3f144f7..c503f90 100644 --- a/src/config.ts +++ b/src/config.ts @@ -173,6 +173,7 @@ export interface Config { }; worktrees: { defaultLocation: string | null; + openAfterCreate: 'always' | 'alwaysNewWindow' | 'onlyWhenEmpty' | 'never' | 'prompt'; promptForLocation: boolean; }; advanced: AdvancedConfig; diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 2de2855..f795e4c 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -4084,7 +4084,9 @@ export class LocalGitProvider implements GitProvider, Disposable { // If we didn't find it, check it as close to the file as possible (will find nested repos) tracked = Boolean(await this.git.ls_files(newRepoPath, newRelativePath)); if (tracked) { - repository = await this.container.git.getOrOpenRepository(Uri.file(path), true); + repository = await this.container.git.getOrOpenRepository(Uri.file(path), { + detectNested: true, + }); if (repository != null) { return splitPath(path, repository.path); } @@ -4109,7 +4111,9 @@ export class LocalGitProvider implements GitProvider, Disposable { const index = relativePath.indexOf('/'); if (index < 0 || index === relativePath.length - 1) return undefined; - const nested = await this.container.git.getOrOpenRepository(Uri.file(path), true); + const nested = await this.container.git.getOrOpenRepository(Uri.file(path), { + detectNested: true, + }); if (nested != null && nested !== repository) { [relativePath, repoPath] = splitPath(path, repository.path); repository = undefined; diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 605e47e..9c867b9 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -2003,7 +2003,10 @@ export class GitProviderService implements Disposable { } @log({ exit: r => `returned ${r?.path}` }) - async getOrOpenRepository(uri: Uri, detectNested?: boolean): Promise { + async getOrOpenRepository( + uri: Uri, + options?: { closeOnOpen?: boolean; detectNested?: boolean }, + ): Promise { const scope = getLogScope(); const path = getBestPath(uri); @@ -2012,7 +2015,7 @@ export class GitProviderService implements Disposable { let isDirectory: boolean | undefined; - detectNested = detectNested ?? configuration.get('detectNestedRepositories', uri); + const detectNested = options?.detectNested ?? configuration.get('detectNestedRepositories', uri); if (!detectNested) { if (repository != null) return repository; } else if (this._visitedPaths.has(path)) { @@ -2053,7 +2056,9 @@ export class GitProviderService implements Disposable { CoreGitConfiguration.AutoRepositoryDetection, ) ?? true; - const closed = autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors'; + const closed = + options?.closeOnOpen ?? + (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors'); Logger.log(scope, `Repository found in '${repoUri.toString(true)}'`); const repositories = provider.openRepository(root?.folder, repoUri, false, undefined, closed); diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 7aeab51..db730cd 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -624,6 +624,15 @@ export class Repository implements Disposable { return this._lastFetched ?? 0; } + @gate() + async getMainRepository(): Promise { + const gitDir = await this.getGitDir(); + if (gitDir?.commonUri == null) return this; + + // If the repository isn't already opened, then open it as a "closed" repo (won't show up in the UI) + return this.container.git.getOrOpenRepository(gitDir.commonUri, { closeOnOpen: true }); + } + getMergeStatus(): Promise { return this.container.git.getMergeStatus(this.path); } @@ -689,11 +698,13 @@ export class Repository implements Disposable { return this.container.git.getTags(this.path, options); } - createWorktree( + async createWorktree( uri: Uri, options?: { commitish?: string; createBranch?: string; detach?: boolean; force?: boolean }, - ): Promise { - return this.container.git.createWorktree(this.path, uri.fsPath, options); + ): Promise { + await this.container.git.createWorktree(this.path, uri.fsPath, options); + const url = uri.toString(); + return this.container.git.getWorktree(this.path, w => w.uri.toString() === url); } getWorktrees(): Promise { @@ -723,6 +734,10 @@ export class Repository implements Disposable { return branch?.upstream != null; } + async isWorktree(): Promise { + return (await this.getGitDir())?.commonUri != null; + } + @log() merge(...args: string[]) { this.runTerminalCommand('merge', ...args);