From a7285d8a3ff04b432124a8b849f6e7edf88f0b02 Mon Sep 17 00:00:00 2001 From: Mukunda Katta Date: Sun, 24 May 2026 15:44:11 -0700 Subject: [PATCH] feat(worktree): use human-readable names for worktree branches Worktrees were named with random 4-digit numbers that read like SHA snippets, making it momentarily unclear which worktree you were on. Generate adjective-noun-NN names instead (e.g. "swift-otter-42") from a small curated word list. Names stay filesystem-safe and short. Fixes #1844 --- packages/git/src/worktree-name.test.ts | 32 ++++++++++++++++++++++++ packages/git/src/worktree-name.ts | 34 ++++++++++++++++++++++++++ packages/git/src/worktree.ts | 4 +-- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 packages/git/src/worktree-name.test.ts create mode 100644 packages/git/src/worktree-name.ts diff --git a/packages/git/src/worktree-name.test.ts b/packages/git/src/worktree-name.test.ts new file mode 100644 index 000000000..ab01d1916 --- /dev/null +++ b/packages/git/src/worktree-name.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { generateHumanReadableName } from "./worktree-name"; + +describe("generateHumanReadableName", () => { + it("returns a string matching adjective-noun-NN", () => { + const name = generateHumanReadableName(); + expect(name).toMatch(/^[a-z]+-[a-z]+-\d{2}$/); + }); + + it("produces varied names over multiple calls", () => { + const names = new Set(); + for (let i = 0; i < 50; i++) { + names.add(generateHumanReadableName()); + } + // With 36 * 36 * 90 = ~116k combinations, 50 draws should yield + // many unique values. Allow generous slack for randomness. + expect(names.size).toBeGreaterThan(20); + }); + + it("uses only filesystem-safe characters", () => { + for (let i = 0; i < 25; i++) { + const name = generateHumanReadableName(); + expect(name).toMatch(/^[a-z0-9-]+$/); + } + }); + + it("stays compact (under 32 chars)", () => { + for (let i = 0; i < 25; i++) { + expect(generateHumanReadableName().length).toBeLessThanOrEqual(32); + } + }); +}); diff --git a/packages/git/src/worktree-name.ts b/packages/git/src/worktree-name.ts new file mode 100644 index 000000000..064cfc51e --- /dev/null +++ b/packages/git/src/worktree-name.ts @@ -0,0 +1,34 @@ +import * as crypto from "node:crypto"; + +/** + * Curated adjective-noun word list used to label worktrees. Words are short, + * filesystem-safe (lowercase a-z only), and chosen to be inoffensive. + */ +// biome-ignore format: keep word lists compact +const ADJECTIVES = [ + "amber", "brave", "calm", "clever", "cosmic", "crisp", "dapper", "dusty", + "eager", "fancy", "fluffy", "gentle", "happy", "jolly", "lively", "lucky", + "merry", "mighty", "nimble", "plucky", "proud", "quick", "quiet", "rapid", + "shiny", "silver", "smooth", "snappy", "spry", "sturdy", "sunny", "swift", + "tidy", "vivid", "witty", "zesty", +]; + +// biome-ignore format: keep word lists compact +const NOUNS = [ + "badger", "beetle", "bison", "cedar", "comet", "cricket", "delta", "ember", + "falcon", "ferret", "finch", "fjord", "glade", "harbor", "heron", "ibex", + "lemur", "lynx", "marlin", "meadow", "mountain", "otter", "panda", "petal", + "pebble", "puffin", "quokka", "raven", "river", "robin", "sparrow", "summit", + "tiger", "valley", "willow", "wombat", +]; + +/** + * Generates a short, human-readable random name (e.g. "swift-otter-42"). + * Suffix is a 2-digit number to reduce collisions while keeping names compact. + */ +export function generateHumanReadableName(): string { + const adjective = ADJECTIVES[crypto.randomInt(0, ADJECTIVES.length)]; + const noun = NOUNS[crypto.randomInt(0, NOUNS.length)]; + const suffix = crypto.randomInt(10, 100).toString(); + return `${adjective}-${noun}-${suffix}`; +} diff --git a/packages/git/src/worktree.ts b/packages/git/src/worktree.ts index 5fe1e7cd9..cb904eaa0 100644 --- a/packages/git/src/worktree.ts +++ b/packages/git/src/worktree.ts @@ -1,5 +1,4 @@ import { execFile, spawn } from "node:child_process"; -import * as crypto from "node:crypto"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import { getCleanEnv, getGitOperationManager } from "./operation-manager"; @@ -11,6 +10,7 @@ import { listWorktrees as listWorktreesRaw, } from "./queries"; import { clonePath, forceRemove, safeSymlink } from "./utils"; +import { generateHumanReadableName } from "./worktree-name"; export interface WorktreeInfo { worktreePath: string; @@ -44,7 +44,7 @@ export class WorktreeManager { } generateWorktreeName(): string { - return crypto.randomInt(1000, 10000).toString(); + return generateHumanReadableName(); } private getWorktreeBaseFolderPath(): string {