diff --git a/src/commands/git/switch.ts b/src/commands/git/switch.ts
index 8c4374da035c5..79e487d68f428 100644
--- a/src/commands/git/switch.ts
+++ b/src/commands/git/switch.ts
@@ -38,7 +38,7 @@ interface State {
reference: GitReference;
createBranch?: string;
fastForwardTo?: GitReference;
- skipWorktreeConfirmations?: boolean;
+ worktreeDefaultOpen?: 'new' | 'current';
}
type ConfirmationChoice =
@@ -223,7 +223,7 @@ export class SwitchGitCommand extends QuickCommand {
openOnly: true,
overrides: {
disallowBack: true,
- confirmation: state.skipWorktreeConfirmations
+ confirmation: state.worktreeDefaultOpen
? undefined
: {
title: `Confirm Switch to Worktree \u2022 ${getReferenceLabel(
@@ -241,12 +241,12 @@ export class SwitchGitCommand extends QuickCommand {
},
onWorkspaceChanging: state.onWorkspaceChanging,
repo: state.repos[0],
- skipWorktreeConfirmations: state.skipWorktreeConfirmations,
+ worktreeDefaultOpen: state.worktreeDefaultOpen,
},
},
this.pickedVia,
);
- if (worktreeResult === StepResultBreak && !state.skipWorktreeConfirmations) continue;
+ if (worktreeResult === StepResultBreak && !state.worktreeDefaultOpen) continue;
endSteps(state);
return;
@@ -263,7 +263,7 @@ export class SwitchGitCommand extends QuickCommand {
state.createBranch = undefined;
context.promptToCreateBranch = false;
- if (state.skipWorktreeConfirmations) {
+ if (state.worktreeDefaultOpen) {
state.reference = context.canSwitchToLocalBranch;
continue outer;
}
@@ -273,7 +273,7 @@ export class SwitchGitCommand extends QuickCommand {
}
if (
- state.skipWorktreeConfirmations ||
+ state.worktreeDefaultOpen ||
this.confirm(context.promptToCreateBranch || context.canSwitchToLocalBranch ? true : state.confirm)
) {
const result = yield* this.confirmStep(state as SwitchStepState, context);
@@ -330,12 +330,12 @@ export class SwitchGitCommand extends QuickCommand {
result === 'switchToNewBranchViaWorktree' ? state.createBranch : undefined,
repo: state.repos[0],
onWorkspaceChanging: state.onWorkspaceChanging,
- skipWorktreeConfirmations: state.skipWorktreeConfirmations,
+ worktreeDefaultOpen: state.worktreeDefaultOpen,
},
},
this.pickedVia,
);
- if (worktreeResult === StepResultBreak && !state.skipWorktreeConfirmations) continue outer;
+ if (worktreeResult === StepResultBreak && !state.worktreeDefaultOpen) continue outer;
endSteps(state);
return;
@@ -355,7 +355,7 @@ export class SwitchGitCommand extends QuickCommand {
const isRemoteBranch = isBranchReference(state.reference) && state.reference.remote;
type StepType = QuickPickItemOfT;
- if (state.skipWorktreeConfirmations && state.repos.length === 1) {
+ if (state.worktreeDefaultOpen && state.repos.length === 1) {
if (isLocalBranch) {
return 'switchViaWorktree';
} else if (!state.createBranch && context.canSwitchToLocalBranch != null) {
diff --git a/src/commands/git/worktree.ts b/src/commands/git/worktree.ts
index e9fc71b3b37ee..fbd12cc9d2873 100644
--- a/src/commands/git/worktree.ts
+++ b/src/commands/git/worktree.ts
@@ -103,7 +103,7 @@ interface CreateState {
};
onWorkspaceChanging?: ((isNewWorktree?: boolean) => Promise) | ((isNewWorktree?: boolean) => void);
- skipWorktreeConfirmations?: boolean;
+ worktreeDefaultOpen?: 'new' | 'current';
}
type DeleteFlags = '--force' | '--delete-branches';
@@ -141,7 +141,7 @@ interface OpenState {
onWorkspaceChanging?: ((isNewWorktree?: boolean) => Promise) | ((isNewWorktree?: boolean) => void);
isNewWorktree?: boolean;
- skipWorktreeConfirmations?: boolean;
+ worktreeDefaultOpen?: 'new' | 'current';
}
interface CopyChangesState {
@@ -632,7 +632,7 @@ export class WorktreeGitCommand extends QuickCommand {
openOnly: true,
overrides: { disallowBack: true },
isNewWorktree: true,
- skipWorktreeConfirmations: state.skipWorktreeConfirmations,
+ worktreeDefaultOpen: state.worktreeDefaultOpen,
onWorkspaceChanging: state.onWorkspaceChanging,
} satisfies OpenStepState,
context,
@@ -739,7 +739,7 @@ export class WorktreeGitCommand extends QuickCommand {
const confirmations: StepType[] = [];
if (!createDirectlyInFolder) {
- if (state.skipWorktreeConfirmations) {
+ if (state.worktreeDefaultOpen) {
return [defaultOption.context, defaultOption.item];
}
@@ -1101,15 +1101,21 @@ export class WorktreeGitCommand extends QuickCommand {
detail: 'Will open the worktree in a new window',
});
- if (state.skipWorktreeConfirmations) {
+ const currentWindowItem = createFlagsQuickPickItem(state.flags, [], {
+ label: 'Open Worktree',
+ detail: 'Will open the worktree in the current window',
+ });
+
+ if (state.worktreeDefaultOpen === 'new') {
return newWindowItem.item;
}
+ if (state.worktreeDefaultOpen === 'current') {
+ return currentWindowItem.item;
+ }
+
const confirmations: StepType[] = [
- createFlagsQuickPickItem(state.flags, [], {
- label: 'Open Worktree',
- detail: 'Will open the worktree in the current window',
- }),
+ currentWindowItem,
newWindowItem,
createFlagsQuickPickItem(state.flags, ['--add-to-workspace'], {
label: `Add Worktree to Workspace`,
diff --git a/src/constants.commands.ts b/src/constants.commands.ts
index 4b14aea516723..c24bbfd61bef7 100644
--- a/src/constants.commands.ts
+++ b/src/constants.commands.ts
@@ -148,6 +148,8 @@ type InternalGraphWebviewCommands =
| 'gitlens.graph.skipPausedOperation';
type InternalHomeWebviewCommands =
+ | 'gitlens.home.deleteBranchOrWorktree'
+ | 'gitlens.home.pushBranch'
| 'gitlens.home.openMergeTargetComparison'
| 'gitlens.home.openPullRequestChanges'
| 'gitlens.home.openPullRequestComparison'
diff --git a/src/env/node/git/sub-providers/branches.ts b/src/env/node/git/sub-providers/branches.ts
index 9c525c8e3c183..137bd3cf8c9a3 100644
--- a/src/env/node/git/sub-providers/branches.ts
+++ b/src/env/node/git/sub-providers/branches.ts
@@ -420,12 +420,39 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
} catch {}
// Cherry-pick detection (handles cherry-picks, rebases, etc)
- const data = await this.git.exec({ cwd: repoPath }, 'cherry', '--abbrev', '-v', into.name, branch.name);
+ let data = await this.git.exec({ cwd: repoPath }, 'cherry', '--abbrev', '-v', into.name, branch.name);
// Check if there are no lines or all lines startwith a `-` (i.e. likely merged)
- if (!data || data.split('\n').every(l => l.startsWith('-'))) {
+ if (
+ !data ||
+ data
+ .trim()
+ .split('\n')
+ .every(l => l.startsWith('-'))
+ ) {
return { merged: true, confidence: 'high' };
}
+ // Attempt to detect squash merges by checking if the diff of the branch can be cleanly removed from the target
+ const mergeBase = await this.provider.refs.getMergeBase(repoPath, into.name, branch.name);
+ data = await this.git.exec({ cwd: repoPath }, 'diff', mergeBase, branch.name);
+ if (data?.length) {
+ // Create a temporary index file
+ await using disposableIndex = await this.provider.staging!.createTemporaryIndex(repoPath, into.name);
+ const { env } = disposableIndex;
+
+ data = await this.git.exec(
+ { cwd: repoPath, env: env, stdin: data },
+ 'apply',
+ '--cached',
+ '--reverse',
+ '--check',
+ '-',
+ );
+ if (!data?.trim().length) {
+ return { merged: true, confidence: 'medium' };
+ }
+ }
+
return { merged: false };
} catch (ex) {
Logger.error(ex, scope);
diff --git a/src/plus/launchpad/launchpad.ts b/src/plus/launchpad/launchpad.ts
index 58af70ff2b233..b7d7f824f0745 100644
--- a/src/plus/launchpad/launchpad.ts
+++ b/src/plus/launchpad/launchpad.ts
@@ -391,7 +391,7 @@ export class LaunchpadCommand extends QuickCommand {
void this.container.launchpad.switchTo(state.item);
break;
case 'open-worktree':
- void this.container.launchpad.switchTo(state.item, { skipWorktreeConfirmations: true });
+ void this.container.launchpad.switchTo(state.item, { openInWorktree: true });
break;
case 'switch-and-code-suggest':
case 'code-suggest':
@@ -900,7 +900,7 @@ export class LaunchpadCommand extends QuickCommand {
case OpenWorktreeInNewWindowQuickInputButton:
this.sendItemActionTelemetry('open-worktree', item, group, context);
- await this.container.launchpad.switchTo(item, { skipWorktreeConfirmations: true });
+ await this.container.launchpad.switchTo(item, { openInWorktree: true });
break;
}
diff --git a/src/plus/launchpad/launchpadProvider.ts b/src/plus/launchpad/launchpadProvider.ts
index 3dd752cec9846..0235f8c54b194 100644
--- a/src/plus/launchpad/launchpadProvider.ts
+++ b/src/plus/launchpad/launchpadProvider.ts
@@ -467,7 +467,7 @@ export class LaunchpadProvider implements Disposable {
@log({ args: { 0: i => `${i.id} (${i.provider.name} ${i.type})` } })
async switchTo(
item: LaunchpadItem,
- options?: { skipWorktreeConfirmations?: boolean; startCodeSuggestion?: boolean },
+ options?: { openInWorktree?: boolean; startCodeSuggestion?: boolean },
): Promise {
if (item.openRepository?.localBranch?.current) {
void showInspectView({
@@ -483,7 +483,7 @@ export class LaunchpadProvider implements Disposable {
item,
options?.startCodeSuggestion
? DeepLinkActionType.SwitchToAndSuggestPullRequest
- : options?.skipWorktreeConfirmations
+ : options?.openInWorktree
? DeepLinkActionType.SwitchToPullRequestWorktree
: DeepLinkActionType.SwitchToPullRequest,
);
diff --git a/src/uris/deepLinks/deepLink.ts b/src/uris/deepLinks/deepLink.ts
index c3ac5b7a85d2f..7e3a16093bbaf 100644
--- a/src/uris/deepLinks/deepLink.ts
+++ b/src/uris/deepLinks/deepLink.ts
@@ -45,6 +45,7 @@ export const DeepLinkCommandTypeToCommand = new Map
@@ -301,6 +321,29 @@ export class GlMergeTargetStatus extends LitElement {
${this.mergedStatus.confidence !== 'highest' ? 'likely ' : ''}been merged into its merge
target's local branch ${renderBranchName(this.mergedStatus.localBranchOnly.name)}.
+
+ Push ${renderBranchName(this.mergedStatus.localBranchOnly.name)}
+ Delete
+ ${this.branch.worktree != null && !this.branch.worktree.isDefault
+ ? 'Worktree'
+ : 'Branch'}
+ ${renderBranchName(this.branch.name, this.branch.worktree != null)}
+
`;
}
@@ -318,6 +361,18 @@ export class GlMergeTargetStatus extends LitElement {
${this.mergedStatus.confidence !== 'highest' ? 'likely ' : ''}been merged into its merge target
${target}.
+
+ Delete
+ ${this.branch.worktree != null && !this.branch.worktree.isDefault ? 'Worktree' : 'Branch'}
+ ${renderBranchName(this.branch.name, this.branch.worktree != null)}
+
`;
}
diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts
index 2ef3336780db9..13440ab7533d7 100644
--- a/src/webviews/home/homeWebview.ts
+++ b/src/webviews/home/homeWebview.ts
@@ -1,5 +1,5 @@
import type { ConfigurationChangeEvent } from 'vscode';
-import { Disposable, Uri, window, workspace } from 'vscode';
+import { Disposable, env, Uri, window, workspace } from 'vscode';
import type { CreatePullRequestActionContext } from '../../api/gitlens';
import type { EnrichedAutolink } from '../../autolinks/models/autolinks';
import { getAvatarUriFromGravatarEmail } from '../../avatars';
@@ -35,6 +35,7 @@ import { getBranchTargetInfo } from '../../git/utils/-webview/branch.utils';
import { getReferenceFromBranch } from '../../git/utils/-webview/reference.utils';
import { sortBranches } from '../../git/utils/-webview/sorting';
import { getOpenedWorktreesByBranch, groupWorktreesByBranch } from '../../git/utils/-webview/worktree.utils';
+import { getBranchNameWithoutRemote } from '../../git/utils/branch.utils';
import { getComparisonRefsForPullRequest } from '../../git/utils/pullRequest.utils';
import { createRevisionRange } from '../../git/utils/revision.utils';
import type { AIModelChangeEvent } from '../../plus/ai/aiProviderService';
@@ -64,6 +65,8 @@ import { debounce } from '../../system/function/debounce';
import { filterMap } from '../../system/iterable';
import { getSettledValue } from '../../system/promise';
import { SubscriptionManager } from '../../system/subscriptionManager';
+import type { UriTypes } from '../../uris/deepLinks/deepLink';
+import { DeepLinkServiceState, DeepLinkType } from '../../uris/deepLinks/deepLink';
import type { ShowInCommitGraphCommandArgs } from '../plus/graph/protocol';
import type { Change } from '../plus/patchDetails/protocol';
import type { IpcMessage } from '../protocol';
@@ -318,6 +321,8 @@ export class HomeWebviewProvider implements WebviewProvider this.container.subscription.validate({ force: true }, src),
this,
),
+ registerCommand('gitlens.home.deleteBranchOrWorktree', this.deleteBranchOrWorktree, this),
+ registerCommand('gitlens.home.pushBranch', this.pushBranch, this),
registerCommand('gitlens.home.openMergeTargetComparison', this.mergeTargetCompare, this),
registerCommand('gitlens.home.openPullRequestChanges', this.pullRequestChanges, this),
registerCommand('gitlens.home.openPullRequestComparison', this.pullRequestCompare, this),
@@ -1167,6 +1172,94 @@ export class HomeWebviewProvider implements WebviewProvider b.id === ref.branchId);
+ if (branch == null) {
+ branch = await repo?.repo.git.branches().getBranch(ref.branchId);
+ }
+
+ const worktree = branch ? repo?.worktreesByBranch.get(branch.id) : undefined;
+ if (branch == null) return;
+
+ if (branch.current && mergeTarget != null && (!worktree || worktree.isDefault)) {
+ const mergeTargetLocalBranchName = getBranchNameWithoutRemote(mergeTarget.branchName);
+ const confirm = await window.showWarningMessage(
+ `Before deleting the current branch '${branch.name}', you will be switched to '${mergeTargetLocalBranchName}'.`,
+ { modal: true },
+ { title: 'Continue' },
+ );
+
+ if (confirm == null || confirm.title !== 'Continue') return;
+
+ await this.container.git.checkout(ref.repoPath, mergeTargetLocalBranchName);
+
+ void executeGitCommand({
+ command: 'branch',
+ state: {
+ subcommand: 'delete',
+ repo: ref.repoPath,
+ references: branch,
+ },
+ });
+ } else if (repo != null && branch != null && worktree != null && !worktree.isDefault) {
+ const commonRepo = await repo.repo.getCommonRepository();
+ const defaultWorktree = [...repo.worktreesByBranch.values()].find(w => w.isDefault);
+ if (defaultWorktree == null || commonRepo == null) return;
+
+ const confirm = await window.showWarningMessage(
+ `Before deleting the worktree for '${branch.name}', you will be switched to the default worktree.`,
+ { modal: true },
+ { title: 'Continue' },
+ );
+
+ if (confirm == null || confirm.title !== 'Continue') return;
+
+ const schemeOverride = configuration.get('deepLinks.schemeOverride');
+ const scheme = typeof schemeOverride === 'string' ? schemeOverride : env.uriScheme;
+ const deleteBranchDeepLink = {
+ url: `${scheme}://${this.container.context.extension.id}/${'link' satisfies UriTypes}/${
+ DeepLinkType.Repository
+ }/-/${DeepLinkType.Branch}/${encodeURIComponent(branch.name)}?path=${encodeURIComponent(
+ commonRepo.path,
+ )}&action=delete-branch`,
+ repoPath: commonRepo.path,
+ useProgress: false,
+ state: DeepLinkServiceState.GoToTarget,
+ };
+
+ void executeGitCommand({
+ command: 'worktree',
+ state: {
+ subcommand: 'open',
+ repo: defaultWorktree.repoPath,
+ worktree: defaultWorktree,
+ onWorkspaceChanging: async (_isNewWorktree?: boolean) =>
+ this.container.storage.store('deepLinks:pending', deleteBranchDeepLink),
+ worktreeDefaultOpen: 'current',
+ },
+ });
+ }
+ }
+
+ private pushBranch(ref: BranchRef) {
+ void this.container.git.push(ref.repoPath, {
+ reference: {
+ name: ref.branchName,
+ ref: ref.branchId,
+ refType: 'branch',
+ remote: false,
+ repoPath: ref.repoPath,
+ upstream: ref.branchUpstreamName
+ ? {
+ name: ref.branchUpstreamName,
+ missing: false,
+ }
+ : undefined,
+ },
+ });
+ }
+
private mergeTargetCompare(ref: BranchAndTargetRefs) {
return this.container.views.searchAndCompare.compare(ref.repoPath, ref.branchName, ref.mergeTargetName);
}
@@ -1401,7 +1494,7 @@ function getOverviewBranchesCore(
timestamp: timestamp,
status: branch.status,
upstream: branch.upstream,
- worktree: wt ? { name: wt.name, uri: wt.uri.toString() } : undefined,
+ worktree: wt ? { name: wt.name, uri: wt.uri.toString(), isDefault: wt.isDefault } : undefined,
});
}
diff --git a/src/webviews/home/protocol.ts b/src/webviews/home/protocol.ts
index ce5a8a03dc992..0106b72bf03d7 100644
--- a/src/webviews/home/protocol.ts
+++ b/src/webviews/home/protocol.ts
@@ -181,6 +181,7 @@ export interface GetOverviewBranch {
worktree?: {
name: string;
uri: string;
+ isDefault: boolean;
};
}
@@ -320,6 +321,11 @@ export interface BranchRef {
repoPath: string;
branchId: string;
branchName: string;
+ branchUpstreamName?: string;
+ worktree?: {
+ name: string;
+ isDefault: boolean;
+ };
}
export interface BranchAndTargetRefs extends BranchRef {
diff --git a/src/webviews/plus/graph/graphWebview.ts b/src/webviews/plus/graph/graphWebview.ts
index 20d33a16d2812..96ba2ac0d7499 100644
--- a/src/webviews/plus/graph/graphWebview.ts
+++ b/src/webviews/plus/graph/graphWebview.ts
@@ -3891,7 +3891,7 @@ export class GraphWebviewProvider implements WebviewProvider