Skip to content

Commit 7e18e64

Browse files
refactor: Complete Phase 4 code consolidation
- Add ProjectContext helper to consolidate duplicate setup logic - Update work, list, remove, prune commands to use ProjectContext - Consolidate Preset/Scaffold manager creation with lazy initialization - Remove unused git functions: BareRepo, CreateBareRepo, CreateGitFile, InitFromWorktree, copyDir, copyFile - Remove empty PersistentPreRunE from root command - Add tests for ProjectContext
1 parent 8a254d3 commit 7e18e64

9 files changed

Lines changed: 334 additions & 261 deletions

File tree

internal/cli/context.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"sync"
8+
9+
"github.com/michaeldyrynda/arbor/internal/config"
10+
arborerrors "github.com/michaeldyrynda/arbor/internal/errors"
11+
"github.com/michaeldyrynda/arbor/internal/git"
12+
"github.com/michaeldyrynda/arbor/internal/presets"
13+
"github.com/michaeldyrynda/arbor/internal/scaffold"
14+
)
15+
16+
type ProjectContext struct {
17+
CWD string
18+
BarePath string
19+
ProjectPath string
20+
Config *config.Config
21+
DefaultBranch string
22+
23+
presetManager *presets.Manager
24+
scaffoldManager *scaffold.ScaffoldManager
25+
managersInit sync.Once
26+
managerErr error
27+
}
28+
29+
func OpenProjectFromCWD() (*ProjectContext, error) {
30+
cwd, err := os.Getwd()
31+
if err != nil {
32+
return nil, fmt.Errorf("getting current directory: %w", err)
33+
}
34+
35+
barePath, err := git.FindBarePath(cwd)
36+
if err != nil {
37+
return nil, fmt.Errorf("finding bare repository: %w", err)
38+
}
39+
40+
projectPath := filepath.Dir(barePath)
41+
cfg, err := config.LoadProject(projectPath)
42+
if err != nil {
43+
return nil, fmt.Errorf("loading project config: %w", err)
44+
}
45+
46+
defaultBranch := cfg.DefaultBranch
47+
if defaultBranch == "" {
48+
defaultBranch, _ = git.GetDefaultBranch(barePath)
49+
if defaultBranch == "" {
50+
defaultBranch = config.DefaultBranch
51+
}
52+
}
53+
54+
return &ProjectContext{
55+
CWD: cwd,
56+
BarePath: barePath,
57+
ProjectPath: projectPath,
58+
Config: cfg,
59+
DefaultBranch: defaultBranch,
60+
}, nil
61+
}
62+
63+
func (pc *ProjectContext) IsInWorktree() bool {
64+
_, err := git.FindBarePath(pc.CWD)
65+
return err == nil
66+
}
67+
68+
func (pc *ProjectContext) MustBeInWorktree() error {
69+
if !pc.IsInWorktree() {
70+
return arborerrors.ErrWorktreeNotFound
71+
}
72+
return nil
73+
}
74+
75+
func (pc *ProjectContext) PresetManager() *presets.Manager {
76+
pc.managersInit.Do(func() {
77+
pc.presetManager = presets.NewManager()
78+
pc.scaffoldManager = scaffold.NewScaffoldManager()
79+
presets.RegisterAllWithScaffold(pc.scaffoldManager)
80+
})
81+
return pc.presetManager
82+
}
83+
84+
func (pc *ProjectContext) ScaffoldManager() *scaffold.ScaffoldManager {
85+
pc.managersInit.Do(func() {
86+
pc.presetManager = presets.NewManager()
87+
pc.scaffoldManager = scaffold.NewScaffoldManager()
88+
presets.RegisterAllWithScaffold(pc.scaffoldManager)
89+
})
90+
return pc.scaffoldManager
91+
}

internal/cli/context_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func evalSymlinks(path string) string {
11+
evalPath, _ := filepath.EvalSymlinks(path)
12+
if evalPath == "" {
13+
return path
14+
}
15+
return evalPath
16+
}
17+
18+
func createTestWorktree(t *testing.T) (string, string) {
19+
tmpDir := t.TempDir()
20+
repoDir := filepath.Join(tmpDir, "repo")
21+
barePath := filepath.Join(tmpDir, ".bare")
22+
23+
if err := os.MkdirAll(repoDir, 0755); err != nil {
24+
t.Fatalf("creating repo dir: %v", err)
25+
}
26+
27+
cmd := exec.Command("git", "init", "-b", "main")
28+
cmd.Dir = repoDir
29+
if err := cmd.Run(); err != nil {
30+
t.Fatalf("initializing git repo: %v", err)
31+
}
32+
33+
cmd = exec.Command("git", "config", "user.email", "test@example.com")
34+
cmd.Dir = repoDir
35+
if err := cmd.Run(); err != nil {
36+
t.Fatalf("setting git user.email: %v", err)
37+
}
38+
39+
cmd = exec.Command("git", "config", "user.name", "Test User")
40+
cmd.Dir = repoDir
41+
if err := cmd.Run(); err != nil {
42+
t.Fatalf("setting git user.name: %v", err)
43+
}
44+
45+
readmePath := filepath.Join(repoDir, "README.md")
46+
if err := os.WriteFile(readmePath, []byte("test"), 0644); err != nil {
47+
t.Fatalf("writing README: %v", err)
48+
}
49+
50+
cmd = exec.Command("git", "add", ".")
51+
cmd.Dir = repoDir
52+
if err := cmd.Run(); err != nil {
53+
t.Fatalf("staging files: %v", err)
54+
}
55+
56+
cmd = exec.Command("git", "commit", "-m", "Initial commit")
57+
cmd.Dir = repoDir
58+
if err := cmd.Run(); err != nil {
59+
t.Fatalf("committing: %v", err)
60+
}
61+
62+
cmd = exec.Command("git", "clone", "--bare", repoDir, barePath)
63+
if err := cmd.Run(); err != nil {
64+
t.Fatalf("cloning to bare: %v", err)
65+
}
66+
67+
worktreePath := filepath.Join(tmpDir, "worktree1")
68+
cmd = exec.Command("git", "worktree", "add", worktreePath, "main")
69+
cmd.Dir = barePath
70+
if err := cmd.Run(); err != nil {
71+
t.Fatalf("creating worktree: %v", err)
72+
}
73+
74+
configPath := filepath.Join(tmpDir, "arbor.yaml")
75+
if err := os.WriteFile(configPath, []byte("preset: php\n"), 0644); err != nil {
76+
t.Fatalf("writing arbor.yaml: %v", err)
77+
}
78+
79+
return worktreePath, barePath
80+
}
81+
82+
func TestOpenProjectFromCWD_NotInWorktree(t *testing.T) {
83+
tmpDir := t.TempDir()
84+
85+
_, err := OpenProjectFromCWD()
86+
if err == nil {
87+
t.Error("expected error when not in worktree, got nil")
88+
}
89+
_ = tmpDir
90+
}
91+
92+
func TestOpenProjectFromCWD_Success(t *testing.T) {
93+
worktreePath, barePath := createTestWorktree(t)
94+
tmpDir := filepath.Dir(barePath)
95+
96+
originalCWD, err := os.Getwd()
97+
if err != nil {
98+
t.Fatalf("failed to get current directory: %v", err)
99+
}
100+
defer os.Chdir(originalCWD)
101+
102+
err = os.Chdir(worktreePath)
103+
if err != nil {
104+
t.Fatalf("failed to change directory: %v", err)
105+
}
106+
107+
pc, err := OpenProjectFromCWD()
108+
if err != nil {
109+
t.Fatalf("OpenProjectFromCWD() error = %v", err)
110+
}
111+
112+
expectedCWD := evalSymlinks(worktreePath)
113+
if evalSymlinks(pc.CWD) != expectedCWD {
114+
t.Errorf("CWD = %v, want %v", pc.CWD, expectedCWD)
115+
}
116+
117+
expectedBarePath := evalSymlinks(barePath)
118+
if evalSymlinks(pc.BarePath) != expectedBarePath {
119+
t.Errorf("BarePath = %v, want %v", pc.BarePath, expectedBarePath)
120+
}
121+
122+
expectedProjectPath := evalSymlinks(tmpDir)
123+
if evalSymlinks(pc.ProjectPath) != expectedProjectPath {
124+
t.Errorf("ProjectPath = %v, want %v", pc.ProjectPath, expectedProjectPath)
125+
}
126+
127+
if pc.DefaultBranch != "main" {
128+
t.Errorf("DefaultBranch = %v, want %v", pc.DefaultBranch, "main")
129+
}
130+
}
131+
132+
func TestProjectContext_IsInWorktree(t *testing.T) {
133+
tmpDir := t.TempDir()
134+
135+
pc := &ProjectContext{
136+
CWD: tmpDir,
137+
}
138+
139+
if pc.IsInWorktree() {
140+
t.Error("IsInWorktree() = true, want false for non-worktree directory")
141+
}
142+
143+
worktreePath, _ := createTestWorktree(t)
144+
145+
pc.CWD = worktreePath
146+
if !pc.IsInWorktree() {
147+
t.Error("IsInWorktree() = false, want true for worktree directory")
148+
}
149+
}
150+
151+
func TestProjectContext_MustBeInWorktree(t *testing.T) {
152+
tmpDir := t.TempDir()
153+
154+
pc := &ProjectContext{
155+
CWD: tmpDir,
156+
}
157+
158+
err := pc.MustBeInWorktree()
159+
if err == nil {
160+
t.Error("MustBeInWorktree() = nil, want error for non-worktree directory")
161+
}
162+
163+
worktreePath, _ := createTestWorktree(t)
164+
165+
pc.CWD = worktreePath
166+
err = pc.MustBeInWorktree()
167+
if err != nil {
168+
t.Errorf("MustBeInWorktree() = %v, want nil for worktree directory", err)
169+
}
170+
}
171+
172+
func TestProjectContext_Managers(t *testing.T) {
173+
worktreePath, _ := createTestWorktree(t)
174+
175+
originalCWD, err := os.Getwd()
176+
if err != nil {
177+
t.Fatalf("failed to get current directory: %v", err)
178+
}
179+
defer os.Chdir(originalCWD)
180+
181+
err = os.Chdir(worktreePath)
182+
if err != nil {
183+
t.Fatalf("failed to change directory: %v", err)
184+
}
185+
186+
pc, err := OpenProjectFromCWD()
187+
if err != nil {
188+
t.Fatalf("OpenProjectFromCWD() error = %v", err)
189+
}
190+
191+
pm := pc.PresetManager()
192+
if pm == nil {
193+
t.Error("PresetManager() returned nil")
194+
}
195+
196+
sm := pc.ScaffoldManager()
197+
if sm == nil {
198+
t.Error("ScaffoldManager() returned nil")
199+
}
200+
201+
pm2 := pc.PresetManager()
202+
if pm2 != pm {
203+
t.Error("PresetManager() called twice returned different instances")
204+
}
205+
206+
sm2 := pc.ScaffoldManager()
207+
if sm2 != sm {
208+
t.Error("ScaffoldManager() called twice returned different instances")
209+
}
210+
}

internal/cli/init.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/michaeldyrynda/arbor/internal/config"
88
"github.com/michaeldyrynda/arbor/internal/git"
99
"github.com/michaeldyrynda/arbor/internal/presets"
10+
"github.com/michaeldyrynda/arbor/internal/scaffold"
1011
"github.com/michaeldyrynda/arbor/internal/utils"
1112
"github.com/spf13/cobra"
1213
)
@@ -72,6 +73,10 @@ Arguments:
7273
preset := mustGetString(cmd, "preset")
7374
interactive := mustGetBool(cmd, "interactive")
7475

76+
presetManager := presets.NewManager()
77+
scaffoldManager := scaffold.NewScaffoldManager()
78+
presets.RegisterAllWithScaffold(scaffoldManager)
79+
7580
if preset != "" {
7681
cfg.Preset = preset
7782
} else if interactive {
@@ -115,8 +120,6 @@ Arguments:
115120
func init() {
116121
rootCmd.AddCommand(initCmd)
117122

118-
presets.RegisterAllWithScaffold(scaffoldManager)
119-
120123
initCmd.Flags().String("preset", "", "Project preset (laravel, php)")
121124
initCmd.Flags().Bool("interactive", false, "Interactive preset selection")
122125
}

internal/cli/list.go

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"path/filepath"
99
"strings"
1010

11-
"github.com/michaeldyrynda/arbor/internal/config"
1211
"github.com/michaeldyrynda/arbor/internal/git"
1312
"github.com/spf13/cobra"
1413
)
@@ -21,36 +20,17 @@ var listCmd = &cobra.Command{
2120
Shows worktrees with merge status, current worktree indicator,
2221
and main branch highlighting.`,
2322
RunE: func(cmd *cobra.Command, args []string) error {
24-
cwd, err := os.Getwd()
23+
pc, err := OpenProjectFromCWD()
2524
if err != nil {
26-
return fmt.Errorf("getting current directory: %w", err)
27-
}
28-
29-
barePath, err := git.FindBarePath(cwd)
30-
if err != nil {
31-
return fmt.Errorf("finding bare repository: %w", err)
32-
}
33-
34-
projectPath := filepath.Dir(barePath)
35-
cfg, err := config.LoadProject(projectPath)
36-
if err != nil {
37-
return fmt.Errorf("loading project config: %w", err)
38-
}
39-
40-
defaultBranch := cfg.DefaultBranch
41-
if defaultBranch == "" {
42-
defaultBranch, _ = git.GetDefaultBranch(barePath)
43-
if defaultBranch == "" {
44-
defaultBranch = config.DefaultBranch
45-
}
25+
return err
4626
}
4727

4828
jsonOutput := mustGetBool(cmd, "json")
4929
porcelain := mustGetBool(cmd, "porcelain")
5030
sortBy := mustGetString(cmd, "sort-by")
5131
reverse := mustGetBool(cmd, "reverse")
5232

53-
worktrees, err := git.ListWorktreesDetailed(barePath, cwd, defaultBranch)
33+
worktrees, err := git.ListWorktreesDetailed(pc.BarePath, pc.CWD, pc.DefaultBranch)
5434
if err != nil {
5535
return fmt.Errorf("listing worktrees: %w", err)
5636
}

0 commit comments

Comments
 (0)