Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion apps/desktop/src/components/commit/CommitListItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
disableCommitActions?: boolean;
active?: boolean;
hasConflicts?: boolean;
busy?: boolean;
disabled?: boolean;
editable?: boolean;
gerritReviewUrl?: string;
Expand Down Expand Up @@ -78,6 +79,7 @@
borderTop,
disabled,
hasConflicts,
busy,
active,
editable,
gerritReviewUrl,
Expand Down Expand Up @@ -214,7 +216,11 @@
{/if}
</div>

{#if !args.disableCommitActions}
{#if busy}
<div class="commit-busy-spinner">
<Icon name="spinner" size={14} />
</div>
{:else if !args.disableCommitActions}
{@render menu?.({ rightClickTrigger: container })}
{/if}
</div>
Expand Down Expand Up @@ -343,6 +349,12 @@
color: var(--fill-danger-bg);
}

.commit-busy-spinner {
display: flex;
align-items: center;
color: var(--text-2);
}

.commit-row__drag-handle {
display: flex;
position: absolute;
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/components/views/BranchCommitList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@
commitMessage={commit.message}
type={commit.state.type}
hasConflicts={commit.hasConflicts}
busy={controller.busyCommitId === commit.id}
diverged={commit.state.type === "LocalAndRemote" &&
commit.id !== commit.state.subject}
createdAt={commitCreatedAt(commit)}
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/components/views/StackView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
class="stack-view-wrapper"
role="presentation"
class:dimmed={controller.dimmed}
class:stack-busy={controller.stackBusy}
tabindex="-1"
data-id={controller.stackId}
data-testid={TestId.Stack}
Expand Down Expand Up @@ -280,7 +281,8 @@
margin-right: calc(var(--details-view-width) + 1.125rem);
}

.dimmed .stack-view {
.dimmed .stack-view,
.stack-busy .stack-view {
pointer-events: none;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/lib/bootstrap/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export function initDependencies(args: {
// ============================================================================

const imeHandler = new IMECompositionHandler();
const reorderDropzoneFactory = new ReorderDropzoneFactory(stackService);
const reorderDropzoneFactory = new ReorderDropzoneFactory(stackService, uiState);
const shortcutService = new ShortcutService(backend);
const dragStateService = new DragStateService();
const dropzoneRegistry = new DropzoneRegistry();
Expand Down
144 changes: 96 additions & 48 deletions apps/desktop/src/lib/dragging/dropHandlers/commitDropHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,27 @@ export class MoveCommitDzHandler implements DropzoneHandler {
}
}

ondrop(data: CommitDropData): void {
async ondrop(data: CommitDropData): Promise<void> {
// Clear the selection from the source lane if this commit was selected
const sourceSelection = untrack(() => this.uiState.lane(data.stackId).selection.current);
if (sourceSelection?.commitId === data.commit.id) {
this.uiState.lane(data.stackId).selection.set(undefined);
}

this.stackService
.moveCommit({
projectId: this.projectId,
targetStackId: this.stackId,
commitId: data.commit.id,
sourceStackId: data.stackId,
})
.then((response) => this.handleIllegalMoveResponse(response));
await withStackBusy(
this.uiState,
this.projectId,
{ commitId: data.commit.id, stackIds: [data.stackId, this.stackId] },
async () => {
const response = await this.stackService.moveCommit({
projectId: this.projectId,
targetStackId: this.stackId,
commitId: data.commit.id,
sourceStackId: data.stackId,
});
this.handleIllegalMoveResponse(response);
},
);
}
}

Expand Down Expand Up @@ -111,19 +117,29 @@ export class AmendCommitWithChangeDzHandler implements DropzoneHandler {
const sourceCommitId = data.selectionId.commitId;
const changes = changesToDiffSpec(await data.treeChanges());
if (sourceStackId && sourceCommitId) {
const { workspace } = await this.stackService.moveChangesBetweenCommits({
projectId: this.projectId,
destinationStackId: this.stackId,
destinationCommitId: this.commit.id,
sourceStackId,
sourceCommitId,
changes,
dryRun: false,
});

// Update the project state to point to the new commit if needed.
updateUiState(this.uiState, sourceStackId, sourceCommitId, workspace.replacedCommits);
updateUiState(this.uiState, this.stackId, this.commit.id, workspace.replacedCommits);
await withStackBusy(
this.uiState,
this.projectId,
{
commitId: sourceCommitId,
stackIds: [sourceStackId, this.stackId],
},
async () => {
const { workspace } = await this.stackService.moveChangesBetweenCommits({
projectId: this.projectId,
destinationStackId: this.stackId,
destinationCommitId: this.commit.id,
sourceStackId,
sourceCommitId,
changes,
dryRun: false,
});

// Update the project state to point to the new commit if needed.
updateUiState(this.uiState, sourceStackId, sourceCommitId, workspace.replacedCommits);
updateUiState(this.uiState, this.stackId, this.commit.id, workspace.replacedCommits);
},
);
} else {
throw new Error("Change drop data must specify the source stackId");
}
Expand Down Expand Up @@ -294,32 +310,42 @@ export class AmendCommitWithHunkDzHandler implements DropzoneHandler {
throw new Error("Can't receive a change without it's source or commit");
}

const { workspace } = await this.stackService.moveChangesBetweenCommits({
const sourceStackId = data.stackId;
const sourceCommitId = data.commitId;

await withStackBusy(
this.uiState,
projectId,
destinationStackId: stackId,
destinationCommitId: commit.id,
sourceStackId: data.stackId,
sourceCommitId: data.commitId,
changes: [
{
previousPathBytes,
pathBytes: data.change.pathBytes,
hunkHeaders: [
{ commitId: sourceCommitId, stackIds: [sourceStackId, stackId] },
async () => {
const { workspace } = await this.stackService.moveChangesBetweenCommits({
projectId,
destinationStackId: stackId,
destinationCommitId: commit.id,
sourceStackId,
sourceCommitId,
changes: [
{
oldStart: data.hunk.oldStart,
oldLines: data.hunk.oldLines,
newStart: data.hunk.newStart,
newLines: data.hunk.newLines,
previousPathBytes,
pathBytes: data.change.pathBytes,
hunkHeaders: [
{
oldStart: data.hunk.oldStart,
oldLines: data.hunk.oldLines,
newStart: data.hunk.newStart,
newLines: data.hunk.newLines,
},
],
},
],
},
],
dryRun: false,
});
dryRun: false,
});

// Update the project state to point to the new commit if needed.
updateUiState(this.uiState, data.stackId, data.commitId, workspace.replacedCommits);
updateUiState(this.uiState, stackId, commit.id, workspace.replacedCommits);
// Update the project state to point to the new commit if needed.
updateUiState(this.uiState, sourceStackId, sourceCommitId, workspace.replacedCommits);
updateUiState(this.uiState, stackId, commit.id, workspace.replacedCommits);
},
);

return;
}
Expand Down Expand Up @@ -368,6 +394,7 @@ export class AmendCommitWithHunkDzHandler implements DropzoneHandler {
* Handler that is able to squash two commits using `DzCommitData`.
*/
export class SquashCommitDzHandler implements DropzoneHandler {
private readonly uiState = inject(UI_STATE);
private readonly stackService = inject(STACK_SERVICE);

constructor(
Expand All @@ -392,16 +419,37 @@ export class SquashCommitDzHandler implements DropzoneHandler {
async ondrop(data: unknown) {
const { projectId, stackId, commit } = this.args;
if (data instanceof CommitDropData) {
await this.stackService.squashCommits({
await withStackBusy(
this.uiState,
projectId,
stackId,
sourceCommitIds: [data.commit.id],
targetCommitId: commit.id,
});
{ commitId: data.commit.id, stackIds: [stackId] },
async () => {
await this.stackService.squashCommits({
projectId,
stackId,
sourceCommitIds: [data.commit.id],
targetCommitId: commit.id,
});
},
);
}
}
}

export async function withStackBusy(
uiState: UiState,
projectId: string,
opts: { commitId?: string; stackIds?: string[] },
fn: () => Promise<void>,
) {
uiState.project(projectId).stackBusy.set({ commitId: opts.commitId, stackIds: opts.stackIds });
try {
await fn();
} finally {
uiState.project(projectId).stackBusy.set(undefined);
}
}

function updateUiState(
uiState: UiState,
stackId: string,
Expand Down
31 changes: 23 additions & 8 deletions apps/desktop/src/lib/dragging/stackingReorderDropzoneManager.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { CommitDropData } from "$lib/dragging/dropHandlers/commitDropHandler";
import { CommitDropData, withStackBusy } from "$lib/dragging/dropHandlers/commitDropHandler";
import { InjectionToken } from "@gitbutler/core/context";
import type { DropzoneHandler } from "$lib/dragging/handler";
import type { StackService } from "$lib/stacks/stackService.svelte";
import type { UiState } from "$lib/state/uiState.svelte";
import type { StackOrder } from "@gitbutler/but-sdk";

export class ReorderCommitDzHandler implements DropzoneHandler {
constructor(
private projectId: string,
private branchId: string,
private stackService: StackService,
private uiState: UiState,
private currentSeriesName: string,
private series: { name: string; commitIds: string[] }[],
public commitId: string,
Expand Down Expand Up @@ -38,11 +40,18 @@ export class ReorderCommitDzHandler implements DropzoneHandler {
);

if (stackOrder) {
await this.stackService.reorderStack({
projectId: this.projectId,
stackId: data.stackId,
stackOrder,
});
await withStackBusy(
this.uiState,
this.projectId,
{ commitId: data.commit.id, stackIds: [data.stackId] },
async () => {
await this.stackService.reorderStack({
projectId: this.projectId,
stackId: data.stackId,
stackOrder,
});
},
);
}
}
}
Expand All @@ -53,6 +62,7 @@ export class ReorderCommitDzFactory {
constructor(
private projectId: string,
private stackService: StackService,
private uiState: UiState,
private stack: { name: string; commitIds: string[] }[],
private laneId: string,
) {
Expand All @@ -73,6 +83,7 @@ export class ReorderCommitDzFactory {
this.projectId,
this.laneId,
this.stackService,
this.uiState,
currentSeries.name,
this.stack,
"top",
Expand All @@ -89,6 +100,7 @@ export class ReorderCommitDzFactory {
this.projectId,
this.laneId,
this.stackService,
this.uiState,
currentSeries.name,
this.stack,
commitId,
Expand All @@ -101,10 +113,13 @@ export const REORDER_DROPZONE_FACTORY = new InjectionToken<ReorderDropzoneFactor
);

export class ReorderDropzoneFactory {
constructor(private stackService: StackService) {}
constructor(
private stackService: StackService,
private uiState: UiState,
) {}

build(projectId: string, laneId: string, series: { name: string; commitIds: string[] }[]) {
return new ReorderCommitDzFactory(projectId, this.stackService, series, laneId);
return new ReorderCommitDzFactory(projectId, this.stackService, this.uiState, series, laneId);
}
}

Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/lib/stacks/stackController.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,20 @@ export class StackController {
);
}

private get stackBusyState() {
return this.projectState.stackBusy.current;
}

/** True when this stack is involved in a busy operation and should block interaction. */
get stackBusy(): boolean {
const stackIds = this.stackBusyState?.stackIds;
return !!stackIds && !!this.stackId && stackIds.includes(this.stackId);
}

get busyCommitId(): string | undefined {
return this.stackBusyState?.commitId;
}

get activeSelectionId(): SelectionId | undefined {
if (this.commitId) {
return createCommitSelection({ commitId: this.commitId, stackId: this.stackId });
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/lib/state/uiState.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,14 @@ export type ExclusiveAction =
branchName: string;
};

export type StackBusyState = {
commitId?: string;
stackIds?: string[];
};

export type ProjectUiState = {
exclusiveAction: ExclusiveAction | undefined;
stackBusy: StackBusyState | undefined;
branchesToPoll: string[];
selectedClaudeSession: { stackId: string; head: string } | undefined;
thinkingLevel: ThinkingLevel;
Expand Down Expand Up @@ -199,6 +205,7 @@ export class UiState {
/** Properties scoped to a specific project. */
readonly project = this.buildScopedProps<ProjectUiState>(this.scopesCache.projects, {
exclusiveAction: undefined,
stackBusy: undefined,
branchesToPoll: [],
selectedClaudeSession: undefined,
thinkingLevel: "normal",
Expand Down
Loading