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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"fast-glob": "^3.3.3",
"file-uri-to-path": "^1.0.0",
"framer-motion": "^12.23.26",
"friendly-words": "^1.3.1",
"fuse.js": "^7.1.0",
"http-proxy": "^1.18.1",
"idb": "^8.0.3",
Expand Down Expand Up @@ -126,7 +127,6 @@
"tailwind-merge": "^3.4.0",
"trpc-electron": "^0.1.2",
"tw-animate-css": "^1.4.0",
"unique-names-generator": "^4.7.1",
"zod": "^4.3.5",
"zustand": "^5.0.8"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
generateBranchName,
getCurrentBranch,
listBranches,
safeCheckoutBranch,
worktreeExists,
} from "../utils/git";
Expand Down Expand Up @@ -47,7 +48,11 @@ export const createCreateProcedures = () => {
throw new Error(`Project ${input.projectId} not found`);
}

const branch = input.branchName?.trim() || generateBranchName();
// Get existing branches to avoid name collisions
const { local, remote } = await listBranches(project.mainRepoPath);
const existingBranches = [...local, ...remote];
const branch =
input.branchName?.trim() || generateBranchName(existingBranches);

const worktreePath = join(
homedir(),
Expand Down
50 changes: 35 additions & 15 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { execFile } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdir, readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";

import friendlyWords = require("friendly-words");

import simpleGit, { type StatusResult } from "simple-git";
import {
adjectives,
animals,
uniqueNamesGenerator,
} from "unique-names-generator";
import { checkGitLfsAvailable, getShellEnvironment } from "./shell-env";

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -294,16 +291,39 @@ function isEnoent(error: unknown): boolean {
);
}

export function generateBranchName(): string {
const name = uniqueNamesGenerator({
dictionaries: [adjectives, animals],
separator: "-",
length: 2,
style: "lowerCase",
});
const suffix = randomBytes(3).toString("hex");
/** Maximum attempts to find a unique word before falling back to suffixed names */
const MAX_ATTEMPTS = 10;
/** Maximum suffix value to try in fallback (exclusive), e.g., 0-99 */
const FALLBACK_MAX_SUFFIX = 100;

/**
* Generates a random branch name using a single friendly word.
* Checks against existing branches to avoid collisions.
* With ~3000 words, collisions are rare even with hundreds of branches.
*/
export function generateBranchName(existingBranches: string[] = []): string {
const words = friendlyWords.objects as string[];
const existingSet = new Set(existingBranches.map((b) => b.toLowerCase()));

// Try to find a unique word
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const word = words[Math.floor(Math.random() * words.length)];
if (!existingSet.has(word.toLowerCase())) {
return word;
}
}

// Fallback: try word with numeric suffix
const baseWord = words[Math.floor(Math.random() * words.length)];
for (let n = 0; n < FALLBACK_MAX_SUFFIX; n++) {
const candidate = `${baseWord}-${n}`;
if (!existingSet.has(candidate.toLowerCase())) {
return candidate;
}
}

return `${name}-${suffix}`;
// Final fallback: use timestamp to guarantee uniqueness
return `${baseWord}-${Date.now()}`;
}

export async function createWorktree(
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/types/friendly-words.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module "friendly-words" {
const friendlyWords: {
objects: string[];
predicates: string[];
teams: string[];
collections: string[];
};
export = friendlyWords;
}
Loading