feat: add refinery merge queue processing loop
- ProcessQueue: iterates pending MRs and processes each - ProcessMR: fetch, merge, test, push workflow - Conflict detection with merge abort - Test integration via configurable test_command - Automatic branch cleanup after successful merge - Stats tracking (merged/failed counts) - 10-second polling loop in foreground mode Closes gt-ov2 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -249,13 +249,217 @@ func (m *Manager) branchToMR(branch string) *MergeRequest {
|
|||||||
|
|
||||||
// run is the main processing loop (for foreground mode).
|
// run is the main processing loop (for foreground mode).
|
||||||
func (m *Manager) run(ref *Refinery) error {
|
func (m *Manager) run(ref *Refinery) error {
|
||||||
// MVP: Just a stub that returns immediately
|
fmt.Println("Refinery running...")
|
||||||
// Full implementation in gt-ov2
|
|
||||||
fmt.Println("Refinery running (stub mode)...")
|
|
||||||
fmt.Println("Press Ctrl+C to stop")
|
fmt.Println("Press Ctrl+C to stop")
|
||||||
|
|
||||||
// Would normally loop here processing the queue
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
select {}
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// Process queue
|
||||||
|
if err := m.ProcessQueue(); err != nil {
|
||||||
|
fmt.Printf("Queue processing error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessQueue processes all pending merge requests.
|
||||||
|
func (m *Manager) ProcessQueue() error {
|
||||||
|
queue, err := m.Queue()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range queue {
|
||||||
|
if item.MR.Status != MRPending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Processing: %s (%s)\n", item.MR.Branch, item.MR.Worker)
|
||||||
|
|
||||||
|
result := m.ProcessMR(item.MR)
|
||||||
|
if result.Success {
|
||||||
|
fmt.Printf(" ✓ Merged successfully\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✗ Failed: %s\n", result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeResult contains the result of a merge attempt.
|
||||||
|
type MergeResult struct {
|
||||||
|
Success bool
|
||||||
|
Error string
|
||||||
|
Conflict bool
|
||||||
|
TestsFailed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessMR processes a single merge request.
|
||||||
|
func (m *Manager) ProcessMR(mr *MergeRequest) MergeResult {
|
||||||
|
ref, _ := m.loadState()
|
||||||
|
|
||||||
|
// Set current MR
|
||||||
|
ref.CurrentMR = mr
|
||||||
|
mr.Status = MRProcessing
|
||||||
|
m.saveState(ref)
|
||||||
|
|
||||||
|
result := MergeResult{}
|
||||||
|
|
||||||
|
// 1. Fetch the branch
|
||||||
|
if err := m.gitRun("fetch", "origin", mr.Branch); err != nil {
|
||||||
|
result.Error = fmt.Sprintf("fetch failed: %v", err)
|
||||||
|
m.completeMR(mr, MRFailed, result.Error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Attempt merge to target branch
|
||||||
|
// First, checkout target
|
||||||
|
if err := m.gitRun("checkout", mr.TargetBranch); err != nil {
|
||||||
|
result.Error = fmt.Sprintf("checkout target failed: %v", err)
|
||||||
|
m.completeMR(mr, MRFailed, result.Error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull latest
|
||||||
|
m.gitRun("pull", "origin", mr.TargetBranch) // Ignore errors
|
||||||
|
|
||||||
|
// Merge
|
||||||
|
err := m.gitRun("merge", "--no-ff", "-m",
|
||||||
|
fmt.Sprintf("Merge %s from %s", mr.Branch, mr.Worker),
|
||||||
|
"origin/"+mr.Branch)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errStr := err.Error()
|
||||||
|
if strings.Contains(errStr, "CONFLICT") || strings.Contains(errStr, "conflict") {
|
||||||
|
result.Conflict = true
|
||||||
|
result.Error = "merge conflict"
|
||||||
|
// Abort the merge
|
||||||
|
m.gitRun("merge", "--abort")
|
||||||
|
m.completeMR(mr, MRFailed, "merge conflict - polecat must rebase")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Error = fmt.Sprintf("merge failed: %v", err)
|
||||||
|
m.completeMR(mr, MRFailed, result.Error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Run tests if configured
|
||||||
|
testCmd := m.getTestCommand()
|
||||||
|
if testCmd != "" {
|
||||||
|
if err := m.runTests(testCmd); err != nil {
|
||||||
|
result.TestsFailed = true
|
||||||
|
result.Error = fmt.Sprintf("tests failed: %v", err)
|
||||||
|
// Reset to before merge
|
||||||
|
m.gitRun("reset", "--hard", "HEAD~1")
|
||||||
|
m.completeMR(mr, MRFailed, result.Error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Push
|
||||||
|
if err := m.gitRun("push", "origin", mr.TargetBranch); err != nil {
|
||||||
|
result.Error = fmt.Sprintf("push failed: %v", err)
|
||||||
|
// Reset to before merge
|
||||||
|
m.gitRun("reset", "--hard", "HEAD~1")
|
||||||
|
m.completeMR(mr, MRFailed, result.Error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success!
|
||||||
|
result.Success = true
|
||||||
|
m.completeMR(mr, MRMerged, "")
|
||||||
|
|
||||||
|
// Optionally delete the merged branch
|
||||||
|
m.gitRun("push", "origin", "--delete", mr.Branch)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeMR marks an MR as complete and updates stats.
|
||||||
|
func (m *Manager) completeMR(mr *MergeRequest, status MRStatus, errMsg string) {
|
||||||
|
ref, _ := m.loadState()
|
||||||
|
|
||||||
|
mr.Status = status
|
||||||
|
mr.Error = errMsg
|
||||||
|
ref.CurrentMR = nil
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
switch status {
|
||||||
|
case MRMerged:
|
||||||
|
ref.LastMergeAt = &now
|
||||||
|
ref.Stats.TotalMerged++
|
||||||
|
ref.Stats.TodayMerged++
|
||||||
|
case MRFailed:
|
||||||
|
ref.Stats.TotalFailed++
|
||||||
|
ref.Stats.TodayFailed++
|
||||||
|
case MRSkipped:
|
||||||
|
ref.Stats.TotalSkipped++
|
||||||
|
}
|
||||||
|
|
||||||
|
m.saveState(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTestCommand returns the test command if configured.
|
||||||
|
func (m *Manager) getTestCommand() string {
|
||||||
|
// Check for .gastown/config.json with test_command
|
||||||
|
configPath := filepath.Join(m.rig.Path, ".gastown", "config.json")
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var config struct {
|
||||||
|
TestCommand string `json:"test_command"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.TestCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTests executes the test command.
|
||||||
|
func (m *Manager) runTests(testCmd string) error {
|
||||||
|
parts := strings.Fields(testCmd)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(parts[0], parts[1:]...)
|
||||||
|
cmd.Dir = m.workDir
|
||||||
|
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %s", err, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitRun executes a git command.
|
||||||
|
func (m *Manager) gitRun(args ...string) error {
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = m.workDir
|
||||||
|
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
errMsg := strings.TrimSpace(stderr.String())
|
||||||
|
if errMsg != "" {
|
||||||
|
return fmt.Errorf("%s", errMsg)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processExists checks if a process with the given PID exists.
|
// processExists checks if a process with the given PID exists.
|
||||||
|
|||||||
Reference in New Issue
Block a user