fix(server): accept Git-valid worktree branch names (#3591)

Branch-off creation reused the filesystem slug validator even when callers supplied a separate branch name and worktree slug. Validate branch refs with Git at the worktree boundary so exact valid refs are preserved while path slugs remain restricted.
This commit is contained in:
Mohamed Boudra
2026-08-20 09:44:29 +02:00
committed by GitHub
parent 2e18620a28
commit 4dbd26c88c
4 changed files with 42 additions and 18 deletions
+4 -12
View File
@@ -42,7 +42,6 @@ import { spawnProcess } from "./spawn.js";
import { resolvePaseoHome } from "../server/paseo-home.js";
import { createExternalProcessEnv } from "../server/paseo-env.js";
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
import { expandTilde, getRealpathAwareRelativePath, isPathInsideRoot } from "./path.js";
import { terminateWithTreeKill } from "./tree-kill.js";
@@ -1310,7 +1309,7 @@ async function resolveWorktreeSourcePlan({
switch (source.kind) {
case "branch-off": {
const branchName = source.branchName;
validateWorktreeBranchName(branchName);
await validateGitBranchName(cwd, branchName);
const normalizedBaseBranch = normalizeRequiredBaseBranch(source.baseBranch);
const resolvedBaseBranch = await resolveBaseBranchForWorktree(cwd, source.baseBranch);
const branchExists = await localBranchExists(cwd, branchName);
@@ -1330,7 +1329,7 @@ async function resolveWorktreeSourcePlan({
};
}
case "checkout-branch": {
await validateExistingWorktreeBranchName(cwd, source.branchName);
await validateGitBranchName(cwd, source.branchName);
if (!(await localBranchExists(cwd, source.branchName))) {
try {
await runGitCommand(["fetch", "origin", `${source.branchName}:${source.branchName}`], {
@@ -1367,7 +1366,7 @@ async function resolveWorktreeSourcePlan({
case "checkout-change-request":
case "checkout-github-pr": {
const localBranchCandidate = source.localBranchName ?? source.headRef;
await validateExistingWorktreeBranchName(cwd, localBranchCandidate);
await validateGitBranchName(cwd, localBranchCandidate);
const localBranchName = await resolveUniqueLocalBranchName(cwd, localBranchCandidate);
const normalizedBaseRefName = normalizeRequiredBaseBranch(source.baseRefName);
const changeRequestNumber =
@@ -1589,14 +1588,7 @@ async function configureWorktreeTrackingRemote(options: {
);
}
function validateWorktreeBranchName(branchName: string): void {
const validation = validateBranchSlug(branchName);
if (!validation.valid) {
throw new Error(`Invalid branch name: ${validation.error}`);
}
}
async function validateExistingWorktreeBranchName(cwd: string, branchName: string): Promise<void> {
async function validateGitBranchName(cwd: string, branchName: string): Promise<void> {
const result = await runGitCommand(["check-ref-format", "--branch", branchName], {
cwd,
timeout: 30_000,