Skip to content

feat: add --branch flag to override worktree/branch name#315

Merged
umputun merged 5 commits intoumputun:masterfrom
bronislav:feat/branch-override-flag
Apr 30, 2026
Merged

feat: add --branch flag to override worktree/branch name#315
umputun merged 5 commits intoumputun:masterfrom
bronislav:feat/branch-override-flag

Conversation

@bronislav
Copy link
Copy Markdown
Contributor

@bronislav bronislav commented Apr 30, 2026

Summary

Related to #306. Replaces #310.

The --branch=NAME flag lets users explicitly set the branch/worktree name instead of relying on auto-derivation from the plan filename.

Motivation

PR #310 attempted to fix branch name derivation with a heuristic (generic filename detection + parent directory fallback). As @umputun pointed out in the review, this approach is too fragile for such a simple case:

  • The heuristic only helps when the filename is in a hardcoded set (tasks, plan, plans, index, readme) — listing all possible generic names is impossible, and users with spec.md, description.md, etc. get nothing
  • The --worktree mode was the only path where the heuristic actually changed anything; regular mode already works because preparePlanBranch returns early if already on a feature branch
  • A flag is strictly more general: works for any layout, custom namespacing (feat/foo), capitalized variants, arbitrary filenames
  • ~5 lines, no regex, no edge cases, self-documenting in --help

Changes

  • Add --branch=NAME CLI flag (overrides branch name derived from plan filename)
  • Thread BranchOverride through executePlanRequest, preparePlanBranch, CreateBranchForPlan, CreateWorktreeForPlan
  • Both the worktree path (.ralphex/worktrees/<name>) and the progress logger use the override when set
  • Works for worktree mode, normal mode, and plan-continuation mode
  • Added tests for CreateWorktreeForPlan and CreateBranchForPlan with override

Usage

# OpenSpec layout: tasks.md → would derive "tasks" without --branch
ralphex --worktree --branch=add-dark-mode openspec/changes/add-dark-mode/tasks.md

# Custom namespacing
ralphex --branch=feat/user-auth docs/plans/auth.md

# Generic plan filenames
ralphex --branch=my-feature docs/plans/plan.md

Test plan

  • go test ./pkg/git/... — new override test cases pass
  • make test — full suite green
  • Manual: ralphex --worktree --branch=my-branch docs/plans/tasks.md creates worktree at .ralphex/worktrees/my-branch on branch my-branch
  • Manual: without --branch, existing behavior unchanged

Replaces fragile auto-detection from plan filenames when plan files use
generic names (tasks.md, plan.md) or spec-driven directory layouts where
ExtractBranchName produces wrong or unstable results.
Extract resolveWorktreePlanFile helper from runWithWorktree to bring
cyclomatic complexity within the linter limit; add explanation to the
nolint:errcheck directive in service_test.go.
@bronislav bronislav force-pushed the feat/branch-override-flag branch from caf18fe to 636fc11 Compare April 30, 2026 16:07
Comment thread cmd/ralphex/main.go
}
}
}
wtPlanFile := resolveWorktreePlanFile(req.PlanFile, req.GitSvc.Root())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this code extracted to lower cyclomatic complexity warning raised by linter

@umputun umputun requested a review from Copilot April 30, 2026 16:11
@bronislav bronislav marked this pull request as ready for review April 30, 2026 16:12
@bronislav bronislav requested a review from umputun as a code owner April 30, 2026 16:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a CLI --branch=NAME override to explicitly control the feature branch/worktree name (instead of deriving it from the plan filename), improving robustness for generic plan filenames and spec-driven layouts.

Changes:

  • Introduces --branch flag and threads the override through plan execution (normal + worktree + continuation paths).
  • Updates git service APIs (CreateBranchForPlan, CreateWorktreeForPlan, internal preparePlanBranch) to accept and use the override.
  • Updates docs and extends git service tests with override scenarios.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
cmd/ralphex/main.go Adds --branch flag plumbing, uses override for worktree creation + progress logger branch label, refactors worktree plan path resolution into helper.
pkg/git/service.go Accepts branch override and uses it for branch/worktree creation (including early worktree path selection).
pkg/git/service_test.go Updates call sites for new signatures and adds override test cases for branch/worktree creation.
llms.txt Documents example usage of --branch.
CLAUDE.md Documents --branch behavior in worktree mode section.
Comments suppressed due to low confidence (1)

cmd/ralphex/main.go:711

  • When --branch is used, the feature branch name may differ from plan.ExtractBranchName(req.PlanFile), but CommitPlanFile derives its commit message from the plan filename (it calls plan.ExtractBranchName(planFile) internally). This can produce misleading commits like add plan: some-long-generated-name on branch my-custom-branch. Consider threading the resolved branch name/override into CommitPlanFile (or have CommitPlanFile use the current branch name) so the commit message matches the actual branch being used.
	wtPlanFile := resolveWorktreePlanFile(req.PlanFile, req.GitSvc.Root())

	// commit plan file on the feature branch (inside worktree), not on the default branch
	if planNeedsCommit {
		if commitErr := wtGitSvc.CommitPlanFile(req.PlanFile, req.GitSvc.Root()); commitErr != nil {
			return fmt.Errorf("commit plan in worktree: %w", commitErr)
		}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/git/service.go Outdated
Comment on lines 278 to 282
earlyBranch := branchOverride
if earlyBranch == "" {
earlyBranch = plan.ExtractBranchName(planFile)
}
wtPath := filepath.Join(s.repo.root(), ".ralphex", "worktrees", earlyBranch)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

branchOverride is used directly as a path element when building wtPath via filepath.Join(...). Because Join cleans .. segments, values like --branch=../../outside (or an absolute path) can escape .ralphex/worktrees/ and create/remove a worktree outside the repo on cleanup. Please validate/sanitize the override before using it in filesystem paths (e.g., reject absolute paths and ensure the final wtPath is within <repo>/.ralphex/worktrees using filepath.Rel), while still allowing safe namespacing like feat/foo if desired.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

@umputun umputun left a comment

Choose a reason for hiding this comment

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

couple of things to address before merge:

1. --branch is silently dropped in --plan mode

at cmd/ralphex/main.go:292-301, the runPlanMode branch builds executePlanRequest without BranchOverride: o.Branch. The non-plan path at line 314 sets it correctly. So ralphex --plan "..." --branch=foo ignores the override entirely, which is exactly the generated/generic-filename case the flag is supposed to help with. Fix is one line, but worth a test that locks the wiring.

2. CommitPlanFile commit message ignores the override

pkg/git/service.go:330 still does branchName := plan.ExtractBranchName(planFile) to build the commit subject. With --branch=my-feature on a 2026-04-30-some-name.md plan, the worktree is on my-feature but the auto-commit reads add plan: some-name. Copilot flagged this too. Easiest fix: have CommitPlanFile use the current branch from git rather than re-deriving from the filename, then this whole class of bug goes away.

3. branch-name derivation is duplicated in 4 places

preparePlanBranch:189, CreateWorktreeForPlan early at :280, runWithWorktree logger at cmd/ralphex/main.go:650, and the broken CommitPlanFile:330. Each one repeats if branchOverride != "" { use it } else { ExtractBranchName(planFile) }. Since most call sites are inside Service, makes sense as a method on *Service, something like s.effectiveBranchName(planFile, override), then all four sites delegate to it. That alone would have prevented #2 and removes the risk of forgetting a site for #1.

4. new override subtests are misplaced

at pkg/git/service_test.go:1261 and :1281. Both live inside TestService_RemoveWorktree (starts at 1200) but exercise CreateWorktreeForPlan and CreateBranchForPlan. Should move to TestService_CreateWorktreeForPlan (line 845) and TestService_CreateBranchForPlan (line 186) respectively. Tests-as-documentation gets weaker when the subtest name doesn't match its parent.

5. gofmt drift

gofmt -d cmd/ralphex/main.go shows a struct alignment diff on the BranchOverride field, the name is longer than the existing fields and the rest of the struct wants realignment. Golangci-lint doesn't catch it because gofmt isn't enabled in formatters: in .golangci.yml. make fmt fixes it. Small but a follow-up format pass will show a noisy diff if left.

Notes that aren't blockers

  • Copilot's path-traversal flag on service.go:282 is a false positive on impact. filepath.Join calls Clean which collapses .., then git worktree add -b rejects the bad ref before any directory is created (verified with git check-ref-format --branch '../../outside' exit 128). Cleanup never reaches an attacker-chosen path because WtCleanup is registered after successful creation. You may still want a defense-in-depth sanitizeBranchName for clearer error messages, but it's not a security issue.
  • PR body says "Closes part of #306", just confirming you don't intend the merge to auto-close #306, since the issue has more aspects (header parsing, MovePlanToCompleted skip, sibling spec files) that this PR correctly leaves for follow-up. "Related to #306" might be safer phrasing.
  • Consider also a test for --branch=existing-feature-branch (does it switch or error?) and one for the runWithWorktree → CommitPlanFile path with override set, which would have caught #2.

…eation

Reject absolute paths and values that escape .ralphex/worktrees/ when
--branch override is used as a filesystem path element.
- add EffectiveBranchName() method on Service, eliminating the 4-site
  override-or-extract duplication (preparePlanBranch, CreateWorktreeForPlan,
  runWithWorktree progress logger, CommitPlanFile)
- thread BranchOverride into runPlanMode so --plan --branch=foo no longer
  silently drops the override
- CommitPlanFile now uses current git branch for the commit message instead
  of re-deriving from the plan filename, fixing the mismatch when --branch
  differs from the filename slug
- move branch-override subtests from TestService_RemoveWorktree into
  TestService_CreateWorktreeForPlan and TestService_CreateBranchForPlan
- fix gofmt struct alignment on executePlanRequest.BranchOverride
Copy link
Copy Markdown
Owner

@umputun umputun left a comment

Choose a reason for hiding this comment

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

thx for working through these.

1, 2, 3, 5 all good. 2's currentBranch() approach is cleaner than what I suggested, sidesteps the threading entirely.

minor on 4: the CreateBranchForPlan override subtest is in the right place (TestService_CreateBranchForPlan:427), but the CreateWorktreeForPlan one ended up in TestService_CommitPlanFile:1217 instead of TestService_CreateWorktreeForPlan - the body only exercises CreateWorktreeForPlan, no commit verification. One-line move would finish it.

also right call reverting f5cd336, the path-traversal validation wasn't needed.

lgtm with the test-placement nit, fine to merge as-is.

@umputun umputun merged commit 567815d into umputun:master Apr 30, 2026
2 checks passed
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.

3 participants