Skip to content

Commit d577ad9

Browse files
authored
Merge pull request #10374 from gitbutlerapp/move-branch
Move branches round and around and around and around
2 parents 688b2b3 + 65c0480 commit d577ad9

File tree

21 files changed

+611
-18
lines changed

21 files changed

+611
-18
lines changed

apps/desktop/src/components/BranchCard.svelte

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
import PrNumberUpdater from '$components/PrNumberUpdater.svelte';
1212
import ReduxResult from '$components/ReduxResult.svelte';
1313
import CodegenBadge from '$components/codegen/CodegenBadge.svelte';
14+
import { BranchDropData } from '$lib/branches/dropHandler';
1415
import { CLAUDE_CODE_SERVICE } from '$lib/codegen/claude';
1516
import { CodegenRuleDropData, CodegenRuleDropHandler } from '$lib/codegen/dropzone';
1617
import { useGoToCodegenPage } from '$lib/codegen/redirect.svelte';
1718
import { MoveCommitDzHandler } from '$lib/commits/dropHandler';
18-
import { draggableChips } from '$lib/dragging/draggable';
19+
import { draggableBranch, draggableChips } from '$lib/dragging/draggable';
1920
import { DROPZONE_REGISTRY } from '$lib/dragging/registry';
2021
import { ReorderCommitDzHandler } from '$lib/dragging/stackingReorderDropzoneManager';
2122
import { DEFAULT_FORGE_FACTORY } from '$lib/forge/forgeFactory.svelte';
@@ -76,6 +77,7 @@
7677
isConflicted: boolean;
7778
contextMenu?: typeof BranchHeaderContextMenu;
7879
dropzones: DropzoneHandler[];
80+
numberOfCommits: number;
7981
onclick: () => void;
8082
menu?: Snippet<[{ rightClickTrigger: HTMLElement }]>;
8183
buttons?: Snippet;
@@ -151,6 +153,19 @@
151153
class:draft={args.type === 'draft-branch'}
152154
data-series-name={branchName}
153155
data-testid={TestId.BranchCard}
156+
data-remove-from-panning
157+
use:draggableBranch={{
158+
disabled: args.type !== 'stack-branch' || args.isConflicted,
159+
label: branchName,
160+
pushStatus: args.type === 'stack-branch' ? args.pushStatus : undefined,
161+
viewportId: 'board-viewport',
162+
data:
163+
args.type === 'stack-branch' && args.stackId
164+
? new BranchDropData(args.stackId, branchName, args.isConflicted, args.numberOfCommits)
165+
: undefined,
166+
dropzoneRegistry,
167+
dragStateService
168+
}}
154169
>
155170
{#if args.type === 'stack-branch'}
156171
{@const moveHandler = args.stackId
@@ -174,13 +189,11 @@
174189
>
175190
{#snippet overlay({ hovered, activated, handler })}
176191
{@const label =
177-
handler instanceof MoveCommitDzHandler
192+
handler instanceof MoveCommitDzHandler || handler instanceof CodegenRuleDropHandler
178193
? 'Move here'
179194
: handler instanceof ReorderCommitDzHandler
180195
? 'Reorder here'
181-
: handler instanceof CodegenRuleDropHandler
182-
? 'Move here'
183-
: 'Start commit'}
196+
: 'Start commit'}
184197
<CardOverlay {hovered} {activated} {label} />
185198
{/snippet}
186199

apps/desktop/src/components/BranchList.svelte

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
import BranchCommitList from '$components/BranchCommitList.svelte';
77
import BranchHeaderContextMenu from '$components/BranchHeaderContextMenu.svelte';
88
import ConflictResolutionConfirmModal from '$components/ConflictResolutionConfirmModal.svelte';
9+
import Dropzone from '$components/Dropzone.svelte';
10+
import LineOverlay from '$components/LineOverlay.svelte';
911
import PushButton from '$components/PushButton.svelte';
1012
import ReduxResult from '$components/ReduxResult.svelte';
1113
import { getColorFromCommitState, getIconFromCommitState } from '$components/lib';
14+
import { MoveBranchDzHandler } from '$lib/branches/dropHandler';
1215
import { REORDER_DROPZONE_FACTORY } from '$lib/dragging/stackingReorderDropzoneManager';
1316
import { editPatch } from '$lib/editMode/editPatchUtils';
1417
import { DEFAULT_FORGE_FACTORY } from '$lib/forge/forgeFactory.svelte';
@@ -124,6 +127,24 @@
124127
const canPublishPR = $derived(forge.current.authenticated);
125128
</script>
126129

130+
{#snippet branchInsertionDz(branchName: string)}
131+
{#if !isCommitting && stackId}
132+
{@const moveBranchHandler = new MoveBranchDzHandler(
133+
stackService,
134+
projectId,
135+
stackId,
136+
branchName
137+
)}
138+
<Dropzone handlers={[moveBranchHandler]}>
139+
{#snippet overlay({ hovered, activated })}
140+
<div data-testid={TestId.BranchListInsertionDropzone}>
141+
<LineOverlay advertize {hovered} {activated} />
142+
</div>
143+
{/snippet}
144+
</Dropzone>
145+
{/if}
146+
{/snippet}
147+
127148
<div class="branches-wrapper">
128149
{#each branches as branch, i}
129150
{@const branchName = branch.name}
@@ -165,6 +186,7 @@
165186
{@const lastUpdatedAt = branchDetails.lastUpdatedAt}
166187
{@const reviewId = branch.reviewId || undefined}
167188
{@const prNumber = branch.prNumber || undefined}
189+
{@render branchInsertionDz(branchName)}
168190
<BranchCard
169191
type="stack-branch"
170192
{projectId}
@@ -182,6 +204,7 @@
182204
{lastUpdatedAt}
183205
{reviewId}
184206
{prNumber}
207+
numberOfCommits={localAndRemoteCommits.length}
185208
dropzones={[stackingReorderDropzoneManager.top(branchName)]}
186209
trackingBranch={branch.remoteTrackingBranch ?? undefined}
187210
readonly={!!branch.remoteTrackingBranch}

apps/desktop/src/components/LineOverlay.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
interface Props {
55
hovered: boolean;
66
activated: boolean;
7+
advertize?: boolean;
78
yOffsetPx?: number;
89
}
910
10-
const { hovered, activated, yOffsetPx = 0 }: Props = $props();
11+
const { hovered, activated, advertize, yOffsetPx = 0 }: Props = $props();
1112
</script>
1213

1314
<div
1415
class="dropzone-target container"
1516
class:activated
17+
class:advertize
1618
class:hovered
1719
style:--y-offset="{pxToRem(yOffsetPx)}rem"
1820
>
@@ -35,6 +37,7 @@
3537
3638
height: var(--dropzone-height);
3739
margin-top: calc(var(--dropzone-overlap) * -1);
40+
transition: background-color 0.3s ease-in-out;
3841
3942
/* It is very important that all children are pointer-events: none */
4043
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
@@ -51,6 +54,12 @@
5154
background-color: var(--clr-theme-pop-element);
5255
}
5356
}
57+
58+
&:not(.hovered).advertize {
59+
& .indicator {
60+
background-color: var(--clr-theme-pop-soft-hover);
61+
}
62+
}
5463
}
5564
5665
.indicator {

apps/desktop/src/components/SnapshotCard.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@
113113
return { text: 'Update commit message', icon: 'edit' };
114114
case 'MoveCommit':
115115
return { text: 'Move commit', icon: 'move-commit' };
116+
case 'MoveBranch':
117+
return { text: 'Move branch', icon: 'move-commit' };
116118
case 'ReorderCommit':
117119
return { text: 'Reorder commit', icon: 'move-commit' };
118120
case 'InsertBlankCommit':
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { DropzoneHandler } from '$lib/dragging/handler';
2+
import type { StackService } from '$lib/stacks/stackService.svelte';
3+
4+
export class BranchDropData {
5+
constructor(
6+
readonly stackId: string,
7+
readonly branchName: string,
8+
readonly hasConflicts: boolean,
9+
readonly numberOfCommits: number
10+
) {}
11+
12+
print(): string {
13+
return `BranchDropData(${this.stackId}, ${this.branchName}, ${this.hasConflicts})`;
14+
}
15+
}
16+
17+
export class MoveBranchDzHandler implements DropzoneHandler {
18+
constructor(
19+
private readonly stackService: StackService,
20+
private readonly projectId: string,
21+
private readonly stackId: string,
22+
private readonly branchName: string
23+
) {}
24+
25+
print(): string {
26+
return `MoveBranchDzHandler(${this.projectId}, ${this.stackId}, ${this.branchName})`;
27+
}
28+
29+
accepts(data: unknown): boolean {
30+
return (
31+
data instanceof BranchDropData &&
32+
data.stackId !== this.stackId &&
33+
!data.hasConflicts &&
34+
data.numberOfCommits > 0 // TODO: If trying to move an empty branch, we should just delete the reference and recreate it.
35+
);
36+
}
37+
ondrop(data: BranchDropData): void {
38+
this.stackService.moveBranch({
39+
projectId: this.projectId,
40+
sourceStackId: data.stackId,
41+
subjectBranchName: data.branchName,
42+
targetBranchName: this.branchName,
43+
targetStackId: this.stackId
44+
});
45+
}
46+
}

apps/desktop/src/lib/dragging/draggable.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { getFileIcon } from '@gitbutler/ui/components/file/getFileIcon';
55
import iconsJson from '@gitbutler/ui/data/icons.json';
66
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
77
import type { DropzoneRegistry } from '$lib/dragging/registry';
8+
import type { PushStatus } from '$lib/stacks/stack';
89
import type { DragStateService } from '@gitbutler/ui/drag/dragStateService.svelte';
910

1011
// Added to element being dragged (not the clone that follows the cursor).
1112
const DRAGGING_CLASS = 'dragging';
1213

13-
type chipType = 'file' | 'hunk' | 'ai-session';
14+
type chipType = 'file' | 'hunk' | 'ai-session' | 'branch';
1415

1516
export type DraggableConfig = {
1617
readonly selector?: string;
@@ -26,6 +27,7 @@ export type DraggableConfig = {
2627
readonly chipType?: chipType;
2728
readonly dropzoneRegistry: DropzoneRegistry;
2829
readonly dragStateService?: DragStateService;
30+
readonly pushStatus?: PushStatus;
2931
};
3032

3133
function createElement<K extends keyof HTMLElementTagNameMap>(
@@ -241,6 +243,26 @@ function setupDragHandlers(
241243
};
242244
}
243245

246+
/////////////////////////////
247+
//// BRANCH DRAGGABLE ///////
248+
/////////////////////////////
249+
250+
function createBranchElement(label: string | undefined): HTMLDivElement {
251+
const cardEl = createElement('div', ['draggable-branch-card', 'text-15', 'text-bold'], label);
252+
253+
return cardEl;
254+
}
255+
256+
export function draggableBranch(node: HTMLElement, initialOpts: DraggableConfig) {
257+
function createClone(opts: DraggableConfig) {
258+
if (opts.disabled) return;
259+
return createBranchElement(opts.label);
260+
}
261+
return setupDragHandlers(node, initialOpts, createClone, {
262+
handlerWidth: false
263+
});
264+
}
265+
244266
/////////////////////////////
245267
//// COMMIT DRAGGABLE V3 ////
246268
/////////////////////////////

apps/desktop/src/lib/dragging/draggables.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { key, type SelectionId } from '$lib/selection/key';
2+
import type { BranchDropData } from '$lib/branches/dropHandler';
23
import type { CodegenRuleDropData } from '$lib/codegen/dropzone';
34
import type { CommitDropData } from '$lib/commits/dropHandler';
45
import type { TreeChange } from '$lib/hunks/change';
@@ -65,4 +66,9 @@ export class ChangeDropData {
6566
}
6667
}
6768

68-
export type DropData = CommitDropData | ChangeDropData | HunkDropDataV3 | CodegenRuleDropData;
69+
export type DropData =
70+
| CommitDropData
71+
| ChangeDropData
72+
| HunkDropDataV3
73+
| CodegenRuleDropData
74+
| BranchDropData;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type Operation =
2525
| 'SquashCommit'
2626
| 'UpdateCommitMessage'
2727
| 'MoveCommit'
28+
| 'MoveBranch'
2829
| 'RestoreFromSnapshot'
2930
| 'ReorderCommit'
3031
| 'InsertBlankCommit'

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,7 @@ export type InteractiveIntegrationStep =
334334
message: string | null;
335335
};
336336
};
337+
338+
export type MoveBranchResult = {
339+
deletedStacks: string[];
340+
};

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import type {
4141
StackDetails,
4242
CreateRefRequest,
4343
InteractiveIntegrationStep,
44-
CreateBranchFromBranchOutcome
44+
CreateBranchFromBranchOutcome,
45+
MoveBranchResult
4546
} from '$lib/stacks/stack';
4647
import type { ReduxError } from '$lib/state/reduxError';
4748

@@ -748,6 +749,10 @@ export class StackService {
748749
return this.api.endpoints.moveCommit.mutate;
749750
}
750751

752+
get moveBranch() {
753+
return this.api.endpoints.moveBranch.mutate;
754+
}
755+
751756
get integrateUpstreamCommits() {
752757
return this.api.endpoints.integrateUpstreamCommits.useMutation();
753758
}
@@ -1462,6 +1467,40 @@ function injectEndpoints(api: ClientState['backendApi'], uiState: UiState) {
14621467
invalidatesItem(ReduxTag.StackDetails, args.targetStackId)
14631468
]
14641469
}),
1470+
moveBranch: build.mutation<
1471+
MoveBranchResult,
1472+
{
1473+
projectId: string;
1474+
sourceStackId: string;
1475+
subjectBranchName: string;
1476+
targetStackId: string;
1477+
targetBranchName: string;
1478+
}
1479+
>({
1480+
extraOptions: {
1481+
command: 'move_branch',
1482+
actionName: 'Move Branch'
1483+
},
1484+
query: (args) => args,
1485+
invalidatesTags: (result, _error, args) => {
1486+
if (result === undefined) return [];
1487+
1488+
if (result.deletedStacks.includes(args.sourceStackId)) {
1489+
// The source stack was deleted, so we need to invalidate the list of stacks.
1490+
return [
1491+
invalidatesList(ReduxTag.Stacks),
1492+
invalidatesList(ReduxTag.WorktreeChanges), // Moving commits can cause conflicts
1493+
invalidatesItem(ReduxTag.StackDetails, args.targetStackId)
1494+
];
1495+
}
1496+
1497+
return [
1498+
invalidatesList(ReduxTag.WorktreeChanges), // Moving commits can cause conflicts
1499+
invalidatesItem(ReduxTag.StackDetails, args.sourceStackId),
1500+
invalidatesItem(ReduxTag.StackDetails, args.targetStackId)
1501+
];
1502+
}
1503+
}),
14651504
integrateUpstreamCommits: build.mutation<
14661505
void,
14671506
{

0 commit comments

Comments
 (0)