From e8d3a0690d925a835c4af3ae712e630a1567e5b1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 22 Jul 2026 15:23:53 +0200 Subject: [PATCH 1/2] Unify temporary worktree branch naming - Normalize generated names to the canonical 8-hex format - Accept legacy UUID-shaped mobile branch names - Add regression coverage for both behaviors --- packages/shared/src/git.test.ts | 12 ++++++++++++ packages/shared/src/git.ts | 13 +++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/git.test.ts b/packages/shared/src/git.test.ts index 80578e262f9..a97a10fdce8 100644 --- a/packages/shared/src/git.test.ts +++ b/packages/shared/src/git.test.ts @@ -71,6 +71,18 @@ 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 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..ddd8f61d8d7 100644 --- a/packages/shared/src/git.ts +++ b/packages/shared/src/git.ts @@ -11,7 +11,11 @@ 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/`, +// so the matcher accepts both to keep those threads eligible for branch regeneration. +const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp( + `^${WORKTREE_BRANCH_PREFIX}\\/(?:[0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$`, +); /** * Sanitize an arbitrary string into a valid, lowercase git refName fragment. @@ -89,7 +93,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}`; } From 3a822dc054c1c9192b1a839cb15363008241b994 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 22 Jul 2026 16:13:11 +0200 Subject: [PATCH 2/2] Validate legacy mobile UUID branch refs - Restrict legacy UUID matching to RFC 4122 version 4 and valid variant bits - Add regression tests for invalid UUID-shaped branch refs --- packages/shared/src/git.test.ts | 11 +++++++++++ packages/shared/src/git.ts | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/git.test.ts b/packages/shared/src/git.test.ts index a97a10fdce8..96539f0aae2 100644 --- a/packages/shared/src/git.test.ts +++ b/packages/shared/src/git.test.ts @@ -83,6 +83,17 @@ describe("isTemporaryWorktreeBranch", () => { ).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 ddd8f61d8d7..71fe2e806cf 100644 --- a/packages/shared/src/git.ts +++ b/packages/shared/src/git.ts @@ -11,10 +11,12 @@ import * as Result from "effect/Result"; import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; export const WORKTREE_BRANCH_PREFIX = "t3code"; -// Canonical form is `t3code/<8 hex>`. Older mobile builds generated `t3code/`, -// so the matcher accepts both to keep those threads eligible for branch regeneration. +// 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}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$`, + `^${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})$`, ); /**