Skip to content

Commit 1d58173

Browse files
authored
[refactor] Extract operations package from cmd layer (#49)
* [refactor] Create internal/operations package with shared worktree helpers Extract ListWorktreeInfos and FindWorktree from cmd/helpers.go into internal/operations/ for reuse by future consumers (exec cmd, MCP server). * [refactor] Wire cmd/helpers.go to operations package listWorktreeInfos and findWorktree become thin wrappers delegating to operations.ListWorktreeInfos and operations.FindWorktree. * [refactor] Extract sync pure functions to operations package Move SyncBranch, CollectTasks, FilterEligible, SyncMethodLabel from cmd/sync.go to internal/operations/sync.go. Remove redundant cmd-layer tests that are now covered by operations/sync_test.go. * [refactor] Extract SyncWorktree to operations package Add SyncWorktreeResult struct and SyncWorktree function that returns a result value instead of writing to stdout. cmd/sync.go syncWorktree becomes a thin wrapper that formats the result for display. * [refactor] Extract rename operation to operations package Move worktree rename logic (prefix resolution, branch check, move, rename) into operations.RenameWorktree returning RenameResult. The cmd layer keeps only findWorktree + spinner + formatting. * [refactor] Extract status collection and filtering to operations package Move CollectWorktreeStatus and FilterDetailsByStatus from cmd/list.go into internal/operations/status.go. The cmd layer keeps parallel orchestration and display logic.
1 parent 5db01b3 commit 1d58173

14 files changed

Lines changed: 917 additions & 296 deletions

cmd/helpers.go

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package cmd
22

33
import (
4-
"fmt"
54
"path/filepath"
65

76
"github.com/lugassawan/rimba/internal/config"
87
"github.com/lugassawan/rimba/internal/git"
8+
"github.com/lugassawan/rimba/internal/operations"
99
"github.com/lugassawan/rimba/internal/resolver"
1010
"github.com/lugassawan/rimba/internal/spinner"
1111
"github.com/lugassawan/rimba/internal/termcolor"
@@ -48,31 +48,10 @@ func resolveMainBranch(r git.Runner) (string, error) {
4848

4949
// listWorktreeInfos converts git worktree entries to resolver-compatible WorktreeInfo slice.
5050
func listWorktreeInfos(r git.Runner) ([]resolver.WorktreeInfo, error) {
51-
entries, err := git.ListWorktrees(r)
52-
if err != nil {
53-
return nil, err
54-
}
55-
56-
worktrees := make([]resolver.WorktreeInfo, len(entries))
57-
for i, e := range entries {
58-
worktrees[i] = resolver.WorktreeInfo{
59-
Path: e.Path,
60-
Branch: e.Branch,
61-
}
62-
}
63-
return worktrees, nil
51+
return operations.ListWorktreeInfos(r)
6452
}
6553

6654
// findWorktree looks up a worktree by task name.
6755
func findWorktree(r git.Runner, task string) (resolver.WorktreeInfo, error) {
68-
worktrees, err := listWorktreeInfos(r)
69-
if err != nil {
70-
return resolver.WorktreeInfo{}, err
71-
}
72-
73-
wt, found := resolver.FindBranchForTask(task, worktrees, resolver.AllPrefixes())
74-
if !found {
75-
return resolver.WorktreeInfo{}, fmt.Errorf(errWorktreeNotFound, task)
76-
}
77-
return wt, nil
56+
return operations.FindWorktree(r, task)
7857
}

cmd/list.go

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/lugassawan/rimba/internal/config"
1111
"github.com/lugassawan/rimba/internal/git"
1212
"github.com/lugassawan/rimba/internal/hint"
13+
"github.com/lugassawan/rimba/internal/operations"
1314
"github.com/lugassawan/rimba/internal/resolver"
1415
"github.com/lugassawan/rimba/internal/spinner"
1516
"github.com/lugassawan/rimba/internal/termcolor"
@@ -146,30 +147,13 @@ var listCmd = &cobra.Command{
146147
sem <- struct{}{}
147148
defer func() { <-sem }()
148149

149-
var status resolver.WorktreeStatus
150-
if dirty, err := git.IsDirty(r, c.entry.Path); err == nil && dirty {
151-
status.Dirty = true
152-
}
153-
ahead, behind, _ := git.AheadBehind(r, c.entry.Path)
154-
status.Ahead = ahead
155-
status.Behind = behind
156-
150+
status := operations.CollectWorktreeStatus(r, c.entry.Path)
157151
rows[idx] = resolver.NewWorktreeDetail(c.entry.Branch, prefixes, c.displayPath, status, c.isCurrent)
158152
}(i, c)
159153
}
160154
wg.Wait()
161155

162-
filtered := rows[:0]
163-
for _, row := range rows {
164-
if listDirty && !row.Status.Dirty {
165-
continue
166-
}
167-
if listBehind && row.Status.Behind == 0 {
168-
continue
169-
}
170-
filtered = append(filtered, row)
171-
}
172-
rows = filtered
156+
rows = operations.FilterDetailsByStatus(rows, listDirty, listBehind)
173157

174158
s.Stop()
175159

cmd/rename.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
"github.com/lugassawan/rimba/internal/config"
88
"github.com/lugassawan/rimba/internal/git"
9-
"github.com/lugassawan/rimba/internal/resolver"
9+
"github.com/lugassawan/rimba/internal/operations"
1010
"github.com/lugassawan/rimba/internal/spinner"
1111
"github.com/spf13/cobra"
1212
)
@@ -44,33 +44,16 @@ var renameCmd = &cobra.Command{
4444
return err
4545
}
4646

47-
prefixes := resolver.AllPrefixes()
48-
49-
_, matchedPrefix := resolver.TaskFromBranch(wt.Branch, prefixes)
50-
if matchedPrefix == "" {
51-
matchedPrefix, _ = resolver.PrefixString(resolver.DefaultPrefixType)
52-
}
53-
54-
newBranch := resolver.BranchName(matchedPrefix, newTask)
55-
56-
if git.BranchExists(r, newBranch) {
57-
return fmt.Errorf("branch %q already exists", newBranch)
58-
}
59-
6047
wtDir := filepath.Join(repoRoot, cfg.WorktreeDir)
61-
newPath := resolver.WorktreePath(wtDir, newBranch)
6248

6349
s := spinner.New(spinnerOpts(cmd))
6450
defer s.Stop()
6551

6652
force, _ := cmd.Flags().GetBool("force")
6753
s.Start("Renaming worktree...")
68-
if err := git.MoveWorktree(r, wt.Path, newPath, force); err != nil {
69-
return err
70-
}
7154

72-
if err := git.RenameBranch(r, wt.Branch, newBranch); err != nil {
73-
return fmt.Errorf("worktree moved but failed to rename branch %q: %w\nTo complete manually: git branch -m %s %s", wt.Branch, err, wt.Branch, newBranch)
55+
if _, err := operations.RenameWorktree(r, wt, newTask, wtDir, force); err != nil {
56+
return err
7457
}
7558

7659
s.Stop()

cmd/sync.go

Lines changed: 21 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/lugassawan/rimba/internal/config"
99
"github.com/lugassawan/rimba/internal/git"
1010
"github.com/lugassawan/rimba/internal/hint"
11+
"github.com/lugassawan/rimba/internal/operations"
1112
"github.com/lugassawan/rimba/internal/resolver"
1213
"github.com/lugassawan/rimba/internal/spinner"
1314
"github.com/spf13/cobra"
@@ -17,7 +18,6 @@ const (
1718
flagAll = "all"
1819
flagSyncMerge = "merge"
1920
flagIncludeInherited = "include-inherited"
20-
verbRebase = "rebase"
2121

2222
hintAll = "Sync all eligible worktrees at once"
2323
hintSyncMerge = "Use merge instead of rebase (preserves history, creates merge commits)"
@@ -119,18 +119,18 @@ func syncOne(sc syncContext, task string, worktrees []resolver.WorktreeInfo, pre
119119
verb = "Merging"
120120
}
121121
sc.s.Update(fmt.Sprintf("%s onto %s...", verb, sc.cfg.DefaultSource))
122-
if err := doSync(sc.r, wt.Path, sc.cfg.DefaultSource, useMerge); err != nil {
122+
if err := operations.SyncBranch(sc.r, wt.Path, sc.cfg.DefaultSource, useMerge); err != nil {
123123
return err
124124
}
125125

126126
sc.s.Stop()
127-
fmt.Fprintf(sc.cmd.OutOrStdout(), "%s %s onto %s\n", syncMethodLabel(useMerge), wt.Branch, sc.cfg.DefaultSource)
127+
fmt.Fprintf(sc.cmd.OutOrStdout(), "%s %s onto %s\n", operations.SyncMethodLabel(useMerge), wt.Branch, sc.cfg.DefaultSource)
128128
return nil
129129
}
130130

131131
func syncAll(sc syncContext, worktrees []resolver.WorktreeInfo, prefixes []string, useMerge, includeInherited bool) error { //nolint:unparam // error return matches RunE contract
132-
allTasks := collectTasks(worktrees, prefixes)
133-
eligible := filterEligible(worktrees, prefixes, sc.cfg.DefaultSource, allTasks, includeInherited)
132+
allTasks := operations.CollectTasks(worktrees, prefixes)
133+
eligible := operations.FilterEligible(worktrees, prefixes, sc.cfg.DefaultSource, allTasks, includeInherited)
134134

135135
var res syncResult
136136
var mu sync.Mutex
@@ -160,65 +160,30 @@ func syncAll(sc syncContext, worktrees []resolver.WorktreeInfo, prefixes []strin
160160
return nil
161161
}
162162

163-
func collectTasks(worktrees []resolver.WorktreeInfo, prefixes []string) []string {
164-
tasks := make([]string, 0, len(worktrees))
165-
for _, wt := range worktrees {
166-
task, _ := resolver.TaskFromBranch(wt.Branch, prefixes)
167-
tasks = append(tasks, task)
168-
}
169-
return tasks
170-
}
163+
func syncWorktree(cmd *cobra.Command, r git.Runner, mainBranch string, wt resolver.WorktreeInfo, useMerge bool, res *syncResult, mu *sync.Mutex) {
164+
sr := operations.SyncWorktree(r, mainBranch, wt, useMerge)
171165

172-
func filterEligible(worktrees []resolver.WorktreeInfo, prefixes []string, mainBranch string, allTasks []string, includeInherited bool) []resolver.WorktreeInfo {
173-
var eligible []resolver.WorktreeInfo
174-
for _, wt := range worktrees {
175-
if wt.Branch == mainBranch || wt.Branch == "" {
176-
continue
177-
}
178-
task, _ := resolver.TaskFromBranch(wt.Branch, prefixes)
179-
if !includeInherited && resolver.IsInherited(task, allTasks) {
180-
continue
181-
}
182-
eligible = append(eligible, wt)
183-
}
184-
return eligible
185-
}
166+
mu.Lock()
167+
defer mu.Unlock()
186168

187-
func syncWorktree(cmd *cobra.Command, r git.Runner, mainBranch string, wt resolver.WorktreeInfo, useMerge bool, res *syncResult, mu *sync.Mutex) {
188-
dirty, err := git.IsDirty(r, wt.Path)
189-
if err != nil {
190-
mu.Lock()
191-
fmt.Fprintf(cmd.OutOrStdout(), "Warning: could not check status of %s: %v\n", wt.Branch, err)
169+
switch {
170+
case sr.Skipped:
192171
res.skippedDirty++
193-
mu.Unlock()
194-
return
195-
}
196-
if dirty {
197-
mu.Lock()
198-
fmt.Fprintf(cmd.OutOrStdout(), "Skipping %s (dirty)\n", wt.Branch)
199-
res.skippedDirty++
200-
mu.Unlock()
201-
return
202-
}
203-
204-
if err := doSync(r, wt.Path, mainBranch, useMerge); err != nil {
205-
mu.Lock()
206-
res.failed++
207-
verb := verbRebase
208-
if useMerge {
209-
verb = flagSyncMerge
172+
if sr.SkipReason == "dirty" {
173+
fmt.Fprintf(cmd.OutOrStdout(), "Skipping %s (dirty)\n", sr.Branch)
174+
} else {
175+
fmt.Fprintf(cmd.OutOrStdout(), "Warning: %s: %s\n", sr.Branch, sr.SkipReason)
210176
}
211-
res.failures = append(res.failures, fmt.Sprintf(" %s: To resolve: cd %s && git %s %s", wt.Branch, wt.Path, verb, mainBranch))
212-
mu.Unlock()
213-
return
177+
case sr.Failed:
178+
res.failed++
179+
res.failures = append(res.failures, fmt.Sprintf(" %s: To resolve: %s", sr.Branch, sr.FailureHint))
180+
default:
181+
res.synced++
214182
}
215-
mu.Lock()
216-
res.synced++
217-
mu.Unlock()
218183
}
219184

220185
func printSyncSummary(cmd *cobra.Command, mainBranch string, useMerge bool, res *syncResult) {
221-
fmt.Fprintf(cmd.OutOrStdout(), "%s %d worktree(s) onto %s", syncMethodLabel(useMerge), res.synced, mainBranch)
186+
fmt.Fprintf(cmd.OutOrStdout(), "%s %d worktree(s) onto %s", operations.SyncMethodLabel(useMerge), res.synced, mainBranch)
222187
if res.skippedDirty > 0 {
223188
fmt.Fprintf(cmd.OutOrStdout(), ", %d skipped (dirty)", res.skippedDirty)
224189
}
@@ -231,22 +196,3 @@ func printSyncSummary(cmd *cobra.Command, mainBranch string, useMerge bool, res
231196
fmt.Fprintln(cmd.OutOrStdout(), f)
232197
}
233198
}
234-
235-
func syncMethodLabel(useMerge bool) string {
236-
if useMerge {
237-
return "Merged"
238-
}
239-
return "Rebased"
240-
}
241-
242-
func doSync(r git.Runner, dir, mainBranch string, useMerge bool) error {
243-
if useMerge {
244-
return git.Merge(r, dir, mainBranch, false)
245-
}
246-
if err := git.Rebase(r, dir, mainBranch); err != nil {
247-
// Abort the failed rebase to leave worktree in a clean state
248-
_ = git.AbortRebase(r, dir)
249-
return err
250-
}
251-
return nil
252-
}

0 commit comments

Comments
 (0)