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
50 changes: 50 additions & 0 deletions apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { shell } from "electron";
import simpleGit from "simple-git";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import { execWithShellEnv } from "../workspaces/utils/shell-env";
import { isUpstreamMissingError } from "./git-utils";
import { assertRegisteredWorktree } from "./security";

Expand Down Expand Up @@ -256,5 +257,54 @@ export const createGitOperationsRouter = () => {
return { success: true, url };
},
),

mergePR: publicProcedure
.input(
z.object({
worktreePath: z.string(),
strategy: z.enum(["merge", "squash", "rebase"]).default("squash"),
deleteBranch: z.boolean().default(true),
}),
)
.mutation(
async ({ input }): Promise<{ success: boolean; mergedAt?: string }> => {
assertRegisteredWorktree(input.worktreePath);

const args = ["pr", "merge", `--${input.strategy}`];
if (input.deleteBranch) {
args.push("--delete-branch");
}

try {
await execWithShellEnv("gh", args, { cwd: input.worktreePath });
return { success: true, mergedAt: new Date().toISOString() };
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
console.error("[git/mergePR] Failed to merge PR:", message);

if (message.includes("no pull requests found")) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No pull request found for this branch",
});
}
if (
message.includes("not mergeable") ||
message.includes("blocked")
) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"PR cannot be merged. Check for merge conflicts or required status checks.",
});
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to merge PR: ${message}`,
});
}
},
),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { useEffect, useRef, useState } from "react";
import { HiArrowPath, HiCheck } from "react-icons/hi2";
import { LuGitBranch, LuLoaderCircle } from "react-icons/lu";
import { LuGitBranch } from "react-icons/lu";
import { VscGitStash, VscGitStashApply } from "react-icons/vsc";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { PRIcon } from "renderer/screens/main/components/PRIcon";
import { usePRStatus } from "renderer/screens/main/hooks";
import { useChangesStore } from "renderer/stores/changes";
import type { ChangesViewMode } from "../../types";
import { ViewModeToggle } from "../ViewModeToggle";
import { PRButton } from "./components/PRButton";

interface ChangesHeaderProps {
onRefresh: () => void;
Expand Down Expand Up @@ -187,35 +186,6 @@ function RefreshButton({ onRefresh }: { onRefresh: () => void }) {
);
}

function PRStatusLink({ workspaceId }: { workspaceId?: string }) {
const { pr, isLoading } = usePRStatus({
workspaceId,
refetchInterval: 10000,
});

if (isLoading) {
return (
<LuLoaderCircle className="w-4 h-4 animate-spin text-muted-foreground" />
);
}

if (!pr) return null;

return (
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:opacity-80 transition-opacity"
>
<PRIcon state={pr.state} className="w-4 h-4" />
<span className="text-xs text-muted-foreground font-mono">
#{pr.number}
</span>
</a>
);
}

export function ChangesHeader({
onRefresh,
viewMode,
Expand All @@ -238,7 +208,11 @@ export function ChangesHeader({
/>
<ViewModeToggle viewMode={viewMode} onViewModeChange={onViewModeChange} />
<RefreshButton onRefresh={onRefresh} />
<PRStatusLink workspaceId={workspaceId} />
<PRButton
workspaceId={workspaceId}
worktreePath={worktreePath}
onRefresh={onRefresh}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@superset/ui/dropdown-menu";
import { toast } from "@superset/ui/sonner";
import { HiChevronDown } from "react-icons/hi2";
import { LuLoaderCircle } from "react-icons/lu";
import { VscGitMerge } from "react-icons/vsc";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { PRIcon } from "renderer/screens/main/components/PRIcon";
import { usePRStatus } from "renderer/screens/main/hooks";

interface PRButtonProps {
workspaceId?: string;
worktreePath: string;
onRefresh: () => void;
}

export function PRButton({
workspaceId,
worktreePath,
onRefresh,
}: PRButtonProps) {
const { pr, isLoading } = usePRStatus({
workspaceId,
refetchInterval: 10000,
});

const mergePRMutation = electronTrpc.changes.mergePR.useMutation({
onSuccess: () => {
toast.success("PR merged successfully");
onRefresh();
},
onError: (error) => toast.error(`Merge failed: ${error.message}`),
});

const handleMergePR = (strategy: "merge" | "squash" | "rebase") =>
mergePRMutation.mutate({ worktreePath, strategy, deleteBranch: true });

if (isLoading) {
return (
<LuLoaderCircle className="w-4 h-4 animate-spin text-muted-foreground" />
);
}

if (!pr) return null;

const canMerge = pr.state === "open";

if (!canMerge) {
return (
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 ml-auto hover:opacity-80 transition-opacity"
>
<PRIcon state={pr.state} className="w-4 h-4" />
<span className="text-xs text-muted-foreground font-mono">
#{pr.number}
</span>
</a>
);
}

return (
<div className="flex items-center ml-auto rounded border border-border overflow-hidden">
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 px-1.5 py-0.5 hover:bg-accent transition-colors"
>
<PRIcon state={pr.state} className="w-4 h-4" />
<span className="text-xs text-muted-foreground font-mono">
#{pr.number}
</span>
</a>
<div className="w-px h-full bg-border" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex items-center px-1 py-0.5 hover:bg-accent transition-colors"
disabled={mergePRMutation.isPending}
>
<HiChevronDown className="size-3 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
Merge
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => handleMergePR("squash")}
className="text-xs"
disabled={mergePRMutation.isPending}
>
<VscGitMerge className="size-3.5" />
Squash and merge
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleMergePR("merge")}
className="text-xs"
disabled={mergePRMutation.isPending}
>
<VscGitMerge className="size-3.5" />
Create merge commit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleMergePR("rebase")}
className="text-xs"
disabled={mergePRMutation.isPending}
>
<VscGitMerge className="size-3.5" />
Rebase and merge
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PRButton } from "./PRButton";
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ export function CommitInput({
);
};

// Determine primary action based on state
const getPrimaryAction = () => {
if (canCommit) {
return {
Expand Down
Loading