diff --git a/packages/shared/src/git.test.ts b/packages/shared/src/git.test.ts index 80578e262f9..96539f0aae2 100644 --- a/packages/shared/src/git.test.ts +++ b/packages/shared/src/git.test.ts @@ -71,6 +71,29 @@ describe("isTemporaryWorktreeBranch", () => { expect(isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/DEADBEEF`)).toBe(true); }); + it("normalizes a UUID-shaped random callback to the canonical 8-hex form", () => { + expect(buildTemporaryWorktreeBranchName(() => "f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12")).toBe( + `${WORKTREE_BRANCH_PREFIX}/f4ae4e0e`, + ); + }); + + it("matches legacy UUID-shaped temporary worktree refs from older mobile builds", () => { + expect( + isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-4d48-b4f2-9cf0aa54ab12`), + ).toBe(true); + }); + + it("rejects UUID-shaped refs that are not RFC 4122 v4", () => { + // version nibble is not 4 + expect( + isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-1d48-b4f2-9cf0aa54ab12`), + ).toBe(false); + // variant nibble is not [89ab] + expect( + isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/f4ae4e0e-f971-4d48-c4f2-9cf0aa54ab12`), + ).toBe(false); + }); + it("rejects non-temporary refName names", () => { expect(isTemporaryWorktreeBranch(`${WORKTREE_BRANCH_PREFIX}/feature/demo`)).toBe(false); expect(isTemporaryWorktreeBranch("main")).toBe(false); diff --git a/packages/shared/src/git.ts b/packages/shared/src/git.ts index ae50b148835..71fe2e806cf 100644 --- a/packages/shared/src/git.ts +++ b/packages/shared/src/git.ts @@ -11,7 +11,13 @@ import * as Result from "effect/Result"; import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; export const WORKTREE_BRANCH_PREFIX = "t3code"; -const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`); +// Canonical form is `t3code/<8 hex>`. Older mobile builds generated `t3code/` +// via Crypto.randomUUID() (always RFC 4122 v4), so the matcher also accepts exactly +// that shape — version nibble `4`, variant nibble `[89ab]` — to keep those threads +// eligible for branch regeneration without loosening beyond what was ever generated. +const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp( + `^${WORKTREE_BRANCH_PREFIX}\\/(?:[0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`, +); /** * Sanitize an arbitrary string into a valid, lowercase git refName fragment. @@ -89,7 +95,12 @@ export function deriveLocalBranchNameFromRemoteRef(branchName: string): string { export function buildTemporaryWorktreeBranchName( randomHex: (byteLength: number) => string, ): string { - const token = randomHex(4).toLowerCase(); + // Normalize to exactly 8 lowercase hex chars so a UUID-shaped callback + // still produces the canonical temporary branch form. + const token = randomHex(4) + .toLowerCase() + .replace(/[^0-9a-f]/g, "") + .slice(0, 8); return `${WORKTREE_BRANCH_PREFIX}/${token}`; }