feat: Implement bare repo architecture for branch visibility (gt-gmqe)
- Add .repo.git as shared bare repo for worktrees - Update polecat manager to use bare repo when available - Add git.NewGitWithDir() and CloneBare() for bare repo support - Update gt rig init to create bare repo architecture for new rigs - Refinery and polecats now share branch visibility via shared .git 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -217,10 +217,11 @@ func runRigAdd(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("\nStructure:\n")
|
||||
fmt.Printf(" %s/\n", name)
|
||||
fmt.Printf(" ├── config.json\n")
|
||||
fmt.Printf(" ├── .repo.git/ (shared bare repo)\n")
|
||||
fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix)
|
||||
fmt.Printf(" ├── plugins/ (rig-level plugins)\n")
|
||||
fmt.Printf(" ├── refinery/rig/ (canonical main)\n")
|
||||
fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n")
|
||||
fmt.Printf(" ├── mayor/rig/ (worktree: main)\n")
|
||||
fmt.Printf(" ├── refinery/rig/ (worktree: refinery)\n")
|
||||
fmt.Printf(" ├── crew/%s/ (your workspace)\n", crewName)
|
||||
fmt.Printf(" ├── witness/\n")
|
||||
fmt.Printf(" └── polecats/\n")
|
||||
|
||||
@@ -20,6 +20,7 @@ var (
|
||||
// Git wraps git operations for a working directory.
|
||||
type Git struct {
|
||||
workDir string
|
||||
gitDir string // Optional: explicit git directory (for bare repos)
|
||||
}
|
||||
|
||||
// NewGit creates a new Git wrapper for the given directory.
|
||||
@@ -27,6 +28,13 @@ func NewGit(workDir string) *Git {
|
||||
return &Git{workDir: workDir}
|
||||
}
|
||||
|
||||
// NewGitWithDir creates a Git wrapper with an explicit git directory.
|
||||
// This is used for bare repos where gitDir points to the .git directory
|
||||
// and workDir may be empty or point to a worktree.
|
||||
func NewGitWithDir(gitDir, workDir string) *Git {
|
||||
return &Git{gitDir: gitDir, workDir: workDir}
|
||||
}
|
||||
|
||||
// WorkDir returns the working directory for this Git instance.
|
||||
func (g *Git) WorkDir() string {
|
||||
return g.workDir
|
||||
@@ -34,8 +42,15 @@ func (g *Git) WorkDir() string {
|
||||
|
||||
// run executes a git command and returns stdout.
|
||||
func (g *Git) run(args ...string) (string, error) {
|
||||
// If gitDir is set (bare repo), prepend --git-dir flag
|
||||
if g.gitDir != "" {
|
||||
args = append([]string{"--git-dir=" + g.gitDir}, args...)
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = g.workDir
|
||||
if g.workDir != "" {
|
||||
cmd.Dir = g.workDir
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
@@ -84,6 +99,18 @@ func (g *Git) Clone(url, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneBare clones a repository as a bare repo (no working directory).
|
||||
// This is used for the shared repo architecture where all worktrees share a single git database.
|
||||
func (g *Git) CloneBare(url, dest string) error {
|
||||
cmd := exec.Command("git", "clone", "--bare", url, dest)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return g.wrapError(err, stderr.String(), []string{"clone", "--bare", url})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checkout checks out the given ref.
|
||||
func (g *Git) Checkout(ref string) error {
|
||||
_, err := g.run("checkout", ref)
|
||||
|
||||
@@ -82,6 +82,25 @@ func (m *Manager) assigneeID(name string) string {
|
||||
return fmt.Sprintf("%s/%s", m.rig.Name, name)
|
||||
}
|
||||
|
||||
// repoBase returns the git directory and Git object to use for worktree operations.
|
||||
// Prefers the shared bare repo (.repo.git) if it exists, otherwise falls back to mayor/rig.
|
||||
// The bare repo architecture allows all worktrees (refinery, polecats) to share branch visibility.
|
||||
func (m *Manager) repoBase() (*git.Git, error) {
|
||||
// First check for shared bare repo (new architecture)
|
||||
bareRepoPath := filepath.Join(m.rig.Path, ".repo.git")
|
||||
if info, err := os.Stat(bareRepoPath); err == nil && info.IsDir() {
|
||||
// Bare repo exists - use it
|
||||
return git.NewGitWithDir(bareRepoPath, ""), nil
|
||||
}
|
||||
|
||||
// Fall back to mayor/rig (legacy architecture)
|
||||
mayorPath := filepath.Join(m.rig.Path, "mayor", "rig")
|
||||
if _, err := os.Stat(mayorPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("no repo base found (neither .repo.git nor mayor/rig exists)")
|
||||
}
|
||||
return git.NewGit(mayorPath), nil
|
||||
}
|
||||
|
||||
// polecatDir returns the directory for a polecat.
|
||||
func (m *Manager) polecatDir(name string) string {
|
||||
return filepath.Join(m.rig.Path, "polecats", name)
|
||||
@@ -93,8 +112,9 @@ func (m *Manager) exists(name string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Add creates a new polecat as a git worktree from the mayor's clone.
|
||||
// This is much faster than a full clone and shares objects with the mayor.
|
||||
// Add creates a new polecat as a git worktree from the repo base.
|
||||
// Uses the shared bare repo (.repo.git) if available, otherwise mayor/rig.
|
||||
// This is much faster than a full clone and shares objects with all worktrees.
|
||||
// Polecat state is derived from beads assignee field, not state.json.
|
||||
func (m *Manager) Add(name string) (*Polecat, error) {
|
||||
if m.exists(name) {
|
||||
@@ -110,17 +130,14 @@ func (m *Manager) Add(name string) (*Polecat, error) {
|
||||
return nil, fmt.Errorf("creating polecats dir: %w", err)
|
||||
}
|
||||
|
||||
// Use Mayor's clone as the base for worktrees (Mayor is canonical for the rig)
|
||||
mayorPath := filepath.Join(m.rig.Path, "mayor", "rig")
|
||||
mayorGit := git.NewGit(mayorPath)
|
||||
|
||||
// Verify Mayor's clone exists
|
||||
if _, err := os.Stat(mayorPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("mayor clone not found at %s (run 'gt rig add' to set up rig structure)", mayorPath)
|
||||
// Get the repo base (bare repo or mayor/rig)
|
||||
repoGit, err := m.repoBase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding repo base: %w", err)
|
||||
}
|
||||
|
||||
// Check if branch already exists (e.g., from previous polecat that wasn't cleaned up)
|
||||
branchExists, err := mayorGit.BranchExists(branchName)
|
||||
branchExists, err := repoGit.BranchExists(branchName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking branch existence: %w", err)
|
||||
}
|
||||
@@ -128,13 +145,13 @@ func (m *Manager) Add(name string) (*Polecat, error) {
|
||||
// Create worktree - reuse existing branch if it exists
|
||||
if branchExists {
|
||||
// Branch exists, create worktree using existing branch
|
||||
if err := mayorGit.WorktreeAddExisting(polecatPath, branchName); err != nil {
|
||||
if err := repoGit.WorktreeAddExisting(polecatPath, branchName); err != nil {
|
||||
return nil, fmt.Errorf("creating worktree with existing branch: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Create new branch with worktree
|
||||
// git worktree add -b polecat/<name> <path>
|
||||
if err := mayorGit.WorktreeAdd(polecatPath, branchName); err != nil {
|
||||
if err := repoGit.WorktreeAdd(polecatPath, branchName); err != nil {
|
||||
return nil, fmt.Errorf("creating worktree: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -197,12 +214,15 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Use Mayor's clone to remove the worktree properly
|
||||
mayorPath := filepath.Join(m.rig.Path, "mayor", "rig")
|
||||
mayorGit := git.NewGit(mayorPath)
|
||||
// Get repo base to remove the worktree properly
|
||||
repoGit, err := m.repoBase()
|
||||
if err != nil {
|
||||
// Fall back to direct removal if repo base not found
|
||||
return os.RemoveAll(polecatPath)
|
||||
}
|
||||
|
||||
// Try to remove as a worktree first (use force flag for worktree removal too)
|
||||
if err := mayorGit.WorktreeRemove(polecatPath, force); err != nil {
|
||||
if err := repoGit.WorktreeRemove(polecatPath, force); err != nil {
|
||||
// Fall back to direct removal if worktree removal fails
|
||||
// (e.g., if this is an old-style clone, not a worktree)
|
||||
if removeErr := os.RemoveAll(polecatPath); removeErr != nil {
|
||||
@@ -211,7 +231,7 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error {
|
||||
}
|
||||
|
||||
// Prune any stale worktree entries
|
||||
_ = mayorGit.WorktreePrune()
|
||||
_ = repoGit.WorktreePrune()
|
||||
|
||||
// Release name back to pool if it's a pooled name
|
||||
m.namePool.Release(name)
|
||||
@@ -257,10 +277,14 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) {
|
||||
|
||||
polecatPath := m.polecatDir(name)
|
||||
branchName := fmt.Sprintf("polecat/%s", name)
|
||||
mayorPath := filepath.Join(m.rig.Path, "mayor", "rig")
|
||||
mayorGit := git.NewGit(mayorPath)
|
||||
polecatGit := git.NewGit(polecatPath)
|
||||
|
||||
// Get the repo base (bare repo or mayor/rig)
|
||||
repoGit, err := m.repoBase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding repo base: %w", err)
|
||||
}
|
||||
|
||||
// Check for uncommitted work unless forced
|
||||
if !force {
|
||||
status, err := polecatGit.CheckUncommittedWork()
|
||||
@@ -270,7 +294,7 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) {
|
||||
}
|
||||
|
||||
// Remove the worktree (use force for git worktree removal)
|
||||
if err := mayorGit.WorktreeRemove(polecatPath, true); err != nil {
|
||||
if err := repoGit.WorktreeRemove(polecatPath, true); err != nil {
|
||||
// Fall back to direct removal
|
||||
if removeErr := os.RemoveAll(polecatPath); removeErr != nil {
|
||||
return nil, fmt.Errorf("removing polecat dir: %w", removeErr)
|
||||
@@ -278,14 +302,14 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) {
|
||||
}
|
||||
|
||||
// Prune stale worktree entries
|
||||
_ = mayorGit.WorktreePrune()
|
||||
_ = repoGit.WorktreePrune()
|
||||
|
||||
// Delete the old branch so worktree starts fresh from current HEAD
|
||||
// Ignore error - branch may not exist (first recreate) or may fail to delete
|
||||
_ = mayorGit.DeleteBranch(branchName, true)
|
||||
_ = repoGit.DeleteBranch(branchName, true)
|
||||
|
||||
// Check if branch still exists (deletion may have failed or branch was protected)
|
||||
branchExists, err := mayorGit.BranchExists(branchName)
|
||||
branchExists, err := repoGit.BranchExists(branchName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking branch existence: %w", err)
|
||||
}
|
||||
@@ -294,12 +318,12 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) {
|
||||
if branchExists {
|
||||
// Branch still exists, create worktree using existing branch
|
||||
// This happens if delete failed (e.g., protected branch)
|
||||
if err := mayorGit.WorktreeAddExisting(polecatPath, branchName); err != nil {
|
||||
if err := repoGit.WorktreeAddExisting(polecatPath, branchName); err != nil {
|
||||
return nil, fmt.Errorf("creating worktree with existing branch: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Branch was deleted, create fresh worktree with new branch from HEAD
|
||||
if err := mayorGit.WorktreeAdd(polecatPath, branchName); err != nil {
|
||||
if err := repoGit.WorktreeAdd(polecatPath, branchName); err != nil {
|
||||
return nil, fmt.Errorf("creating fresh worktree: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,28 +217,40 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
||||
return nil, fmt.Errorf("saving rig config: %w", err)
|
||||
}
|
||||
|
||||
// Clone repository for mayor (must be first - serves as base for worktrees)
|
||||
// Create shared bare repo as single source of truth for all worktrees.
|
||||
// This architecture allows all worktrees (mayor, refinery, polecats) to share
|
||||
// branch visibility without needing to push to remote.
|
||||
bareRepoPath := filepath.Join(rigPath, ".repo.git")
|
||||
if err := m.git.CloneBare(opts.GitURL, bareRepoPath); err != nil {
|
||||
return nil, fmt.Errorf("creating bare repo: %w", err)
|
||||
}
|
||||
bareGit := git.NewGitWithDir(bareRepoPath, "")
|
||||
|
||||
// Create mayor as worktree from bare repo on main
|
||||
mayorRigPath := filepath.Join(rigPath, "mayor", "rig")
|
||||
if err := os.MkdirAll(filepath.Dir(mayorRigPath), 0755); err != nil {
|
||||
return nil, fmt.Errorf("creating mayor dir: %w", err)
|
||||
}
|
||||
if err := m.git.Clone(opts.GitURL, mayorRigPath); err != nil {
|
||||
return nil, fmt.Errorf("cloning for mayor: %w", err)
|
||||
if err := bareGit.WorktreeAddExisting(mayorRigPath, "main"); err != nil {
|
||||
return nil, fmt.Errorf("creating mayor worktree: %w", err)
|
||||
}
|
||||
// Create mayor CLAUDE.md (overrides any from cloned repo)
|
||||
if err := m.createRoleCLAUDEmd(mayorRigPath, "mayor", opts.Name, ""); err != nil {
|
||||
return nil, fmt.Errorf("creating mayor CLAUDE.md: %w", err)
|
||||
}
|
||||
|
||||
// Create refinery as a worktree of mayor's clone.
|
||||
// This allows refinery to see polecat branches locally (shared .git).
|
||||
// Refinery uses the "refinery" branch which tracks main.
|
||||
// Create refinery as worktree from bare repo on main.
|
||||
// Refinery stays on main to merge polecat branches into main.
|
||||
// Uses the same main branch as mayor - they share the working copy.
|
||||
refineryRigPath := filepath.Join(rigPath, "refinery", "rig")
|
||||
if err := os.MkdirAll(filepath.Dir(refineryRigPath), 0755); err != nil {
|
||||
return nil, fmt.Errorf("creating refinery dir: %w", err)
|
||||
}
|
||||
mayorGit := git.NewGit(mayorRigPath)
|
||||
if err := mayorGit.WorktreeAdd(refineryRigPath, "refinery"); err != nil {
|
||||
// Create a refinery branch from main for the worktree
|
||||
if err := bareGit.CreateBranchFrom("refinery", "main"); err != nil {
|
||||
return nil, fmt.Errorf("creating refinery branch: %w", err)
|
||||
}
|
||||
if err := bareGit.WorktreeAddExisting(refineryRigPath, "refinery"); err != nil {
|
||||
return nil, fmt.Errorf("creating refinery worktree: %w", err)
|
||||
}
|
||||
// Create refinery CLAUDE.md (overrides any from cloned repo)
|
||||
|
||||
Reference in New Issue
Block a user