Skip to content

Commit da9ebcc

Browse files
authored
Merge pull request #13147 from gitbutlerapp/push-suzlqpwnuyll
Lite: add ability to drag and drop changes before/after commits to create new commits
2 parents 3d40291 + 9d60cd2 commit da9ebcc

4 files changed

Lines changed: 182 additions & 78 deletions

File tree

apps/lite/ui/src/Operation.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { useMutation } from "@tanstack/react-query";
33
import { Match } from "effect";
44
import {
55
CommitCreateParams,
6+
CommitInsertBlankParams,
67
CommitMoveParams,
8+
CommitMoveChangesBetweenParams,
79
MoveBranchParams,
810
TearOffBranchParams,
911
} from "#electron/ipc.ts";
1012
import { rejectedChangesToastOptions } from "#ui/components/RejectedChanges.tsx";
1113
import {
1214
commitCreateMutationOptions,
15+
commitInsertBlankMutationOptions,
1316
commitMoveMutationOptions,
17+
commitMoveChangesBetweenMutationOptions,
1418
moveBranchMutationOptions,
1519
rubMutationOptions,
1620
tearOffBranchMutationOptions,
@@ -22,6 +26,10 @@ export type RubOperation = Omit<RubParams, "projectId">;
2226
export type Operation =
2327
| ({ _tag: "Rub" } & RubOperation)
2428
| ({ _tag: "CommitCreate" } & Omit<CommitCreateParams, "projectId">)
29+
| ({
30+
_tag: "CommitCreateFromCommittedChanges";
31+
} & Omit<CommitInsertBlankParams, "projectId"> &
32+
Pick<CommitMoveChangesBetweenParams, "changes" | "sourceCommitId">)
2533
| ({ _tag: "CommitMove" } & Omit<CommitMoveParams, "projectId">)
2634
| ({ _tag: "MoveBranch" } & Omit<MoveBranchParams, "projectId">)
2735
| ({ _tag: "TearOffBranch" } & Omit<TearOffBranchParams, "projectId">);
@@ -30,7 +38,9 @@ export const useRunOperation = (projectId: string) => {
3038
const toastManager = Toast.useToastManager();
3139
const rubMutation = useMutation(rubMutationOptions);
3240
const commitCreate = useMutation(commitCreateMutationOptions);
41+
const commitInsertBlank = useMutation(commitInsertBlankMutationOptions);
3342
const commitMove = useMutation(commitMoveMutationOptions);
43+
const commitMoveChangesBetween = useMutation(commitMoveChangesBetweenMutationOptions);
3444
const moveBranch = useMutation(moveBranchMutationOptions);
3545
const tearOffBranch = useMutation(tearOffBranchMutationOptions);
3646

@@ -80,6 +90,24 @@ export const useRunOperation = (projectId: string) => {
8090
},
8191
);
8292
}),
93+
Match.tag("CommitCreateFromCommittedChanges", (operation) => {
94+
// Ideally this would be an atomic backend operation.
95+
void (async () => {
96+
const insertedCommit = await commitInsertBlank.mutateAsync({
97+
projectId,
98+
relativeTo: operation.relativeTo,
99+
side: operation.side,
100+
});
101+
102+
await commitMoveChangesBetween.mutateAsync({
103+
projectId,
104+
sourceCommitId:
105+
insertedCommit.replacedCommits[operation.sourceCommitId] ?? operation.sourceCommitId,
106+
destinationCommitId: insertedCommit.newCommit,
107+
changes: operation.changes,
108+
});
109+
})();
110+
}),
83111
Match.tag("CommitMove", (operation) => {
84112
commitMove.mutate({
85113
projectId,

apps/lite/ui/src/api/mutations.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export const commitMoveMutationOptions = mutationOptions({
3333
},
3434
});
3535

36+
export const commitMoveChangesBetweenMutationOptions = mutationOptions({
37+
mutationFn: window.lite.commitMoveChangesBetween,
38+
onSuccess: async (_data, _input, _ctx, { client }) => {
39+
await client.invalidateQueries();
40+
},
41+
});
42+
3643
export const commitCreateMutationOptions = mutationOptions({
3744
mutationFn: window.lite.commitCreate,
3845
onSuccess: async (_data, _input, _ctx, { client }) => {

apps/lite/ui/src/routes/project/$id/workspace/route.module.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
flex-direction: column;
7777
}
7878

79-
.commitMoveTarget {
79+
.commitInsertionTarget {
8080
--indicator-height: 3px;
8181

8282
position: absolute;
@@ -86,12 +86,12 @@
8686
pointer-events: none;
8787
}
8888

89-
.commitMoveTargetAbove {
89+
.commitInsertionTargetAbove {
9090
top: 0;
9191
transform: translateY(calc(var(--indicator-height) / -2));
9292
}
9393

94-
.commitMoveTargetBelow {
94+
.commitInsertionTargetBelow {
9595
bottom: 0;
9696
transform: translateY(calc(var(--indicator-height) / 2));
9797
}

apps/lite/ui/src/routes/project/$id/workspace/route.tsx

Lines changed: 144 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -603,76 +603,122 @@ const StackMenuPopup: FC<{
603603
);
604604
};
605605

606+
const getCommitTargetOperation = ({
607+
sourceItem,
608+
commitId,
609+
previousCommitId,
610+
nextCommitId,
611+
input,
612+
element,
613+
}: {
614+
sourceItem: SourceItem;
615+
commitId: string;
616+
previousCommitId: string | undefined;
617+
nextCommitId: string | undefined;
618+
input: Parameters<typeof attachInstruction>[1]["input"];
619+
element: Element;
620+
}): Operation | null => {
621+
const isNoOpCommitMove = (sourceCommitId: string, side: InsertSide): boolean =>
622+
sourceCommitId === commitId ||
623+
(side === "above" && previousCommitId === sourceCommitId) ||
624+
(side === "below" && nextCommitId === sourceCommitId);
625+
626+
const rubOperation = getRubOperation({
627+
sourceItem,
628+
target: { _tag: "Commit", commitId },
629+
});
630+
631+
const instruction = extractInstruction(
632+
attachInstruction(
633+
{ sourceItem },
634+
{
635+
input,
636+
element,
637+
operations: {
638+
"reorder-before":
639+
(sourceItem._tag === "Commit" && !isNoOpCommitMove(sourceItem.commitId, "above")) ||
640+
(sourceItem._tag === "TreeChanges" && sourceItem.source.parent._tag === "Changes") ||
641+
(sourceItem._tag === "TreeChanges" && sourceItem.source.parent._tag === "Commit")
642+
? "available"
643+
: "not-available",
644+
"reorder-after":
645+
(sourceItem._tag === "Commit" && !isNoOpCommitMove(sourceItem.commitId, "below")) ||
646+
(sourceItem._tag === "TreeChanges" && sourceItem.source.parent._tag === "Changes") ||
647+
(sourceItem._tag === "TreeChanges" && sourceItem.source.parent._tag === "Commit")
648+
? "available"
649+
: "not-available",
650+
combine: rubOperation ? "available" : "not-available",
651+
},
652+
},
653+
),
654+
);
655+
656+
if (!instruction) return null;
657+
658+
return Match.value(instruction.operation).pipe(
659+
Match.when("combine", (): Operation | null =>
660+
rubOperation ? { _tag: "Rub", ...rubOperation } : null,
661+
),
662+
Match.orElse((side): Operation | null => {
663+
const insertSide = Match.value(side).pipe(
664+
Match.when("reorder-before", (): InsertSide => "above"),
665+
Match.when("reorder-after", (): InsertSide => "below"),
666+
Match.exhaustive,
667+
);
668+
669+
if (sourceItem._tag === "Commit")
670+
return {
671+
_tag: "CommitMove",
672+
subjectCommitId: sourceItem.commitId,
673+
relativeTo: { type: "commit", subject: commitId },
674+
side: insertSide,
675+
};
676+
677+
if (sourceItem._tag === "TreeChanges" && sourceItem.source.parent._tag === "Changes")
678+
return {
679+
_tag: "CommitCreate",
680+
relativeTo: { type: "commit", subject: commitId },
681+
side: insertSide,
682+
changes: sourceItem.source.changes.map(({ change, hunkHeaders }) =>
683+
createDiffSpec(change, hunkHeaders),
684+
),
685+
message: "",
686+
};
687+
688+
if (sourceItem._tag === "TreeChanges" && sourceItem.source.parent._tag === "Commit")
689+
return {
690+
_tag: "CommitCreateFromCommittedChanges",
691+
sourceCommitId: sourceItem.source.parent.commitId,
692+
relativeTo: { type: "commit", subject: commitId },
693+
side: insertSide,
694+
changes: sourceItem.source.changes.map(({ change, hunkHeaders }) =>
695+
createDiffSpec(change, hunkHeaders),
696+
),
697+
};
698+
699+
return null;
700+
}),
701+
);
702+
};
703+
606704
const CommitTarget: FC<
607705
{
608706
commitId: string;
609707
previousCommitId: string | undefined;
610708
nextCommitId: string | undefined;
611709
} & useRender.ComponentProps<"div">
612710
> = ({ commitId, previousCommitId, nextCommitId, render, ...props }) => {
613-
const isNoOpCommitMove = (sourceCommitId: string, side: InsertSide): boolean =>
614-
sourceCommitId === commitId ||
615-
(side === "above" && previousCommitId === sourceCommitId) ||
616-
(side === "below" && nextCommitId === sourceCommitId);
617-
618711
const [operation, dropRef] = useDroppable(({ source, input, element }) => {
619712
const sourceItem = parseDragData(source.data);
620713
if (!sourceItem) return null;
621-
622-
const rubOperation = getRubOperation({
714+
return getCommitTargetOperation({
623715
sourceItem,
624-
target: { _tag: "Commit", commitId },
716+
commitId,
717+
previousCommitId,
718+
nextCommitId,
719+
input,
720+
element,
625721
});
626-
627-
const instruction = extractInstruction(
628-
attachInstruction(
629-
{ sourceItem },
630-
{
631-
input,
632-
element,
633-
operations: {
634-
"reorder-before":
635-
sourceItem._tag === "Commit" && !isNoOpCommitMove(sourceItem.commitId, "above")
636-
? "available"
637-
: "not-available",
638-
"reorder-after":
639-
sourceItem._tag === "Commit" && !isNoOpCommitMove(sourceItem.commitId, "below")
640-
? "available"
641-
: "not-available",
642-
combine: rubOperation ? "available" : "not-available",
643-
},
644-
},
645-
),
646-
);
647-
648-
if (!instruction) return null;
649-
650-
return Match.value(instruction.operation).pipe(
651-
Match.when("combine", (): Operation | null =>
652-
rubOperation ? { _tag: "Rub", ...rubOperation } : null,
653-
),
654-
Match.when("reorder-before", (): Operation | null =>
655-
sourceItem._tag === "Commit"
656-
? {
657-
_tag: "CommitMove",
658-
subjectCommitId: sourceItem.commitId,
659-
relativeTo: { type: "commit", subject: commitId },
660-
side: "above",
661-
}
662-
: null,
663-
),
664-
Match.when("reorder-after", (): Operation | null =>
665-
sourceItem._tag === "Commit"
666-
? {
667-
_tag: "CommitMove",
668-
subjectCommitId: sourceItem.commitId,
669-
relativeTo: { type: "commit", subject: commitId },
670-
side: "below",
671-
}
672-
: null,
673-
),
674-
Match.exhaustive,
675-
);
676722
});
677723

678724
const droppable = useRender({
@@ -683,39 +729,62 @@ const CommitTarget: FC<
683729
}),
684730
});
685731

686-
const tooltip = operation
732+
const rubTooltip = operation
687733
? Match.value(operation).pipe(
688734
Match.tag("Rub", (operation) => rubOperationLabel(operation)),
689-
Match.tag("CommitMove", () => null),
690735
Match.orElse(() => null),
691736
)
692737
: null;
693738

694739
return (
695740
<div className={styles.commit}>
696-
<Tooltip.Root open={tooltip !== null}>
741+
<Tooltip.Root open={rubTooltip !== null}>
697742
<Tooltip.Trigger render={droppable} />
698743
<Tooltip.Portal>
699744
<Tooltip.Positioner sideOffset={8}>
700745
<Tooltip.Popup className={classes(uiStyles.popup, uiStyles.tooltip)}>
701-
{tooltip}
746+
{rubTooltip}
702747
</Tooltip.Popup>
703748
</Tooltip.Positioner>
704749
</Tooltip.Portal>
705750
</Tooltip.Root>
706-
{operation?._tag === "CommitMove" && (
707-
<div
708-
className={classes(
709-
styles.commitMoveTarget,
710-
pipe(
711-
operation.side,
712-
Match.value,
713-
Match.when("above", () => styles.commitMoveTargetAbove),
714-
Match.when("below", () => styles.commitMoveTargetBelow),
715-
Match.exhaustive,
716-
),
717-
)}
718-
/>
751+
752+
{(operation?._tag === "CommitMove" ||
753+
operation?._tag === "CommitCreate" ||
754+
operation?._tag === "CommitCreateFromCommittedChanges") && (
755+
<Tooltip.Root open>
756+
<Tooltip.Trigger
757+
render={
758+
<div
759+
className={classes(
760+
styles.commitInsertionTarget,
761+
pipe(
762+
operation.side,
763+
Match.value,
764+
Match.when("above", () => styles.commitInsertionTargetAbove),
765+
Match.when("below", () => styles.commitInsertionTargetBelow),
766+
Match.exhaustive,
767+
),
768+
)}
769+
/>
770+
}
771+
/>
772+
<Tooltip.Portal>
773+
<Tooltip.Positioner sideOffset={8}>
774+
<Tooltip.Popup className={classes(uiStyles.popup, uiStyles.tooltip)}>
775+
{Match.value(operation).pipe(
776+
Match.tag("CommitMove", () => "Move commit here"),
777+
Match.tag(
778+
"CommitCreateFromCommittedChanges",
779+
"CommitCreate",
780+
() => "Commit changes here",
781+
),
782+
Match.exhaustive,
783+
)}
784+
</Tooltip.Popup>
785+
</Tooltip.Positioner>
786+
</Tooltip.Portal>
787+
</Tooltip.Root>
719788
)}
720789
</div>
721790
);

0 commit comments

Comments
 (0)