Add EnsureGitignorePatterns to rig package that ensures .gitignore has required Gas Town patterns (.runtime/, .claude/, .beads/, .logs/). Called from crew and polecat managers when creating new workers. This prevents runtime-gitignore warnings from gt doctor. The function: - Creates .gitignore if it doesn't exist - Appends missing patterns to existing files - Recognizes pattern variants (.runtime vs .runtime/) - Adds "# Gas Town" header when appending Includes comprehensive tests for all scenarios.
157 lines
4.1 KiB
Go
157 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/",
|
|
".beads/",
|
|
".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
|
|
}
|