From b030ee9a9a96026b018687d119b2c4f426961797 Mon Sep 17 00:00:00 2001 From: Andy Mroczkowski Date: Thu, 14 May 2026 06:55:13 -0400 Subject: [PATCH] persist repo metadata to workspace.toml - add WorkspaceMetadata with RepoConfig map - AddRepo writes repo entries to workspace.toml - refactor AddRepo to method on *Workspace - add NewWorkspace constructor for map init - fix load round-trip with pointer receiver --- cmd/addrepo.go | 2 +- internal/workspace/integration_test.go | 18 +++--- internal/workspace/types.go | 1 + internal/workspace/workspace.go | 87 ++++++++++++++++++++++---- internal/workspace/workspace_test.go | 11 +++- 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/cmd/addrepo.go b/cmd/addrepo.go index 6b2cb1d..55dc588 100644 --- a/cmd/addrepo.go +++ b/cmd/addrepo.go @@ -38,7 +38,7 @@ var addRepoCmd = &cobra.Command{ return err } - err = workspace.AddRepo(ws, repoURL, "", config) + err = ws.AddRepo(repoURL, "", config) if err != nil { return err } diff --git a/internal/workspace/integration_test.go b/internal/workspace/integration_test.go index aea5493..a1f6fde 100644 --- a/internal/workspace/integration_test.go +++ b/internal/workspace/integration_test.go @@ -57,7 +57,7 @@ func TestAddRepoIntegration(t *testing.T) { ws, config := createTestWorkspace(t, "test-ws") remoteRepo := createTestRepo(t) - err := AddRepo(ws, remoteRepo, "", config) + err := ws.AddRepo(remoteRepo, "", config) if err != nil { t.Fatalf("AddRepo: %v", err) } @@ -89,7 +89,7 @@ func TestAddRepoIntegration(t *testing.T) { t.Fatalf("cloneBareRepo: %v", err) } - err = AddRepo(ws, remoteRepo, "", config) + err = ws.AddRepo(remoteRepo, "", config) if err != nil { t.Fatalf("AddRepo: %v", err) } @@ -105,12 +105,12 @@ func TestAddRepoIntegration(t *testing.T) { remoteRepo := createTestRepo(t) // create a branch matching our ws branch - err := exec.Command("git", "-C", remoteRepo, "branch", DefaultBranchName(ws, config)).Run() + err := exec.Command("git", "-C", remoteRepo, "branch", defaultBranchName(ws, config)).Run() if err != nil { t.Fatalf("git remote branch creation failed: %v", err) } - err = AddRepo(ws, remoteRepo, "", config) + err = ws.AddRepo(remoteRepo, "", config) if err != nil { t.Fatalf("AddRepo: %v", err) } @@ -131,12 +131,12 @@ func TestAddRepoIntegration(t *testing.T) { localBare, err := cloneBareRepo(remoteRepo, config) // create a branch matching our ws branch - err = exec.Command("git", "-C", localBare, "branch", DefaultBranchName(ws, config)).Run() + err = exec.Command("git", "-C", localBare, "branch", defaultBranchName(ws, config)).Run() if err != nil { t.Fatalf("git local branch creation failed: %v", err) } - err = AddRepo(ws, remoteRepo, "", config) + err = ws.AddRepo(remoteRepo, "", config) if err != nil { t.Fatalf("AddRepo: %v", err) } @@ -151,7 +151,7 @@ func TestAddRepoIntegration(t *testing.T) { remoteRepo := t.TempDir() - err := AddRepo(ws, remoteRepo, "", config) + err := ws.AddRepo(remoteRepo, "", config) if err == nil { t.Fatalf("AddRepo: expected error, got ") } @@ -168,12 +168,12 @@ func TestAddRepoIntegration(t *testing.T) { // create a worktree with branch matching our ws branch worktreeDest := t.TempDir() - err = exec.Command("git", "-C", localBare, "worktree", "add", worktreeDest, "-B", DefaultBranchName(ws, config)).Run() + err = exec.Command("git", "-C", localBare, "worktree", "add", worktreeDest, "-B", defaultBranchName(ws, config)).Run() if err != nil { t.Fatalf("git worktree creation failed: %v", err) } - err = AddRepo(ws, remoteRepo, "", config) + err = ws.AddRepo(remoteRepo, "", config) if err == nil { t.Fatalf("AddRepo: expected error, got ") } diff --git a/internal/workspace/types.go b/internal/workspace/types.go index faba454..4c44312 100644 --- a/internal/workspace/types.go +++ b/internal/workspace/types.go @@ -88,6 +88,7 @@ func LoadOrDefaultConfig(path string) (Config, error) { type Workspace struct { Path string + Metadata WorkspaceMetadata } func (ws Workspace) Name() string { diff --git a/internal/workspace/workspace.go b/internal/workspace/workspace.go index 1be29f9..f8fceea 100644 --- a/internal/workspace/workspace.go +++ b/internal/workspace/workspace.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/pelletier/go-toml/v2" ) //go:embed templates/CLAUDE.md @@ -13,6 +15,18 @@ var claudeMdTemplate string //go:embed templates/WORKSPACE.md var workspaceMdTemplate string +type RepoConfig struct { + URL string `toml:"url"` +} + +type WorkspaceMetadata struct { + + Config struct { + WorktreeSubdir string `toml:"worktree_subdir"` + } `toml:"config"` + Repos map[string]RepoConfig `toml:"repos"` +} + func FindWorkspaceDir(startDir string) (string, error) { dir := startDir @@ -31,6 +45,42 @@ func FindWorkspaceDir(startDir string) (string, error) { } } +func (w *Workspace) writeMetadata() error { + bytes, err := toml.Marshal(w.Metadata) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(w.Path, "workspace.toml"), bytes, 0666) + if err != nil { + return err + } + return nil +} + +func (w *Workspace) loadMetadata() error { + + bytes, err := os.ReadFile(filepath.Join(w.Path, "workspace.toml")) + if err != nil { + return err + } + + err = toml.Unmarshal(bytes, &w.Metadata) + if err != nil { + return err + } + return nil +} + +func NewWorkspace(path string) Workspace { + return Workspace{ + Path: path, + Metadata: WorkspaceMetadata{ + Repos: make(map[string]RepoConfig), + }, + } +} + func CreateWorkspace(name string, config Config) (Workspace, error) { var err error @@ -56,23 +106,25 @@ func CreateWorkspace(name string, config Config) (Workspace, error) { return Workspace{}, err } - err = os.WriteFile(filepath.Join(wsPath, "workspace.toml"), []byte{}, 0666) + err = os.WriteFile(filepath.Join(wsPath, "CLAUDE.md"), []byte(claudeMdTemplate), 0666) if err != nil { return Workspace{}, err } - err = os.WriteFile(filepath.Join(wsPath, "CLAUDE.md"), []byte(claudeMdTemplate), 0666) + err = os.WriteFile(filepath.Join(wsPath, "WORKSPACE.md"), []byte(workspaceMdTemplate), 0666) if err != nil { return Workspace{}, err } - err = os.WriteFile(filepath.Join(wsPath, "WORKSPACE.md"), []byte(workspaceMdTemplate), 0666) + ws := NewWorkspace(wsPath) + ws.Metadata.Config.WorktreeSubdir = config.WorktreeSubdir + err = ws.writeMetadata() if err != nil { return Workspace{}, err } success = true - return Workspace{Path: wsPath}, nil + return ws, nil } func LoadWorkspace(wsPath string) (Workspace, error) { @@ -82,13 +134,13 @@ func LoadWorkspace(wsPath string) (Workspace, error) { return Workspace{}, fmt.Errorf("workspace path %q not found: %w", wsPath, err) } - wsTomlPath := filepath.Join(wsPath, "workspace.toml") - _, err = os.Stat(wsTomlPath) + ws := NewWorkspace(wsPath) + err = ws.loadMetadata() if err != nil { - return Workspace{}, fmt.Errorf("%q is not a workspace (missing workspace.toml): %w", wsPath, err) + return Workspace{}, err } - return Workspace{Path: wsPath}, nil + return ws, nil } func ListWorkspaces(config Config) ([]Workspace, error) { @@ -117,11 +169,11 @@ func ListWorkspaces(config Config) ([]Workspace, error) { return workspaces, nil } -func DefaultBranchName(workspace Workspace, config Config) string { +func defaultBranchName(workspace Workspace, config Config) string { return config.BranchPrefix + workspace.Name() } -func AddRepo(ws Workspace, repoURL string, branch string, config Config) error { +func (ws *Workspace) AddRepo(repoURL string, branch string, config Config) error { repo, err := parseRepoURL(repoURL) if err != nil { @@ -134,18 +186,29 @@ func AddRepo(ws Workspace, repoURL string, branch string, config Config) error { } if branch == "" { - branch = DefaultBranchName(ws, config) + branch = defaultBranchName(*ws, config) } // TODO: handle conflicting worktree paths workTreeDest := filepath.Join(ws.Path, config.WorktreeSubdir, repo.Name) + success := false + defer func() { + if !success { + _ = os.RemoveAll(workTreeDest) + } + }() err = addWorktree(bareRepoPath, workTreeDest, branch) if err != nil { return err } - // TODO: add to workspace.toml + ws.Metadata.Repos[repo.Name] = RepoConfig{URL: repo.Url} + err = ws.writeMetadata() + if err != nil { + return err + } + success = true return nil } \ No newline at end of file diff --git a/internal/workspace/workspace_test.go b/internal/workspace/workspace_test.go index 752d8f3..e735cbb 100644 --- a/internal/workspace/workspace_test.go +++ b/internal/workspace/workspace_test.go @@ -102,7 +102,7 @@ func TestCreateWorkspace(t *testing.T) { t.Errorf("expected os.ErrExists, got: %v:", err) } - if ws != (Workspace{}) { + if ws.Path != "" { t.Errorf("expected empty Workspace struct got: %v:", ws) } }) @@ -161,12 +161,16 @@ func TestLoadWorkspace(t *testing.T) { tmpDir := t.TempDir() parentDir := filepath.Join(tmpDir, "Workspaces") - config := Config{WorkspacesDir: parentDir} + config := DefaultConfig() + config.WorkspacesDir = parentDir ws, err := CreateWorkspace("ws1", config) if err != nil { t.Fatalf("CreateWorkspace expected no error, got %v", err) } + if ws.Metadata.Config.WorktreeSubdir != config.WorktreeSubdir { + t.Errorf("WorktreeSubdir expected %v, got %v", config.WorktreeSubdir, ws.Metadata.Config.WorktreeSubdir) + } ws2, err := LoadWorkspace(ws.Path) if err != nil { @@ -175,6 +179,9 @@ func TestLoadWorkspace(t *testing.T) { if ws.Path != ws2.Path { t.Errorf("expected workspace paths to be equal, ws.path: %v ws2.path: %v", ws.Path, ws2.Path) } + if ws.Metadata.Config.WorktreeSubdir != ws2.Metadata.Config.WorktreeSubdir { + t.Errorf("expected worktreeSubdir config to be equal, ws..WorktreeSubdir: %v ws2..WorktreeSubdir: %v", ws.Metadata.Config.WorktreeSubdir, ws2.Metadata.Config.WorktreeSubdir) + } }) }