Skip to content

Commit 2d39f5f

Browse files
authored
[fix] Normalize <service>/<task> in duplicate, rename, restore (#182)
[fix] Normalize <service>/<task> in duplicate, rename, restore (#166) Route post-lookup args through ResolveTaskInput so the service prefix is not double-applied to branch construction targets. Extend FindArchivedBranch with a service parameter for monorepo restore.
1 parent 073cdfc commit 2d39f5f

6 files changed

Lines changed: 214 additions & 33 deletions

File tree

cmd/duplicate.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ var duplicateCmd = &cobra.Command{
4949
return err
5050
}
5151

52+
_, task = operations.ResolveTaskInput(task, repoRoot)
53+
5254
prefixes := resolver.AllPrefixes()
5355

5456
if wt.Branch == cfg.DefaultSource {
@@ -64,7 +66,7 @@ var duplicateCmd = &cobra.Command{
6466
asFlag, _ := cmd.Flags().GetString(flagAs)
6567
var newTask string
6668
if asFlag != "" {
67-
newTask = asFlag
69+
_, newTask = operations.ResolveTaskInput(asFlag, repoRoot)
6870
} else {
6971
// Auto-suffix: try task-1, task-2, etc.
7072
for i := 1; i <= maxDuplicateSuffix; i++ {

cmd/rename.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ var renameCmd = &cobra.Command{
3939
return err
4040
}
4141

42+
_, task = operations.ResolveTaskInput(task, repoRoot)
43+
_, newTask = operations.ResolveTaskInput(newTask, repoRoot)
44+
4245
wtDir := filepath.Join(repoRoot, cfg.WorktreeDir)
4346

4447
s := spinner.New(spinnerOpts(cmd))

cmd/restore.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ var restoreCmd = &cobra.Command{
2525
return completeArchivedTasks(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp
2626
},
2727
RunE: func(cmd *cobra.Command, args []string) error {
28-
task := args[0]
29-
cfg := config.FromContext(cmd.Context())
30-
3128
r := newRunner()
3229

3330
repoRoot, err := git.MainRepoRoot(r)
3431
if err != nil {
3532
return err
3633
}
3734

38-
branch, err := operations.FindArchivedBranch(r, task)
35+
service, task := operations.ResolveTaskInput(args[0], repoRoot)
36+
cfg := config.FromContext(cmd.Context())
37+
38+
branch, err := operations.FindArchivedBranch(r, service, task)
3939
if err != nil {
4040
return err
4141
}
@@ -65,13 +65,11 @@ var restoreCmd = &cobra.Command{
6565
configModules = cfg.Deps.Modules
6666
}
6767

68-
svc, _, _ := resolver.ServiceFromBranch(branch, resolver.AllPrefixes())
69-
7068
pcResult, err := operations.PostCreateSetup(r, operations.PostCreateParams{
7169
RepoRoot: repoRoot,
7270
WtPath: wtPath,
7371
Task: task,
74-
Service: svc,
72+
Service: service,
7573
CopyFiles: cfg.CopyFiles,
7674
SkipDeps: skipDeps,
7775
AutoDetect: cfg.IsAutoDetectDeps(),

internal/operations/archived.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import (
77
"github.com/lugassawan/rimba/internal/resolver"
88
)
99

10-
// FindArchivedBranch finds a branch for the given task that is not associated
11-
// with any active worktree. It tries prefix+task combinations first, then exact
12-
// match, then falls back to task extraction.
13-
func FindArchivedBranch(r git.Runner, task string) (string, error) {
10+
// FindArchivedBranch finds a branch for the given service+task that is not
11+
// associated with any active worktree. service may be empty for non-monorepo repos.
12+
func FindArchivedBranch(r git.Runner, service, task string) (string, error) {
1413
branches, err := git.LocalBranches(r)
1514
if err != nil {
1615
return "", fmt.Errorf("list branches: %w", err)
@@ -23,13 +22,13 @@ func FindArchivedBranch(r git.Runner, task string) (string, error) {
2322

2423
prefixes := resolver.AllPrefixes()
2524

26-
if b, ok := searchByPrefixedTask(branches, active, prefixes, task); ok {
25+
if b, ok := searchByPrefixedTask(branches, active, prefixes, service, task); ok {
2726
return b, nil
2827
}
29-
if b, ok := searchByExactMatch(branches, active, task); ok {
28+
if b, ok := searchByExactMatch(branches, active, service, task); ok {
3029
return b, nil
3130
}
32-
if b, ok := searchByTaskExtraction(branches, active, prefixes, task); ok {
31+
if b, ok := searchByTaskExtraction(branches, active, prefixes, service, task); ok {
3332
return b, nil
3433
}
3534

@@ -60,9 +59,10 @@ func ListArchivedBranches(r git.Runner, mainBranch string) ([]string, error) {
6059
}
6160

6261
// searchByPrefixedTask tries prefix+task combinations against branches.
63-
func searchByPrefixedTask(branches []string, active map[string]bool, prefixes []string, task string) (string, bool) {
62+
// For monorepo (service non-empty), candidates include the service segment.
63+
func searchByPrefixedTask(branches []string, active map[string]bool, prefixes []string, service, task string) (string, bool) {
6464
for _, p := range prefixes {
65-
candidate := resolver.BranchName(p, task)
65+
candidate := resolver.FullBranchName(service, p, task)
6666
for _, b := range branches {
6767
if b == candidate && !active[b] {
6868
return b, true
@@ -73,25 +73,34 @@ func searchByPrefixedTask(branches []string, active map[string]bool, prefixes []
7373
}
7474

7575
// searchByExactMatch checks if the task name exactly matches an inactive branch.
76-
func searchByExactMatch(branches []string, active map[string]bool, task string) (string, bool) {
76+
// For monorepo (service non-empty), also tries service+"/"+task as an exact branch name.
77+
func searchByExactMatch(branches []string, active map[string]bool, service, task string) (string, bool) {
7778
for _, b := range branches {
78-
if b == task && !active[b] {
79+
if !active[b] && (b == task || (service != "" && b == service+"/"+task)) {
7980
return b, true
8081
}
8182
}
8283
return "", false
8384
}
8485

8586
// searchByTaskExtraction finds a branch whose extracted task matches the given task.
86-
func searchByTaskExtraction(branches []string, active map[string]bool, prefixes []string, task string) (string, bool) {
87+
// For monorepo (service non-empty), also verifies the branch's service segment matches.
88+
func searchByTaskExtraction(branches []string, active map[string]bool, prefixes []string, service, task string) (string, bool) {
8789
for _, b := range branches {
8890
if active[b] {
8991
continue
9092
}
9193
t, _ := resolver.PureTaskFromBranch(b, prefixes)
92-
if t == task {
93-
return b, true
94+
if t != task {
95+
continue
96+
}
97+
if service != "" {
98+
svc, _, _ := resolver.ServiceFromBranch(b, prefixes)
99+
if svc != service {
100+
continue
101+
}
94102
}
103+
return b, true
95104
}
96105
return "", false
97106
}

internal/operations/archived_test.go

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const (
1010
cmdList = "list"
1111

1212
branchListArchived = "main\nfeature/archived-task\nfeature/active-task"
13+
14+
wtListMainOnly = "worktree /repo\nHEAD abc\nbranch refs/heads/main\n"
1315
)
1416

1517
func TestFindArchivedBranch(t *testing.T) {
@@ -27,7 +29,7 @@ func TestFindArchivedBranch(t *testing.T) {
2729
runInDir: noopRunInDir,
2830
}
2931

30-
branch, err := FindArchivedBranch(mr, "archived-task")
32+
branch, err := FindArchivedBranch(mr, "", "archived-task")
3133
if err != nil {
3234
t.Fatalf("FindArchivedBranch: %v", err)
3335
}
@@ -51,7 +53,7 @@ func TestFindArchivedBranchNotFound(t *testing.T) {
5153
runInDir: noopRunInDir,
5254
}
5355

54-
_, err := FindArchivedBranch(mr, "nonexistent")
56+
_, err := FindArchivedBranch(mr, "", "nonexistent")
5557
if err == nil {
5658
t.Fatal("expected error for nonexistent archived branch")
5759
}
@@ -64,14 +66,14 @@ func TestFindArchivedBranchExactMatch(t *testing.T) {
6466
case args[0] == cmdBranch:
6567
return "main\nmy-custom-branch", nil
6668
case args[0] == cmdWorktreeTest && args[1] == cmdList:
67-
return "worktree /repo\nHEAD abc\nbranch refs/heads/main\n", nil
69+
return wtListMainOnly, nil
6870
}
6971
return "", nil
7072
},
7173
runInDir: noopRunInDir,
7274
}
7375

74-
branch, err := FindArchivedBranch(mr, "my-custom-branch")
76+
branch, err := FindArchivedBranch(mr, "", "my-custom-branch")
7577
if err != nil {
7678
t.Fatalf("FindArchivedBranch: %v", err)
7779
}
@@ -87,14 +89,14 @@ func TestFindArchivedBranchByTaskExtraction(t *testing.T) {
8789
case args[0] == cmdBranch:
8890
return "main\nbugfix/some-task", nil
8991
case args[0] == cmdWorktreeTest && args[1] == cmdList:
90-
return "worktree /repo\nHEAD abc\nbranch refs/heads/main\n", nil
92+
return wtListMainOnly, nil
9193
}
9294
return "", nil
9395
},
9496
runInDir: noopRunInDir,
9597
}
9698

97-
branch, err := FindArchivedBranch(mr, "some-task")
99+
branch, err := FindArchivedBranch(mr, "", "some-task")
98100
if err != nil {
99101
t.Fatalf("FindArchivedBranch: %v", err)
100102
}
@@ -121,7 +123,7 @@ func TestFindArchivedBranchExactMatchSkipsActive(t *testing.T) {
121123
runInDir: noopRunInDir,
122124
}
123125

124-
branch, err := FindArchivedBranch(mr, "my-task")
126+
branch, err := FindArchivedBranch(mr, "", "my-task")
125127
if err != nil {
126128
t.Fatalf("FindArchivedBranch: %v", err)
127129
}
@@ -148,7 +150,7 @@ func TestFindArchivedBranchPrefixMatchSkipsActive(t *testing.T) {
148150
runInDir: noopRunInDir,
149151
}
150152

151-
branch, err := FindArchivedBranch(mr, "task-x")
153+
branch, err := FindArchivedBranch(mr, "", "task-x")
152154
if err != nil {
153155
t.Fatalf("FindArchivedBranch: %v", err)
154156
}
@@ -175,7 +177,7 @@ func TestFindArchivedBranchSkipsActiveInFallback(t *testing.T) {
175177
runInDir: noopRunInDir,
176178
}
177179

178-
_, err := FindArchivedBranch(mr, "some-task")
180+
_, err := FindArchivedBranch(mr, "", "some-task")
179181
if err == nil {
180182
t.Fatal("expected error when only matching branch is active")
181183
}
@@ -192,7 +194,7 @@ func TestFindArchivedBranchError(t *testing.T) {
192194
runInDir: noopRunInDir,
193195
}
194196

195-
_, err := FindArchivedBranch(mr, "any")
197+
_, err := FindArchivedBranch(mr, "", "any")
196198
if err == nil {
197199
t.Fatal("expected error from LocalBranches failure")
198200
}
@@ -212,7 +214,7 @@ func TestFindArchivedBranchWorktreeError(t *testing.T) {
212214
runInDir: noopRunInDir,
213215
}
214216

215-
_, err := FindArchivedBranch(mr, "task")
217+
_, err := FindArchivedBranch(mr, "", "task")
216218
if err == nil {
217219
t.Fatal("expected error from ListWorktrees failure")
218220
}
@@ -281,3 +283,93 @@ func TestListArchivedBranchesWorktreeError(t *testing.T) {
281283
t.Fatal("expected error from ListWorktrees failure")
282284
}
283285
}
286+
287+
func TestFindArchivedBranchMonorepoByPrefix(t *testing.T) {
288+
mr := &mockRunner{
289+
run: func(args ...string) (string, error) {
290+
switch {
291+
case args[0] == cmdBranch:
292+
return "main\nauth-api/feature/login", nil
293+
case args[0] == cmdWorktreeTest && args[1] == cmdList:
294+
return wtListMainOnly, nil
295+
}
296+
return "", nil
297+
},
298+
runInDir: noopRunInDir,
299+
}
300+
301+
branch, err := FindArchivedBranch(mr, "auth-api", "login")
302+
if err != nil {
303+
t.Fatalf("FindArchivedBranch: %v", err)
304+
}
305+
if branch != "auth-api/feature/login" {
306+
t.Errorf("branch = %q, want %q", branch, "auth-api/feature/login")
307+
}
308+
}
309+
310+
func TestFindArchivedBranchMonorepoByTaskExtraction(t *testing.T) {
311+
mr := &mockRunner{
312+
run: func(args ...string) (string, error) {
313+
switch {
314+
case args[0] == cmdBranch:
315+
return "main\nauth-api/bugfix/login", nil
316+
case args[0] == cmdWorktreeTest && args[1] == cmdList:
317+
return wtListMainOnly, nil
318+
}
319+
return "", nil
320+
},
321+
runInDir: noopRunInDir,
322+
}
323+
324+
branch, err := FindArchivedBranch(mr, "auth-api", "login")
325+
if err != nil {
326+
t.Fatalf("FindArchivedBranch: %v", err)
327+
}
328+
if branch != "auth-api/bugfix/login" {
329+
t.Errorf("branch = %q, want %q", branch, "auth-api/bugfix/login")
330+
}
331+
}
332+
333+
func TestFindArchivedBranchMonorepoWrongServiceNoMatch(t *testing.T) {
334+
mr := &mockRunner{
335+
run: func(args ...string) (string, error) {
336+
switch {
337+
case args[0] == cmdBranch:
338+
return "main\nweb-api/feature/login", nil
339+
case args[0] == cmdWorktreeTest && args[1] == cmdList:
340+
return wtListMainOnly, nil
341+
}
342+
return "", nil
343+
},
344+
runInDir: noopRunInDir,
345+
}
346+
347+
_, err := FindArchivedBranch(mr, "auth-api", "login")
348+
if err == nil {
349+
t.Fatal("expected error when service does not match")
350+
}
351+
}
352+
353+
func TestFindArchivedBranchNonMonorepoRegression(t *testing.T) {
354+
mr := &mockRunner{
355+
run: func(args ...string) (string, error) {
356+
switch {
357+
case args[0] == cmdBranch:
358+
return branchListArchived, nil
359+
case args[0] == cmdWorktreeTest && args[1] == cmdList:
360+
return "worktree /repo\nHEAD abc\nbranch refs/heads/main\n\n" +
361+
"worktree /wt/feature-active-task\nHEAD abc\nbranch refs/heads/feature/active-task\n", nil
362+
}
363+
return "", nil
364+
},
365+
runInDir: noopRunInDir,
366+
}
367+
368+
branch, err := FindArchivedBranch(mr, "", "archived-task")
369+
if err != nil {
370+
t.Fatalf("FindArchivedBranch: %v", err)
371+
}
372+
if branch != "feature/archived-task" {
373+
t.Errorf("branch = %q, want %q", branch, "feature/archived-task")
374+
}
375+
}

0 commit comments

Comments
 (0)