From 4bf2c1132b02a7302f3ab379228020e06b0fe791 Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Tue, 31 Mar 2026 10:56:33 -0700 Subject: [PATCH] feat(code): unified PR creation workflow --- apps/code/src/main/services/git/service.ts | 4 +-- .../components/BranchSelector.tsx | 6 ++-- .../hooks/useGitInteraction.ts | 5 +-- .../git-interaction/hooks/useGitQueries.ts | 11 +++++++ .../utils/deriveBranchName.test.ts | 22 ++++++++----- .../git-interaction/utils/deriveBranchName.ts | 22 ++----------- .../utils/getSuggestedBranchName.ts | 33 +++++++++++++++++++ apps/code/src/shared/constants.ts | 14 -------- 8 files changed, 68 insertions(+), 49 deletions(-) create mode 100644 apps/code/src/renderer/features/git-interaction/utils/getSuggestedBranchName.ts diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 1d361373e..6cd915494 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -26,7 +26,6 @@ import { DiscardFileChangesSaga } from "@posthog/git/sagas/discard"; import { PullSaga } from "@posthog/git/sagas/pull"; import { PushSaga } from "@posthog/git/sagas/push"; import { parseGitHubUrl } from "@posthog/git/utils"; -import { isCodeBranch } from "@shared/constants"; import { inject, injectable } from "inversify"; import { MAIN_TOKENS } from "../../di/tokens"; import { logger } from "../../utils/logger"; @@ -229,8 +228,7 @@ export class GitService extends TypedEventEmitter { } public async getAllBranches(directoryPath: string): Promise { - const branches = await getAllBranches(directoryPath); - return branches.filter((branch) => !isCodeBranch(branch)); + return getAllBranches(directoryPath); } public async createBranch( diff --git a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx index 05d179859..e17840c8a 100644 --- a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx @@ -1,6 +1,6 @@ import { Combobox } from "@components/ui/combobox/Combobox"; import { useGitInteractionStore } from "@features/git-interaction/state/gitInteractionStore"; -import { getSuggestedBranchName } from "@features/git-interaction/utils/deriveBranchName"; +import { getSuggestedBranchName } from "@features/git-interaction/utils/getSuggestedBranchName"; import { invalidateGitBranchQueries } from "@features/git-interaction/utils/gitCacheKeys"; import { GitBranch, Plus } from "@phosphor-icons/react"; import { Flex, Spinner, Tooltip } from "@radix-ui/themes"; @@ -144,7 +144,9 @@ export function BranchSelector({ onClick={() => { setOpen(false); actions.openBranch( - taskId ? getSuggestedBranchName(taskId) : undefined, + taskId + ? getSuggestedBranchName(taskId, repoPath ?? undefined) + : undefined, ); }} > diff --git a/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts b/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts index 783ad0954..2d47d2fb6 100644 --- a/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts +++ b/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts @@ -14,7 +14,6 @@ import { createBranch, getBranchNameInputState, } from "@features/git-interaction/utils/branchCreation"; -import { getSuggestedBranchName } from "@features/git-interaction/utils/deriveBranchName"; import { updateGitCacheFromSnapshot } from "@features/git-interaction/utils/updateGitCache"; import { trpc, trpcClient } from "@renderer/trpc"; import { ANALYTICS_EVENTS } from "@shared/types/analytics"; @@ -22,6 +21,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { track } from "@utils/analytics"; import { logger } from "@utils/logger"; import { useMemo } from "react"; +import { getSuggestedBranchName } from "../utils/getSuggestedBranchName"; const log = logger.scope("git-interaction"); @@ -167,7 +167,8 @@ export function useGitInteraction( publish: () => modal.openPush("publish"), "view-pr": () => viewPr(), "create-pr": () => openCreatePr(), - "branch-here": () => modal.openBranch(getSuggestedBranchName(taskId)), + "branch-here": () => + modal.openBranch(getSuggestedBranchName(taskId, repoPath)), }; actionMap[id](); }; diff --git a/apps/code/src/renderer/features/git-interaction/hooks/useGitQueries.ts b/apps/code/src/renderer/features/git-interaction/hooks/useGitQueries.ts index f654129f9..e0ad14dea 100644 --- a/apps/code/src/renderer/features/git-interaction/hooks/useGitQueries.ts +++ b/apps/code/src/renderer/features/git-interaction/hooks/useGitQueries.ts @@ -106,6 +106,17 @@ export function useGitQueries(repoPath?: string) { ), ); + useQuery( + trpc.git.getAllBranches.queryOptions( + { directoryPath: repoPath as string }, + { + enabled: repoEnabled, + ...GIT_QUERY_DEFAULTS, + staleTime: 60_000, + }, + ), + ); + const hasChanges = changedFiles.length > 0; const aheadOfRemote = syncStatus?.aheadOfRemote ?? 0; const behind = syncStatus?.behind ?? 0; diff --git a/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts index 7a76b9a82..72e293f7b 100644 --- a/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts +++ b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.test.ts @@ -4,44 +4,48 @@ import { deriveBranchName } from "./deriveBranchName"; describe("deriveBranchName", () => { it("converts a simple title to a branch name", () => { expect(deriveBranchName("Fix authentication login bug", "abc123")).toBe( - "posthog/fix-authentication-login-bug", + "posthog-code/fix-authentication-login-bug", ); }); it("handles special characters", () => { expect(deriveBranchName("PostHog issue #1234", "abc123")).toBe( - "posthog/posthog-issue-1234", + "posthog-code/posthog-issue-1234", ); }); it("collapses consecutive dashes", () => { expect(deriveBranchName("Fix the bug", "abc123")).toBe( - "posthog/fix-the-bug", + "posthog-code/fix-the-bug", ); }); it("strips leading and trailing dashes", () => { - expect(deriveBranchName(" Fix bug ", "abc123")).toBe("posthog/fix-bug"); + expect(deriveBranchName(" Fix bug ", "abc123")).toBe( + "posthog-code/fix-bug", + ); }); it("truncates long titles", () => { const longTitle = "This is a very long task title that should be truncated to a reasonable length for git"; const result = deriveBranchName(longTitle, "abc123"); - expect(result.length).toBeLessThanOrEqual(68); // 60 slug + "posthog/" prefix - expect(result.startsWith("posthog/")).toBe(true); + expect(result.length).toBeLessThanOrEqual(73); // 60 slug + "posthog-code/" prefix + expect(result.startsWith("posthog-code/")).toBe(true); expect(result.endsWith("-")).toBe(false); }); it("falls back to task ID when title is empty", () => { - expect(deriveBranchName("", "abc123")).toBe("posthog/task-abc123"); + expect(deriveBranchName("", "abc123")).toBe("posthog-code/task-abc123"); }); it("falls back to task ID when title is only whitespace", () => { - expect(deriveBranchName(" ", "abc123")).toBe("posthog/task-abc123"); + expect(deriveBranchName(" ", "abc123")).toBe("posthog-code/task-abc123"); }); it("falls back to task ID when title is only special characters", () => { - expect(deriveBranchName("!@#$%", "abc123")).toBe("posthog/task-abc123"); + expect(deriveBranchName("!@#$%", "abc123")).toBe( + "posthog-code/task-abc123", + ); }); }); diff --git a/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts index 9014b4832..9177511c6 100644 --- a/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts +++ b/apps/code/src/renderer/features/git-interaction/utils/deriveBranchName.ts @@ -1,5 +1,4 @@ -import type { Task } from "@shared/types"; -import { queryClient } from "@utils/queryClient"; +import { BRANCH_PREFIX } from "@shared/constants"; export function deriveBranchName(title: string, fallbackId: string): string { const slug = title @@ -11,21 +10,6 @@ export function deriveBranchName(title: string, fallbackId: string): string { .slice(0, 60) .replace(/-$/, ""); - if (!slug) return `posthog/task-${fallbackId}`; - return `posthog/${slug}`; -} - -export function getSuggestedBranchName(taskId: string): string { - const queries = queryClient.getQueriesData({ - queryKey: ["tasks", "list"], - }); - let task: Task | undefined; - for (const [, tasks] of queries) { - task = tasks?.find((t) => t.id === taskId); - if (task) break; - } - const fallbackId = task?.task_number - ? String(task.task_number) - : (task?.slug ?? taskId); - return deriveBranchName(task?.title ?? "", fallbackId); + if (!slug) return `${BRANCH_PREFIX}task-${fallbackId}`; + return `${BRANCH_PREFIX}${slug}`; } diff --git a/apps/code/src/renderer/features/git-interaction/utils/getSuggestedBranchName.ts b/apps/code/src/renderer/features/git-interaction/utils/getSuggestedBranchName.ts new file mode 100644 index 000000000..05cccb539 --- /dev/null +++ b/apps/code/src/renderer/features/git-interaction/utils/getSuggestedBranchName.ts @@ -0,0 +1,33 @@ +import { deriveBranchName } from "@features/git-interaction/utils/deriveBranchName"; +import { trpc } from "@renderer/trpc"; +import type { Task } from "@shared/types"; +import { queryClient } from "@utils/queryClient"; + +export function getSuggestedBranchName( + taskId: string, + repoPath?: string, +): string { + const queries = queryClient.getQueriesData({ + queryKey: ["tasks", "list"], + }); + let task: Task | undefined; + for (const [, tasks] of queries) { + task = tasks?.find((t) => t.id === taskId); + if (task) break; + } + const fallbackId = task?.task_number + ? String(task.task_number) + : (task?.slug ?? taskId); + const base = deriveBranchName(task?.title ?? "", fallbackId); + + if (!repoPath) return base; + + const cached = queryClient.getQueryData( + trpc.git.getAllBranches.queryKey({ directoryPath: repoPath }), + ); + if (!cached?.includes(base)) return base; + + let n = 2; + while (cached.includes(`${base}-${n}`)) n++; + return `${base}-${n}`; +} diff --git a/apps/code/src/shared/constants.ts b/apps/code/src/shared/constants.ts index 7f505aea4..532677416 100644 --- a/apps/code/src/shared/constants.ts +++ b/apps/code/src/shared/constants.ts @@ -1,18 +1,4 @@ -/** - * Branch naming conventions. - * - Reading: Accept all prefixes for backwards compatibility - * - Writing: Always use BRANCH_PREFIX (posthog-code/) - */ export const BRANCH_PREFIX = "posthog-code/"; -export const LEGACY_BRANCH_PREFIXES = ["twig/", "array/", "posthog/"]; - -export function isCodeBranch(branchName: string): boolean { - return ( - branchName.startsWith(BRANCH_PREFIX) || - LEGACY_BRANCH_PREFIXES.some((p) => branchName.startsWith(p)) - ); -} - export const DATA_DIR = ".posthog-code"; export const WORKTREES_DIR = ".posthog-code/worktrees"; export const LEGACY_DATA_DIRS = [