Files
gastown/internal/rig/overlay.go
Christian Sieber 5bb74b19ed Revert erroneous addition of .beads to agent .gitignore template (#891)
Gas town agents need to ignore working-file directories like .logs,
.runtime, and .claude, but Beads provides its own .gitignore handling
in `bd init`, creating a .beads/.gitignore file which ignores the
relevant Beads working files while allowing .beads/issues.jsonl
to be tracked correctly. PR #753 broke this, causing new polecats
to attempt to merge their changed .gitignore into the project repo,
ultimately breaking bd sync and causing issues to become untracked.
2026-01-24 21:47:09 -08:00

156 lines
4.1 KiB
Go

package rig
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// CopyOverlay copies files from <rigPath>/.runtime/overlay/ to the destination path.
// This allows storing gitignored files (like .env) that services need at their root.
// The overlay is copied non-recursively - only files, not subdirectories.
// File permissions from the source are preserved.
//
// Structure:
//
// rig/
// .runtime/
// overlay/
// .env <- Copied to destPath
// config.json <- Copied to destPath
//
// Returns nil if the overlay directory doesn't exist (nothing to copy).
// Individual file copy failures are logged as warnings but don't stop the process.
func CopyOverlay(rigPath, destPath string) error {
overlayDir := filepath.Join(rigPath, ".runtime", "overlay")
// Check if overlay directory exists
entries, err := os.ReadDir(overlayDir)
if err != nil {
if os.IsNotExist(err) {
// No overlay directory - not an error, just nothing to copy
return nil
}
return fmt.Errorf("reading overlay dir: %w", err)
}
// Copy each file (not directories) from overlay to destination
for _, entry := range entries {
if entry.IsDir() {
// Skip subdirectories - only copy files at overlay root
continue
}
srcPath := filepath.Join(overlayDir, entry.Name())
dstPath := filepath.Join(destPath, entry.Name())
if err := copyFilePreserveMode(srcPath, dstPath); err != nil {
// Log warning but continue - don't fail spawn for overlay issues
fmt.Printf("Warning: could not copy overlay file %s: %v\n", entry.Name(), err)
continue
}
}
return nil
}
// EnsureGitignorePatterns ensures the .gitignore has required Gas Town patterns.
// This is called after cloning to add patterns that may be missing from the source repo.
func EnsureGitignorePatterns(worktreePath string) error {
gitignorePath := filepath.Join(worktreePath, ".gitignore")
// Required patterns for Gas Town worktrees
requiredPatterns := []string{
".runtime/",
".claude/",
".logs/",
}
// Read existing gitignore content
var existingContent string
if data, err := os.ReadFile(gitignorePath); err == nil {
existingContent = string(data)
}
// Find missing patterns
var missing []string
for _, pattern := range requiredPatterns {
// Check various forms: .runtime, .runtime/, /.runtime, etc.
found := false
for _, line := range strings.Split(existingContent, "\n") {
line = strings.TrimSpace(line)
if line == pattern || line == strings.TrimSuffix(pattern, "/") ||
line == "/"+pattern || line == "/"+strings.TrimSuffix(pattern, "/") {
found = true
break
}
}
if !found {
missing = append(missing, pattern)
}
}
if len(missing) == 0 {
return nil // All patterns present
}
// Append missing patterns
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("opening .gitignore: %w", err)
}
defer f.Close()
// Add header if appending to existing file
if existingContent != "" && !strings.HasSuffix(existingContent, "\n") {
if _, err := f.WriteString("\n"); err != nil {
return err
}
}
if existingContent != "" {
if _, err := f.WriteString("\n# Gas Town (added by gt)\n"); err != nil {
return err
}
}
for _, pattern := range missing {
if _, err := f.WriteString(pattern + "\n"); err != nil {
return err
}
}
return nil
}
// copyFilePreserveMode copies a file from src to dst, preserving the source file's permissions.
func copyFilePreserveMode(src, dst string) error {
// Get source file info for permissions
srcInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("stat source: %w", err)
}
// Open source file
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer srcFile.Close()
// Create destination file with same permissions
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode().Perm())
if err != nil {
return fmt.Errorf("create destination: %w", err)
}
defer dstFile.Close()
// Copy contents
if _, err := io.Copy(dstFile, srcFile); err != nil {
return fmt.Errorf("copy contents: %w", err)
}
return nil
}