feat: add --branch flag to override worktree/branch name#315
feat: add --branch flag to override worktree/branch name#315umputun merged 5 commits intoumputun:masterfrom
Conversation
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.
caf18fe to
636fc11
Compare
| } | ||
| } | ||
| } | ||
| wtPlanFile := resolveWorktreePlanFile(req.PlanFile, req.GitSvc.Root()) |
There was a problem hiding this comment.
this code extracted to lower cyclomatic complexity warning raised by linter
There was a problem hiding this comment.
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
--branchflag and threads the override through plan execution (normal + worktree + continuation paths). - Updates git service APIs (
CreateBranchForPlan,CreateWorktreeForPlan, internalpreparePlanBranch) 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
--branchis used, the feature branch name may differ fromplan.ExtractBranchName(req.PlanFile), butCommitPlanFilederives its commit message from the plan filename (it callsplan.ExtractBranchName(planFile)internally). This can produce misleading commits likeadd plan: some-long-generated-nameon branchmy-custom-branch. Consider threading the resolved branch name/override intoCommitPlanFile(or haveCommitPlanFileuse 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.
| earlyBranch := branchOverride | ||
| if earlyBranch == "" { | ||
| earlyBranch = plan.ExtractBranchName(planFile) | ||
| } | ||
| wtPath := filepath.Join(s.repo.root(), ".ralphex", "worktrees", earlyBranch) |
There was a problem hiding this comment.
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.
umputun
left a comment
There was a problem hiding this comment.
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:282is a false positive on impact.filepath.JoincallsCleanwhich collapses.., thengit worktree add -brejects the bad ref before any directory is created (verified withgit check-ref-format --branch '../../outside'exit 128). Cleanup never reaches an attacker-chosen path becauseWtCleanupis registered after successful creation. You may still want a defense-in-depthsanitizeBranchNamefor 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 therunWithWorktree → CommitPlanFilepath 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.
…ktree creation" This reverts commit f5cd336.
- 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
umputun
left a comment
There was a problem hiding this comment.
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.
Summary
Related to #306. Replaces #310.
The
--branch=NAMEflag 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:
tasks,plan,plans,index,readme) — listing all possible generic names is impossible, and users withspec.md,description.md, etc. get nothing--worktreemode was the only path where the heuristic actually changed anything; regular mode already works becausepreparePlanBranchreturns early if already on a feature branchfeat/foo), capitalized variants, arbitrary filenames--helpChanges
--branch=NAMECLI flag (overrides branch name derived from plan filename)BranchOverridethroughexecutePlanRequest,preparePlanBranch,CreateBranchForPlan,CreateWorktreeForPlan.ralphex/worktrees/<name>) and the progress logger use the override when setCreateWorktreeForPlanandCreateBranchForPlanwith overrideUsage
Test plan
go test ./pkg/git/...— new override test cases passmake test— full suite greenralphex --worktree --branch=my-branch docs/plans/tasks.mdcreates worktree at.ralphex/worktrees/my-branchon branchmy-branch--branch, existing behavior unchanged