Skip to content

Commit baa4844

Browse files
authored
Merge pull request #10564 from gitbutlerapp/tear-branch
tear-branch
2 parents 04695b1 + 4d19692 commit baa4844

File tree

17 files changed

+313
-59
lines changed

17 files changed

+313
-59
lines changed

apps/desktop/src/components/BranchCard.svelte

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
contextMenu?: typeof BranchHeaderContextMenu;
7979
dropzones: DropzoneHandler[];
8080
numberOfCommits: number;
81+
numberOfBranchesInStack: number;
8182
onclick: () => void;
8283
menu?: Snippet<[{ rightClickTrigger: HTMLElement }]>;
8384
buttons?: Snippet;
@@ -161,7 +162,13 @@
161162
viewportId: 'board-viewport',
162163
data:
163164
args.type === 'stack-branch' && args.stackId
164-
? new BranchDropData(args.stackId, branchName, args.isConflicted, args.numberOfCommits)
165+
? new BranchDropData(
166+
args.stackId,
167+
branchName,
168+
args.isConflicted,
169+
args.numberOfBranchesInStack,
170+
args.numberOfCommits
171+
)
165172
: undefined,
166173
dropzoneRegistry,
167174
dragStateService

apps/desktop/src/components/BranchList.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
{reviewId}
207207
{prNumber}
208208
numberOfCommits={localAndRemoteCommits.length}
209+
numberOfBranchesInStack={branches.length}
209210
dropzones={[stackingReorderDropzoneManager.top(branchName)]}
210211
trackingBranch={branch.remoteTrackingBranch ?? undefined}
211212
readonly={!!branch.remoteTrackingBranch}

apps/desktop/src/components/MultiStackOfflaneDropzone.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { STACK_SERVICE } from '$lib/stacks/stackService.svelte';
88
import { UI_STATE } from '$lib/state/uiState.svelte';
99
import { inject } from '@gitbutler/core/context';
10+
import { TestId } from '@gitbutler/ui';
1011
import { focusable } from '@gitbutler/ui/focus/focusable';
1112
import { intersectionObserver } from '@gitbutler/ui/utils/intersectionObserver';
1213
import type { Snippet } from 'svelte';
@@ -32,6 +33,7 @@
3233

3334
<div
3435
class="hidden-dropzone"
36+
data-testid={TestId.StackOfflaneDropzone}
3537
use:focusable
3638
use:intersectionObserver={{
3739
callback: (entry) => {

apps/desktop/src/components/SnapshotCard.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@
115115
return { text: 'Move commit', icon: 'move-commit' };
116116
case 'MoveBranch':
117117
return { text: 'Move branch', icon: 'move-commit' };
118+
case 'TearOffBranch':
119+
return { text: 'Tear off branch', icon: 'move-commit' };
118120
case 'ReorderCommit':
119121
return { text: 'Reorder commit', icon: 'move-commit' };
120122
case 'InsertBlankCommit':

apps/desktop/src/lib/branches/dropHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export class BranchDropData {
66
readonly stackId: string,
77
readonly branchName: string,
88
readonly hasConflicts: boolean,
9+
readonly numberOfBranchesInStack: number,
910
readonly numberOfCommits: number
1011
) {}
1112

apps/desktop/src/lib/history/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type Operation =
2626
| 'UpdateCommitMessage'
2727
| 'MoveCommit'
2828
| 'MoveBranch'
29+
| 'TearOffBranch'
2930
| 'RestoreFromSnapshot'
3031
| 'ReorderCommit'
3132
| 'InsertBlankCommit'

apps/desktop/src/lib/stacks/dropHandler.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { BranchDropData } from '$lib/branches/dropHandler';
12
import { changesToDiffSpec } from '$lib/commits/utils';
23
import { ChangeDropData } from '$lib/dragging/draggables';
34
import StackMacros from '$lib/stacks/macros';
5+
import { handleMoveBranchResult } from '$lib/stacks/stack';
46
import { ensureValue } from '$lib/utils/validation';
57
import { chipToasts } from '@gitbutler/ui';
68
import type { DropzoneHandler } from '$lib/dragging/handler';
@@ -23,13 +25,25 @@ export class OutsideLaneDzHandler implements DropzoneHandler {
2325
this.macros = new StackMacros(this.projectId, this.stackService, this.uiState);
2426
}
2527

26-
accepts(data: unknown) {
28+
private acceptsChangeDropData(data: unknown): data is ChangeDropData {
2729
if (!(data instanceof ChangeDropData)) return false;
2830
if (data.selectionId.type === 'commit' && data.stackId === undefined) return false;
2931
return true;
3032
}
3133

32-
async ondrop(data: ChangeDropData) {
34+
private acceptsBranchDropData(data: unknown): data is BranchDropData {
35+
if (!(data instanceof BranchDropData)) return false;
36+
if (data.hasConflicts) return false;
37+
if (data.numberOfBranchesInStack <= 1) return false; // Can't tear off the last branch of a stack
38+
if (data.numberOfCommits === 0) return false; // TODO: Allow to rip empty branches
39+
return true;
40+
}
41+
42+
accepts(data: unknown) {
43+
return this.acceptsChangeDropData(data) || this.acceptsBranchDropData(data);
44+
}
45+
46+
async ondropChangeData(data: ChangeDropData) {
3347
switch (data.selectionId.type) {
3448
case 'branch': {
3549
const newBranchName = await this.stackService.fetchNewBranchName(this.projectId);
@@ -112,4 +126,28 @@ export class OutsideLaneDzHandler implements DropzoneHandler {
112126
}
113127
}
114128
}
129+
130+
async ondropBranchData(data: BranchDropData) {
131+
await this.stackService
132+
.tearOffBranch({
133+
projectId: this.projectId,
134+
sourceStackId: data.stackId,
135+
subjectBranchName: data.branchName
136+
})
137+
.then((result) => {
138+
handleMoveBranchResult(result);
139+
});
140+
}
141+
142+
async ondrop(data: unknown): Promise<void> {
143+
if (this.acceptsChangeDropData(data)) {
144+
await this.ondropChangeData(data);
145+
return;
146+
}
147+
148+
if (this.acceptsBranchDropData(data)) {
149+
await this.ondropBranchData(data);
150+
return;
151+
}
152+
}
115153
}

apps/desktop/src/lib/stacks/stack.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,16 @@ export type InteractiveIntegrationStep =
337337

338338
export type MoveBranchResult = {
339339
deletedStacks: string[];
340+
unappliedStacks: string[];
340341
};
342+
343+
export function handleMoveBranchResult(result: MoveBranchResult) {
344+
if (result.unappliedStacks.length > 0) {
345+
showToast({
346+
testId: TestId.StacksUnappliedToast,
347+
title: 'Heads up: We had to unapply some stacks to move this branch',
348+
message: `It seems that the branch moved couldn't be applied cleanly alongside your other ${result.unappliedStacks.length} ${result.unappliedStacks.length === 1 ? 'stack' : 'stacks'}.
349+
You can always re-apply them later from the branches page.`
350+
});
351+
}
352+
}

apps/desktop/src/lib/stacks/stackService.svelte.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,10 @@ export class StackService {
755755
return this.api.endpoints.moveBranch.mutate;
756756
}
757757

758+
get tearOffBranch() {
759+
return this.api.endpoints.tearOffBranch.mutate;
760+
}
761+
758762
get integrateUpstreamCommits() {
759763
return this.api.endpoints.integrateUpstreamCommits.useMutation();
760764
}
@@ -1485,9 +1489,7 @@ function injectEndpoints(api: ClientState['backendApi'], uiState: UiState) {
14851489
},
14861490
query: (args) => args,
14871491
invalidatesTags: (result, _error, args) => {
1488-
if (result === undefined) return [];
1489-
1490-
if (result.deletedStacks.includes(args.sourceStackId)) {
1492+
if (result?.deletedStacks.includes(args.sourceStackId)) {
14911493
// The source stack was deleted, so we need to invalidate the list of stacks.
14921494
return [
14931495
invalidatesList(ReduxTag.Stacks),
@@ -1503,6 +1505,27 @@ function injectEndpoints(api: ClientState['backendApi'], uiState: UiState) {
15031505
];
15041506
}
15051507
}),
1508+
tearOffBranch: build.mutation<
1509+
MoveBranchResult,
1510+
{
1511+
projectId: string;
1512+
sourceStackId: string;
1513+
subjectBranchName: string;
1514+
}
1515+
>({
1516+
extraOptions: {
1517+
command: 'tear_off_branch',
1518+
actionName: 'Tear Off Branch'
1519+
},
1520+
query: (args) => args,
1521+
invalidatesTags: (_result, _error, args) => {
1522+
return [
1523+
invalidatesList(ReduxTag.Stacks),
1524+
invalidatesList(ReduxTag.WorktreeChanges), // Moving commits can cause conflicts
1525+
invalidatesItem(ReduxTag.StackDetails, args.sourceStackId)
1526+
];
1527+
}
1528+
}),
15061529
integrateUpstreamCommits: build.mutation<
15071530
void,
15081531
{

crates/but-api/src/commands/virtual_branches.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,20 @@ pub fn move_branch(
502502
.map_err(Into::into)
503503
}
504504

505+
#[api_cmd]
506+
#[tauri::command(async)]
507+
#[instrument(err(Debug))]
508+
pub fn tear_off_branch(
509+
project_id: ProjectId,
510+
source_stack_id: StackId,
511+
subject_branch_name: String,
512+
) -> Result<MoveBranchResult, Error> {
513+
let project = gitbutler_project::get(project_id)?;
514+
let ctx = CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?;
515+
gitbutler_branch_actions::tear_off_branch(&ctx, source_stack_id, subject_branch_name.as_str())
516+
.map_err(Into::into)
517+
}
518+
505519
#[api_cmd]
506520
#[tauri::command(async)]
507521
#[instrument(err(Debug))]

0 commit comments

Comments
 (0)