Implement git merge logic in Engineer.ProcessMR (gt-pnv61)

Adds actual git merge functionality to ProcessMR and ProcessMRFromQueue:
- Fetch source branch from origin
- Checkout target branch and pull latest
- Check for merge conflicts using test merge
- Run configured tests with retry support
- Perform --no-ff merge with descriptive message
- Push to origin
- Return detailed ProcessResult with success/conflict/test status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/polecats/rictus
2025-12-30 22:40:10 -08:00
committed by Steve Yegge
parent 4f99617b49
commit 24d5231661

View File

@@ -2,11 +2,14 @@
package refinery
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"time"
@@ -200,8 +203,6 @@ type ProcessResult struct {
}
// ProcessMR processes a single merge request from a beads issue.
// Note: The Refinery agent primarily processes merges via git commands
// in the role prompt, not via this Go code path.
func (e *Engineer) ProcessMR(ctx context.Context, mr *beads.Issue) ProcessResult {
// Parse MR fields from description
mrFields := beads.ParseMRFields(mr)
@@ -212,16 +213,164 @@ func (e *Engineer) ProcessMR(ctx context.Context, mr *beads.Issue) ProcessResult
}
}
// Log what we would process
fmt.Fprintln(e.output, "[Engineer] Would process:")
// Log what we're processing
fmt.Fprintln(e.output, "[Engineer] Processing MR:")
fmt.Fprintf(e.output, " Branch: %s\n", mrFields.Branch)
fmt.Fprintf(e.output, " Target: %s\n", mrFields.Target)
fmt.Fprintf(e.output, " Worker: %s\n", mrFields.Worker)
// This code path is not used in production - Refinery agent uses git commands
return e.doMerge(ctx, mrFields.Branch, mrFields.Target, mrFields.SourceIssue)
}
// doMerge performs the actual git merge operation.
// This is the core merge logic shared by ProcessMR and ProcessMRFromQueue.
func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue string) ProcessResult {
// Step 1: Fetch the source branch from origin
fmt.Fprintf(e.output, "[Engineer] Fetching branch %s from origin...\n", branch)
if err := e.git.FetchBranch("origin", branch); err != nil {
return ProcessResult{
Success: false,
Error: fmt.Sprintf("failed to fetch branch %s: %v", branch, err),
}
}
// Step 2: Checkout the target branch
fmt.Fprintf(e.output, "[Engineer] Checking out target branch %s...\n", target)
if err := e.git.Checkout(target); err != nil {
return ProcessResult{
Success: false,
Error: fmt.Sprintf("failed to checkout target %s: %v", target, err),
}
}
// Make sure target is up to date with origin
if err := e.git.Pull("origin", target); err != nil {
// Pull might fail if nothing to pull, that's ok
fmt.Fprintf(e.output, "[Engineer] Warning: pull from origin/%s: %v (continuing)\n", target, err)
}
// Step 3: Check for merge conflicts
fmt.Fprintf(e.output, "[Engineer] Checking for conflicts...\n")
remoteBranch := "origin/" + branch
conflicts, err := e.git.CheckConflicts(remoteBranch, target)
if err != nil {
return ProcessResult{
Success: false,
Conflict: true,
Error: fmt.Sprintf("conflict check failed: %v", err),
}
}
if len(conflicts) > 0 {
return ProcessResult{
Success: false,
Conflict: true,
Error: fmt.Sprintf("merge conflicts in: %v", conflicts),
}
}
// Step 4: Run tests if configured
if e.config.RunTests && e.config.TestCommand != "" {
fmt.Fprintf(e.output, "[Engineer] Running tests: %s\n", e.config.TestCommand)
result := e.runTests(ctx)
if !result.Success {
return ProcessResult{
Success: false,
TestsFailed: true,
Error: result.Error,
}
}
fmt.Fprintln(e.output, "[Engineer] Tests passed")
}
// Step 5: Perform the actual merge
mergeMsg := fmt.Sprintf("Merge %s into %s", branch, target)
if sourceIssue != "" {
mergeMsg = fmt.Sprintf("Merge %s into %s (%s)", branch, target, sourceIssue)
}
fmt.Fprintf(e.output, "[Engineer] Merging with message: %s\n", mergeMsg)
if err := e.git.MergeNoFF(remoteBranch, mergeMsg); err != nil {
if errors.Is(err, git.ErrMergeConflict) {
_ = e.git.AbortMerge()
return ProcessResult{
Success: false,
Conflict: true,
Error: "merge conflict during actual merge",
}
}
return ProcessResult{
Success: false,
Error: fmt.Sprintf("merge failed: %v", err),
}
}
// Step 6: Get the merge commit SHA
mergeCommit, err := e.git.Rev("HEAD")
if err != nil {
return ProcessResult{
Success: false,
Error: fmt.Sprintf("failed to get merge commit SHA: %v", err),
}
}
// Step 7: Push to origin
fmt.Fprintf(e.output, "[Engineer] Pushing to origin/%s...\n", target)
if err := e.git.Push("origin", target, false); err != nil {
return ProcessResult{
Success: false,
Error: fmt.Sprintf("failed to push to origin: %v", err),
}
}
fmt.Fprintf(e.output, "[Engineer] Successfully merged: %s\n", mergeCommit[:8])
return ProcessResult{
Success: false,
Error: "ProcessMR: use Refinery agent's git-based merge flow instead",
Success: true,
MergeCommit: mergeCommit,
}
}
// runTests runs the configured test command and returns the result.
func (e *Engineer) runTests(ctx context.Context) ProcessResult {
if e.config.TestCommand == "" {
return ProcessResult{Success: true}
}
// Run the test command with retries for flaky tests
maxRetries := e.config.RetryFlakyTests
if maxRetries < 1 {
maxRetries = 1
}
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
fmt.Fprintf(e.output, "[Engineer] Retrying tests (attempt %d/%d)...\n", attempt, maxRetries)
}
cmd := exec.CommandContext(ctx, "sh", "-c", e.config.TestCommand)
cmd.Dir = e.workDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err == nil {
return ProcessResult{Success: true}
}
lastErr = err
// Check if context was cancelled
if ctx.Err() != nil {
return ProcessResult{
Success: false,
Error: "test run cancelled",
}
}
}
return ProcessResult{
Success: false,
TestsFailed: true,
Error: fmt.Sprintf("tests failed after %d attempts: %v", maxRetries, lastErr),
}
}
@@ -296,8 +445,6 @@ func (e *Engineer) handleFailure(mr *beads.Issue, result ProcessResult) {
}
// ProcessMRFromQueue processes a merge request from wisp queue.
// Note: The Refinery agent primarily processes merges via git commands
// in the role prompt, not via this Go code path.
func (e *Engineer) ProcessMRFromQueue(ctx context.Context, mr *mrqueue.MR) ProcessResult {
// MR fields are directly on the struct (no parsing needed)
fmt.Fprintln(e.output, "[Engineer] Processing MR from queue:")
@@ -311,11 +458,8 @@ func (e *Engineer) ProcessMRFromQueue(ctx context.Context, mr *mrqueue.MR) Proce
fmt.Fprintf(e.output, "[Engineer] Warning: failed to log merge_started event: %v\n", err)
}
// This code path is not used in production - Refinery agent uses git commands
return ProcessResult{
Success: false,
Error: "ProcessMRFromQueue: use Refinery agent's git-based merge flow instead",
}
// Use the shared merge logic
return e.doMerge(ctx, mr.Branch, mr.Target, mr.SourceIssue)
}
// handleSuccessFromQueue handles a successful merge from wisp queue.