Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/robmorgan/metamorph/internal/constants"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -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:")
Expand Down
4 changes: 2 additions & 2 deletions internal/gitops/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
22 changes: 11 additions & 11 deletions internal/gitops/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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))
}
})
}