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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,6 @@ superset-dev-data/
!.codex/config.toml
!.codex/commands
!.codex/prompts
.serena/
test-conflict-repo/
.amp/*
103 changes: 101 additions & 2 deletions apps/desktop/src/lib/trpc/routers/changes/branches.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { access } from "node:fs/promises";
import { join, resolve } from "node:path";
import { worktrees } from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import type { SimpleGit } from "simple-git";
Expand All @@ -17,6 +20,19 @@ import { clearStatusCacheForWorktree } from "./utils/status-cache";

const DEFAULT_REF_SEARCH_LIMIT = 50;
const MAX_REF_SEARCH_LIMIT = 200;
const GIT_PROGRESS_OPERATIONS = [
{ kind: "merge", path: "MERGE_HEAD" },
{ kind: "cherry-pick", path: "CHERRY_PICK_HEAD" },
{ kind: "revert", path: "REVERT_HEAD" },
{ kind: "bisect", path: "BISECT_LOG" },
] as const;

type BranchProgressOperation =
| "merge"
| "rebase"
| "cherry-pick"
| "revert"
| "bisect";

type SearchableRef = {
name: string;
Expand All @@ -42,6 +58,27 @@ type ParsedRefEntry = {
const REF_FIELD_SEPARATOR = "\u001f";
const REF_RECORD_SEPARATOR = "\u001e";

function normalizeBranchRef(branch: string): string {
if (branch.startsWith("refs/heads/")) {
return branch.slice("refs/heads/".length);
}
if (branch.startsWith("refs/remotes/origin/")) {
return branch.slice("refs/remotes/origin/".length);
}
if (branch.startsWith("remotes/origin/")) {
return branch.slice("remotes/origin/".length);
}
return branch;
}

async function assertWorktreePathExists(worktreePath: string): Promise<void> {
if (await pathExists(worktreePath)) return;
throw new TRPCError({
code: "NOT_FOUND",
message: `Worktree path does not exist: ${worktreePath}`,
});
}

export const createBranchesRouter = () => {
return router({
getBranches: publicProcedure
Expand All @@ -58,6 +95,7 @@ export const createBranchesRouter = () => {
currentBranch: string | null;
}> => {
assertRegisteredWorktree(input.worktreePath);
await assertWorktreePathExists(input.worktreePath);

const git = await getSimpleGitWithShellPath(input.worktreePath);

Expand Down Expand Up @@ -189,15 +227,38 @@ export const createBranchesRouter = () => {
}),
)
.mutation(async ({ input }): Promise<{ success: boolean }> => {
await gitSwitchBranch(input.worktreePath, input.branch);
await assertWorktreePathExists(input.worktreePath);
const branch = normalizeBranchRef(input.branch);
await gitSwitchBranch(input.worktreePath, branch);
const currentBranch =
(await getCurrentBranch(input.worktreePath)) ?? input.branch;
(await getCurrentBranch(input.worktreePath)) ?? branch;
persistWorktreeBranch(input.worktreePath, currentBranch);

clearStatusCacheForWorktree(input.worktreePath);
return { success: true };
}),

getBranchGuardState: publicProcedure
.input(z.object({ worktreePath: z.string() }))
.query(
async ({
input,
}): Promise<{
operationInProgress: BranchProgressOperation | null;
}> => {
assertRegisteredWorktree(input.worktreePath);

const git = await getSimpleGitWithShellPath(input.worktreePath);

return {
operationInProgress: await detectGitProgressOperation(
git,
input.worktreePath,
),
};
},
),

createBranch: publicProcedure
.input(
z.object({
Expand Down Expand Up @@ -357,6 +418,44 @@ function getPersistedWorktree(worktreePath: string) {
.get();
}

async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}

async function detectGitProgressOperation(
git: SimpleGit,
worktreePath: string,
): Promise<BranchProgressOperation | null> {
let gitDirPath: string;

try {
const gitDir = (await git.revparse(["--git-dir"])).trim();
gitDirPath = resolve(worktreePath, gitDir);
} catch {
return null;
}

if (
(await pathExists(join(gitDirPath, "rebase-merge"))) ||
(await pathExists(join(gitDirPath, "rebase-apply")))
) {
return "rebase";
}

for (const candidate of GIT_PROGRESS_OPERATIONS) {
if (await pathExists(join(gitDirPath, candidate.path))) {
return candidate.kind;
}
}

return null;
}

function persistWorktreeBranch(worktreePath: string, branch: string): void {
const persistedWorktree = getPersistedWorktree(worktreePath);
if (!persistedWorktree) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { SimpleGit } from "simple-git";
import { z } from "zod";
import { execGitWithShellPath } from "../../workspaces/utils/git-client";
import { getRepoContext } from "../../workspaces/utils/github";
import { getPullRequestRepoArgs } from "../../workspaces/utils/github/repo-context";
import { getPullRequestRepoNames } from "../../workspaces/utils/github/repo-context";
import { execWithShellEnv } from "../../workspaces/utils/shell-env";
import {
buildPullRequestCompareUrl,
Expand All @@ -24,32 +24,57 @@ async function findOpenPRByHeadCommit(
return null;
}

const repoArgs = getPullRequestRepoArgs(await getRepoContext(worktreePath));

const { stdout } = await execWithShellEnv(
"gh",
[
"pr",
"list",
...repoArgs,
"--state",
"open",
"--search",
`${headSha} is:pr`,
"--limit",
"20",
"--json",
"url,headRefOid",
],
{ cwd: worktreePath },
const repoNames = getPullRequestRepoNames(
await getRepoContext(worktreePath),
);
const repoArgSets =
repoNames.length > 0
? repoNames.map((repoName) => ["--repo", repoName])
: [[]];

for (const repoArgs of repoArgSets) {
try {
const { stdout } = await execWithShellEnv(
"gh",
[
"pr",
"list",
...repoArgs,
"--state",
"open",
"--search",
`${headSha} is:pr`,
"--limit",
"20",
"--json",
"url,headRefOid",
],
{ cwd: worktreePath },
);

const parsed = JSON.parse(stdout) as Array<{
url?: string;
headRefOid?: string;
}>;
const match = parsed.find((candidate) => candidate.headRefOid === headSha);
return match?.url?.trim() || null;
const parsed = JSON.parse(stdout) as Array<{
url?: string;
headRefOid?: string;
}>;
const match = parsed.find(
(candidate) => candidate.headRefOid === headSha,
);
if (match?.url?.trim()) {
return match.url.trim();
}
} catch (error) {
console.warn(
"[git/findExistingOpenPRUrl] Failed repo-scoped commit-based PR lookup:",
{
worktreePath,
repoArgs,
message: error instanceof Error ? error.message : String(error),
},
);
}
}
Comment thread
MocA-Love marked this conversation as resolved.

return null;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface TrackingStatus {
}

const MAX_LINE_COUNT_SIZE = 1 * 1024 * 1024;
const MAX_UNTRACKED_LINE_COUNT_FILES = 200;
const WORKER_DEBUG = process.env.SUPERSET_WORKER_DEBUG === "1";

function logWorkerWarning(message: string, error: unknown): void {
Expand Down Expand Up @@ -74,6 +75,8 @@ async function applyUntrackedLineCount(
worktreePath: string,
untracked: ChangedFile[],
): Promise<void> {
if (untracked.length > MAX_UNTRACKED_LINE_COUNT_FILES) return;

let worktreeReal: string;
try {
worktreeReal = await realpath(worktreePath);
Expand Down
46 changes: 42 additions & 4 deletions apps/desktop/src/lib/trpc/routers/external/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ const ExternalAppSchema = z.enum(EXTERNAL_APPS);

const nonEditorSet = new Set<ExternalApp>(NON_EDITOR_APPS);

function isMissingExternalAppError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
return (
error.message.includes("Unable to find application named") ||
error.message.includes("Ensure the application is installed.")
);
}

function normalizeOpenInAppError(error: unknown): never {
if (isMissingExternalAppError(error)) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Requested application is not available",
});
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: error instanceof Error ? error.message : "Unknown error",
});
}

/** Sets the global default editor if one hasn't been set yet. Skips non-editor apps. */
function ensureGlobalDefaultEditor(app: ExternalApp) {
if (nonEditorSet.has(app)) return;
Expand Down Expand Up @@ -80,7 +104,10 @@ async function openPathInApp(
throw lastError;
}

await shell.openPath(filePath);
const openError = await shell.openPath(filePath);
if (openError) {
throw new Error(openError);
}
}

/**
Expand Down Expand Up @@ -118,7 +145,11 @@ export const createExternalRouter = () => {
}),
)
.mutation(async ({ input }) => {
await openPathInApp(input.path, input.app);
try {
await openPathInApp(input.path, input.app);
} catch (error) {
normalizeOpenInAppError(error);
}
Comment thread
MocA-Love marked this conversation as resolved.

// Persist defaults only after successful launch
if (input.projectId) {
Expand Down Expand Up @@ -175,11 +206,18 @@ export const createExternalRouter = () => {
// No preferred editor configured yet.
// Fall back to OS default file handler so Cmd/Ctrl+click still works
// even when Cursor (or any specific editor) isn't installed.
await shell.openPath(filePath);
const openError = await shell.openPath(filePath);
if (openError) {
throw new Error(openError);
}
return;
}

await openPathInApp(filePath, app);
try {
await openPathInApp(filePath, app);
} catch (error) {
normalizeOpenInAppError(error);
}
}),
});
};
Expand Down
Loading
Loading