Skip to content

feat: add configurable worktrees directory path#1322

Open
naaa760 wants to merge 1 commit intogeneralaction:mainfrom
naaa760:feat/cust-wrkt-path
Open

feat: add configurable worktrees directory path#1322
naaa760 wants to merge 1 commit intogeneralaction:mainfrom
naaa760:feat/cust-wrkt-path

Conversation

@naaa760
Copy link
Contributor

@naaa760 naaa760 commented Mar 6, 2026

summary

  • adds a global setting to choose where worktrees are created. When empty, behavior stays the same (../worktrees).
  • supports absolute paths and ~ for home. Validates that the path exists and is writable.

Fixes

fix : #1313

Snapshot

  • settings → Repository → new “Worktrees directory” input.

Type of change

  • New feature (non-breaking change which adds functionality)

Mandatory Tasks

[ ] I have self-reviewed the code
[ ] A decent size PR without self-review might be rejected
Checklist
[ ] I have read the contributing guide
[ ] My code follows the style guidelines of this project (pnpm run format)
[ ] I have commented my code, particularly in hard-to-understand areas
[ ] I have checked if my PR needs changes to the documentation
[ ] I have checked if my changes generate no new warnings (pnpm run lint)
[ ] I have added tests that prove my fix is effective or that my feature works
[ ] I haven't checked if new and existing unit tests pass locally with my changes

Ctrl+K to generate command

@vercel
Copy link

vercel bot commented Mar 6, 2026

@naaa760 is attempting to deploy a commit to the General Action Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds a global "Worktrees directory" setting that lets users choose where git worktrees are created, replacing the previously hard-coded ../worktrees sibling path. The setting supports absolute paths and ~-prefixed home-relative paths, is validated before saving, and is propagated consistently across WorktreeService, WorktreePoolService, and githubIpc.

However, three critical issues must be fixed before merging:

  1. Runtime crash in WorktreePoolService.cleanupOrphanedReserves: The homedir variable was removed but is still referenced on lines 593–597, causing a ReferenceError at startup.

  2. Validation/resolution path mismatch: validateWorktreesPath resolves relative paths against process.cwd(), while getWorktreesBaseDir resolves them against projectPath. When these differ (typical in Electron), a relative path can pass validation but refer to the wrong location at runtime. Relative paths should be rejected in favor of absolute or ~-prefixed paths.

  3. Missing directory-type check in validation: accessSync succeeds for writable files as well as directories. If a user points to a file, validation passes but worktree creation fails later at the OS level. Add an isDirectory() check.

Confidence Score: 1/5

  • Not safe to merge — contains a runtime crash on startup and two validation correctness issues.
  • The PR has three blockers: (1) a reference error in cleanupOrphanedReserves that will crash the app ~2 seconds after startup (line 593–597), (2) a path resolution mismatch that silently directs relative paths to wrong locations, and (3) missing directory validation that allows files to pass validation. The first alone prevents merging; all three must be fixed.
  • src/main/services/WorktreePoolService.ts (runtime crash) and src/main/settings.ts (validation issues)

Sequence Diagram

sequenceDiagram
    participant User as User (Settings UI)
    participant Renderer as RepositorySettingsCard
    participant IPC as settingsIpc (Main)
    participant Settings as settings.ts
    participant Services as WorktreeService / WorktreePoolService / githubIpc

    User->>Renderer: blur "Worktrees directory" input
    Renderer->>IPC: rpc.appSettings.validateWorktreesPath(value)
    IPC->>Settings: validateWorktreesPath(path)
    Settings-->>IPC: null (ok) or error string
    IPC-->>Renderer: null | error

    alt validation passes
        Renderer->>IPC: updateSettings({ repository: { worktreesDirectory: value } })
        IPC->>Settings: updateAppSettings(partial)
        Settings->>Settings: normalizeSettings() + validateWorktreesPath()
        Settings-->>IPC: AppSettings (persisted to disk)
        IPC-->>Renderer: updated settings
    else validation fails
        Renderer->>User: show inline error message
    end

    User->>Services: create worktree (task / PR / pool reserve)
    Services->>Settings: getAppSettings()
    Settings-->>Services: settings.repository.worktreesDirectory
    Services->>Settings: getWorktreesBaseDir(projectPath, worktreesDirectory)
    Settings-->>Services: resolved base directory path
    Services->>Services: path.join(base, sluggedName-hash)
Loading

Comments Outside Diff (1)

  1. src/main/services/WorktreePoolService.ts, line 593-597 (link)

    homedir is not defined. The variable was removed in the diff (line const homedir = require('os').homedir(); was deleted), but the subsequent lines still reference it. This will cause a ReferenceError at runtime when cleanupOrphanedReserves runs (~2 seconds after app startup).

    Alternatively, add import { homedir } from 'node:os'; at the top and call homedir() directly (without require).

Last reviewed commit: 1651873

Comment on lines +594 to +620
export function validateWorktreesPath(dirPath: string): string | null {
const trimmed = typeof dirPath === 'string' ? dirPath.trim() : '';
if (!trimmed) return null;
let expanded = trimmed;
if (expanded.startsWith('~')) {
expanded = join(homedir(), expanded.slice(1));
}
const resolved = resolve(expanded);
if (existsSync(resolved)) {
try {
accessSync(resolved, constants.W_OK);
return null;
} catch {
return `Directory exists but is not writable: ${resolved}`;
}
}
const parent = dirname(resolved);
if (!existsSync(parent)) {
return `Parent directory does not exist: ${parent}`;
}
try {
accessSync(parent, constants.W_OK);
return null;
} catch {
return `Parent directory is not writable: ${parent}`;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation and runtime path resolution use different bases for relative paths:

  • validateWorktreesPath resolves relative paths with resolve(expanded) → anchored to process.cwd()
  • getWorktreesBaseDir resolves relative paths with resolve(projectPath, expanded) → anchored to projectPath

When process.cwd() !== projectPath (typical in Electron), a relative path like ../shared-worktrees will pass validation (if it exists relative to cwd) but resolve to a different location at runtime (relative to projectPath).

Since this validator is exposed over IPC and the renderer can't meaningfully provide projectPath, reject relative paths entirely and accept only absolute paths or ~-prefixed home paths:

Suggested change
export function validateWorktreesPath(dirPath: string): string | null {
const trimmed = typeof dirPath === 'string' ? dirPath.trim() : '';
if (!trimmed) return null;
let expanded = trimmed;
if (expanded.startsWith('~')) {
expanded = join(homedir(), expanded.slice(1));
}
const resolved = resolve(expanded);
if (existsSync(resolved)) {
try {
accessSync(resolved, constants.W_OK);
return null;
} catch {
return `Directory exists but is not writable: ${resolved}`;
}
}
const parent = dirname(resolved);
if (!existsSync(parent)) {
return `Parent directory does not exist: ${parent}`;
}
try {
accessSync(parent, constants.W_OK);
return null;
} catch {
return `Parent directory is not writable: ${parent}`;
}
}
export function validateWorktreesPath(dirPath: string): string | null {
const trimmed = typeof dirPath === 'string' ? dirPath.trim() : '';
if (!trimmed) return null;
let expanded = trimmed;
// Reject relative paths
if (!expanded.startsWith('/') && !expanded.startsWith('~')) {
return 'Path must be absolute or start with ~ (home directory)';
}
if (expanded.startsWith('~')) {
expanded = join(homedir(), expanded.slice(1));
}
const resolved = resolve(expanded);
// ... rest of validation

Comment on lines +602 to +608
if (existsSync(resolved)) {
try {
accessSync(resolved, constants.W_OK);
return null;
} catch {
return `Directory exists but is not writable: ${resolved}`;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation accepts any writable path, including files. The accessSync(resolved, constants.W_OK) check passes for regular files as well as directories. If a user accidentally points to a file, validation silently succeeds but subsequent worktree creation will fail at the OS level.

Add a directory check:

Suggested change
if (existsSync(resolved)) {
try {
accessSync(resolved, constants.W_OK);
return null;
} catch {
return `Directory exists but is not writable: ${resolved}`;
}
if (existsSync(resolved)) {
try {
const stat = require('node:fs').lstatSync(resolved);
if (!stat.isDirectory()) {
return `Path exists but is not a directory: ${resolved}`;
}
accessSync(resolved, constants.W_OK);
return null;
} catch (e: any) {
if (e?.message?.includes('is not a directory')) throw e;
return `Directory exists but is not writable: ${resolved}`;
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Customizable worktrees directory path

1 participant