feat: better branch titles
This commit is contained in:
2
index.ts
2
index.ts
@@ -260,7 +260,7 @@ async function executePlanBatches(
|
|||||||
|
|
||||||
// Clean up stale worktrees from interrupted runs before starting.
|
// Clean up stale worktrees from interrupted runs before starting.
|
||||||
if (config.execution.worktrees !== "never" && projectDir) {
|
if (config.execution.worktrees !== "never" && projectDir) {
|
||||||
const removed = cleanupStaleWorktrees(projectDir, progress.getKey());
|
const removed = cleanupStaleWorktrees(projectDir, config.paths.stateDir);
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
`Cleaned up ${removed.length} stale worktree(s) from previous run.`,
|
`Cleaned up ${removed.length} stale worktree(s) from previous run.`,
|
||||||
|
|||||||
@@ -778,6 +778,8 @@ async function executeTask(
|
|||||||
config.paths.stateDir,
|
config.paths.stateDir,
|
||||||
task.id,
|
task.id,
|
||||||
progress.getKey(),
|
progress.getKey(),
|
||||||
|
undefined,
|
||||||
|
task.title,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
const worktreeDir = wt?.dir ?? projectDir;
|
const worktreeDir = wt?.dir ?? projectDir;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ensureDir } from "./utils";
|
|||||||
export interface WorktreeHandle {
|
export interface WorktreeHandle {
|
||||||
/** Absolute path to the worktree working directory. */
|
/** Absolute path to the worktree working directory. */
|
||||||
dir: string;
|
dir: string;
|
||||||
/** Branch name: `ralpi/<prdKey>/<taskId>`. */
|
/** Branch name: slugified task title, or `ralpi/<prdKey>/<taskId>` as a fallback. */
|
||||||
branch: string;
|
branch: string;
|
||||||
/** Main repo directory (where the primary working tree lives). */
|
/** Main repo directory (where the primary working tree lives). */
|
||||||
mainDir: string;
|
mainDir: string;
|
||||||
@@ -108,12 +108,36 @@ function safeBranchSuffix(taskId: string): string {
|
|||||||
return taskId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
return taskId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise a free-form task title into a git-branch-safe slug.
|
||||||
|
*
|
||||||
|
* Lowercases, replaces runs of non-alphanumeric characters with single
|
||||||
|
* hyphens, trims leading/trailing hyphens, and caps the length so the
|
||||||
|
* branch name stays readable and within reasonable git limits.
|
||||||
|
*
|
||||||
|
* Returns an empty string when the title produces no usable slug.
|
||||||
|
*/
|
||||||
|
function slugifyTitle(title: string): string {
|
||||||
|
return title
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
.slice(0, 60);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a git worktree for a task.
|
* Create a git worktree for a task.
|
||||||
*
|
*
|
||||||
* The worktree is created at `<mainDir>/.ralpi/worktrees/<taskId>` on a new
|
* The worktree is created at `<mainDir>/.ralpi/worktrees/<taskId>` on a new
|
||||||
* branch `ralpi/<prdKey>/<taskId>`, based at `baseRef` (defaults to the
|
* branch. When `taskTitle` is provided the branch name is the slugified title
|
||||||
* current HEAD of `mainDir`).
|
* alone (e.g. `fix-plans-tab-grammar-casing-icons`); otherwise it falls back
|
||||||
|
* to `ralpi/<prdKey>/<taskId>`. Based at `baseRef` (defaults to the current
|
||||||
|
* HEAD of `mainDir`).
|
||||||
|
*
|
||||||
|
* The worktree directory always uses the bare `taskId` for a stable path;
|
||||||
|
* stale-worktree cleanup identifies ralpi worktrees by that path, not by
|
||||||
|
* branch name, so descriptive branch names are safe.
|
||||||
*
|
*
|
||||||
* Returns null if `mainDir` is not a git repo or the worktree creation fails.
|
* Returns null if `mainDir` is not a git repo or the worktree creation fails.
|
||||||
*/
|
*/
|
||||||
@@ -123,6 +147,7 @@ export function createWorktree(
|
|||||||
taskId: string,
|
taskId: string,
|
||||||
prdKey: string,
|
prdKey: string,
|
||||||
baseRef?: string,
|
baseRef?: string,
|
||||||
|
taskTitle?: string,
|
||||||
): WorktreeHandle | null {
|
): WorktreeHandle | null {
|
||||||
if (!isGitRepo(mainDir)) return null;
|
if (!isGitRepo(mainDir)) return null;
|
||||||
|
|
||||||
@@ -130,7 +155,8 @@ export function createWorktree(
|
|||||||
if (!ref) return null;
|
if (!ref) return null;
|
||||||
|
|
||||||
const safeId = safeBranchSuffix(taskId);
|
const safeId = safeBranchSuffix(taskId);
|
||||||
const branch = `ralpi/${prdKey}/${safeId}`;
|
const slug = taskTitle ? slugifyTitle(taskTitle) : "";
|
||||||
|
const branch = slug || `ralpi/${prdKey}/${safeId}`;
|
||||||
const wtDir = worktreePath(mainDir, stateDir, taskId);
|
const wtDir = worktreePath(mainDir, stateDir, taskId);
|
||||||
|
|
||||||
// Ensure the parent directory exists so `git worktree add` can create
|
// Ensure the parent directory exists so `git worktree add` can create
|
||||||
@@ -271,13 +297,14 @@ export function removeWorktree(mainDir: string, wt: WorktreeHandle): void {
|
|||||||
/**
|
/**
|
||||||
* Clean up stale worktrees from interrupted runs.
|
* Clean up stale worktrees from interrupted runs.
|
||||||
*
|
*
|
||||||
* Lists all worktrees whose branches start with `ralpi/<prdKey>/` and
|
* Identifies ralpi-owned worktrees by their path living under
|
||||||
* removes them. Called at the start of a loop to ensure a clean slate.
|
* `<mainDir>/<stateDir>/worktrees/` and removes them. Called at the start
|
||||||
* Returns the list of removed worktree directories.
|
* of a loop to ensure a clean slate. Returns the list of removed worktree
|
||||||
|
* directories.
|
||||||
*/
|
*/
|
||||||
export function cleanupStaleWorktrees(
|
export function cleanupStaleWorktrees(
|
||||||
mainDir: string,
|
mainDir: string,
|
||||||
prdKey: string,
|
stateDir: string,
|
||||||
): string[] {
|
): string[] {
|
||||||
const removed: string[] = [];
|
const removed: string[] = [];
|
||||||
|
|
||||||
@@ -287,6 +314,9 @@ export function cleanupStaleWorktrees(
|
|||||||
const list = git("worktree list --porcelain", mainDir);
|
const list = git("worktree list --porcelain", mainDir);
|
||||||
if (!list) return removed;
|
if (!list) return removed;
|
||||||
|
|
||||||
|
// Worktrees we manage live under <mainDir>/<stateDir>/worktrees/.
|
||||||
|
const managedRoot = path.resolve(mainDir, stateDir, "worktrees");
|
||||||
|
|
||||||
// Parse worktree list: each entry is `worktree <path>` followed by metadata.
|
// Parse worktree list: each entry is `worktree <path>` followed by metadata.
|
||||||
const wtLines = list
|
const wtLines = list
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -297,14 +327,18 @@ export function cleanupStaleWorktrees(
|
|||||||
// Skip the main working tree (always first in the list).
|
// Skip the main working tree (always first in the list).
|
||||||
if (path.resolve(wtDir) === path.resolve(mainDir)) continue;
|
if (path.resolve(wtDir) === path.resolve(mainDir)) continue;
|
||||||
|
|
||||||
// Check if this worktree is on a ralpi branch for this PRD.
|
// Only touch worktrees that live under the ralpi worktrees directory.
|
||||||
const branch = git(`rev-parse --abbrev-ref HEAD`, wtDir);
|
const resolved = path.resolve(wtDir);
|
||||||
if (!branch) continue;
|
if (
|
||||||
if (!branch.startsWith(`ralpi/${prdKey}/`)) continue;
|
resolved !== managedRoot &&
|
||||||
|
!resolved.startsWith(managedRoot + path.sep)
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Remove the worktree and its branch.
|
// Remove the worktree and its branch.
|
||||||
git(`worktree remove --force "${wtDir}"`, mainDir);
|
git(`worktree remove --force "${wtDir}"`, mainDir);
|
||||||
if (branch !== "HEAD" && branch !== "detached") {
|
const branch = git(`rev-parse --abbrev-ref HEAD`, wtDir);
|
||||||
|
if (branch && branch !== "HEAD" && branch !== "detached") {
|
||||||
git(`branch -D "${branch}"`, mainDir);
|
git(`branch -D "${branch}"`, mainDir);
|
||||||
}
|
}
|
||||||
removed.push(wtDir);
|
removed.push(wtDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user