diff --git a/cmd/init.go b/cmd/init.go index 55e2d83..c514da7 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/robmorgan/metamorph/internal/constants" "github.com/spf13/cobra" @@ -116,13 +117,37 @@ Add project-specific instructions for your agents here. fmt.Printf(" Created %s/\n", d) } - // Write .gitignore. + // Append entries to .gitignore (create if missing, never overwrite). gitignorePath := filepath.Join(absDir, ".gitignore") - gitignoreContent := ".metamorph/\nagent_logs/\n" - if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0644); err != nil { - return fmt.Errorf("failed to write .gitignore: %w", err) + requiredEntries := []string{".metamorph/", "agent_logs/"} + existing, _ := os.ReadFile(gitignorePath) + existingStr := string(existing) + var toAdd []string + for _, entry := range requiredEntries { + if !strings.Contains(existingStr, entry) { + toAdd = append(toAdd, entry) + } + } + if len(toAdd) > 0 { + f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open .gitignore: %w", err) + } + // Add a newline before appending if the file doesn't end with one. + if len(existingStr) > 0 && !strings.HasSuffix(existingStr, "\n") { + _, _ = f.WriteString("\n") + } + for _, entry := range toAdd { + if _, err := f.WriteString(entry + "\n"); err != nil { + _ = f.Close() + return fmt.Errorf("failed to write .gitignore entry: %w", err) + } + } + _ = f.Close() + fmt.Println(" Updated .gitignore") + } else { + fmt.Println(" .gitignore already up to date") } - fmt.Println(" Created .gitignore") fmt.Printf("\nProject %q initialized successfully!\n\n", projectName) fmt.Println("Next steps:") diff --git a/internal/gitops/gitops.go b/internal/gitops/gitops.go index f3747e8..ee8114a 100644 --- a/internal/gitops/gitops.go +++ b/internal/gitops/gitops.go @@ -206,8 +206,8 @@ func SyncToProjectDir(upstreamPath, projectDir string) (string, error) { return "", fmt.Errorf("gitops: fetch failed: %w", err) } - // Merge FETCH_HEAD. - if _, err := git(projectDir, "merge", "FETCH_HEAD", "--no-edit"); err != nil { + // Merge FETCH_HEAD, auto-resolving conflicts in favor of upstream (agent work). + if _, err := git(projectDir, "merge", "-X", "theirs", "FETCH_HEAD", "--no-edit"); err != nil { if _, abortErr := git(projectDir, "merge", "--abort"); abortErr != nil { slog.Warn("gitops: failed to abort merge", "error", abortErr) } diff --git a/internal/gitops/gitops_test.go b/internal/gitops/gitops_test.go index 15d10f9..7e324c4 100644 --- a/internal/gitops/gitops_test.go +++ b/internal/gitops/gitops_test.go @@ -674,7 +674,7 @@ func TestSyncToProjectDir(t *testing.T) { } }) - t.Run("aborts merge on conflict", func(t *testing.T) { + t.Run("auto-resolves conflict in favor of upstream", func(t *testing.T) { projectDir, upstreamPath := setupUpstream(t) // Push a conflicting change via upstream (simulating an agent). @@ -713,19 +713,19 @@ func TestSyncToProjectDir(t *testing.T) { t.Fatal(err) } - // Sync should fail due to merge conflict. + // Sync should auto-resolve the conflict in favor of upstream (theirs). _, err := SyncToProjectDir(upstreamPath, projectDir) - if err == nil { - t.Fatal("expected merge conflict error") - } - if !strings.Contains(err.Error(), "merge failed") { - t.Errorf("expected 'merge failed' in error, got: %v", err) + if err != nil { + t.Fatalf("expected auto-resolved merge, got error: %v", err) } - // Verify merge was aborted — MERGE_HEAD should not exist. - mergeHead := filepath.Join(projectDir, ".git", "MERGE_HEAD") - if _, err := os.Stat(mergeHead); err == nil { - t.Error("MERGE_HEAD exists — merge was not aborted") + // Verify upstream version won. + content, err := os.ReadFile(filepath.Join(projectDir, "conflict.txt")) + if err != nil { + t.Fatal(err) + } + if string(content) != "agent version\n" { + t.Errorf("expected upstream content to win, got: %q", string(content)) } }) }