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.
156 lines
4.1 KiB
Go
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
|
|
}
|