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:
Steve Yegge
2025-12-25 18:49:34 -08:00
parent 200b2065a6
commit 1b66b9a2f2
4 changed files with 100 additions and 36 deletions

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)