diff --git a/internal/git/git.go b/internal/git/git.go index 4e79ef12..8ab9b523 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -570,6 +570,27 @@ func (g *Git) MergeNoFF(branch, message string) error { return err } +// MergeSquash performs a squash merge of the given branch and commits with the provided message. +// This stages all changes from the branch without creating a merge commit, then commits them +// as a single commit with the given message. This eliminates redundant merge commits while +// preserving the original commit message from the source branch. +func (g *Git) MergeSquash(branch, message string) error { + // Stage all changes from the branch without committing + if _, err := g.run("merge", "--squash", branch); err != nil { + return err + } + // Commit the staged changes with the provided message + _, err := g.run("commit", "-m", message) + return err +} + +// GetBranchCommitMessage returns the commit message of the HEAD commit on the given branch. +// This is useful for preserving the original conventional commit message (feat:/fix:) when +// performing squash merges. +func (g *Git) GetBranchCommitMessage(branch string) (string, error) { + return g.run("log", "-1", "--format=%B", branch) +} + // DeleteRemoteBranch deletes a branch on the remote. func (g *Git) DeleteRemoteBranch(remote, branch string) error { _, err := g.run("push", remote, "--delete", branch) diff --git a/internal/refinery/engineer.go b/internal/refinery/engineer.go index 526aca87..1b0a1a2f 100644 --- a/internal/refinery/engineer.go +++ b/internal/refinery/engineer.go @@ -319,13 +319,20 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri _, _ = 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) + // Step 5: Perform the actual merge using squash merge + // Get the original commit message from the polecat branch to preserve the + // conventional commit format (feat:/fix:) instead of creating redundant merge commits + originalMsg, err := e.git.GetBranchCommitMessage(branch) + if err != nil { + // Fallback to a descriptive message if we can't get the original + originalMsg = fmt.Sprintf("Squash merge %s into %s", branch, target) + if sourceIssue != "" { + originalMsg = fmt.Sprintf("Squash merge %s into %s (%s)", branch, target, sourceIssue) + } + _, _ = fmt.Fprintf(e.output, "[Engineer] Warning: could not get original commit message: %v\n", err) } - _, _ = fmt.Fprintf(e.output, "[Engineer] Merging with message: %s\n", mergeMsg) - if err := e.git.MergeNoFF(branch, mergeMsg); err != nil { + _, _ = fmt.Fprintf(e.output, "[Engineer] Squash merging with message: %s\n", strings.TrimSpace(originalMsg)) + if err := e.git.MergeSquash(branch, originalMsg); err != nil { // ZFC: Use git's porcelain output to detect conflicts instead of parsing stderr. // GetConflictingFiles() uses `git diff --diff-filter=U` which is proper. conflicts, conflictErr := e.git.GetConflictingFiles()