feat(sling): add --no-merge flag to skip merge queue (#939)
When contributing to upstream repos or wanting human review before merge, the --no-merge flag keeps polecat work on a feature branch instead of auto-merging to main. Changes: - Add --no-merge flag to gt sling command - Store no_merge field in bead's AttachmentFields - Modify gt done to skip merge queue when no_merge is set - Push branch and mail dispatcher "READY_FOR_REVIEW" instead - Add tests for field parsing and sling flag storage Closes: gt-p7b8 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
a86c7d954f
commit
92ccacffd9
@@ -903,6 +903,80 @@ func TestAttachmentFieldsRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoMergeField tests the no_merge field in AttachmentFields.
|
||||
// The no_merge flag tells gt done to skip the merge queue and keep work on a feature branch.
|
||||
func TestNoMergeField(t *testing.T) {
|
||||
t.Run("parse no_merge true", func(t *testing.T) {
|
||||
issue := &Issue{Description: "no_merge: true\ndispatched_by: mayor"}
|
||||
fields := ParseAttachmentFields(issue)
|
||||
if fields == nil {
|
||||
t.Fatal("ParseAttachmentFields() = nil")
|
||||
}
|
||||
if !fields.NoMerge {
|
||||
t.Error("NoMerge should be true")
|
||||
}
|
||||
if fields.DispatchedBy != "mayor" {
|
||||
t.Errorf("DispatchedBy = %q, want 'mayor'", fields.DispatchedBy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse no_merge false", func(t *testing.T) {
|
||||
issue := &Issue{Description: "no_merge: false\ndispatched_by: crew"}
|
||||
fields := ParseAttachmentFields(issue)
|
||||
if fields == nil {
|
||||
t.Fatal("ParseAttachmentFields() = nil")
|
||||
}
|
||||
if fields.NoMerge {
|
||||
t.Error("NoMerge should be false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse no-merge alternate format", func(t *testing.T) {
|
||||
issue := &Issue{Description: "no-merge: true"}
|
||||
fields := ParseAttachmentFields(issue)
|
||||
if fields == nil {
|
||||
t.Fatal("ParseAttachmentFields() = nil")
|
||||
}
|
||||
if !fields.NoMerge {
|
||||
t.Error("NoMerge should be true with hyphen format")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format no_merge", func(t *testing.T) {
|
||||
fields := &AttachmentFields{
|
||||
NoMerge: true,
|
||||
DispatchedBy: "mayor",
|
||||
}
|
||||
got := FormatAttachmentFields(fields)
|
||||
if !strings.Contains(got, "no_merge: true") {
|
||||
t.Errorf("FormatAttachmentFields() missing no_merge, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "dispatched_by: mayor") {
|
||||
t.Errorf("FormatAttachmentFields() missing dispatched_by, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("round-trip with no_merge", func(t *testing.T) {
|
||||
original := &AttachmentFields{
|
||||
AttachedMolecule: "mol-test",
|
||||
AttachedAt: "2026-01-24T12:00:00Z",
|
||||
DispatchedBy: "gastown/crew/max",
|
||||
NoMerge: true,
|
||||
}
|
||||
|
||||
formatted := FormatAttachmentFields(original)
|
||||
issue := &Issue{Description: formatted}
|
||||
parsed := ParseAttachmentFields(issue)
|
||||
|
||||
if parsed == nil {
|
||||
t.Fatal("round-trip parse returned nil")
|
||||
}
|
||||
if *parsed != *original {
|
||||
t.Errorf("round-trip mismatch:\ngot %+v\nwant %+v", parsed, original)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestResolveBeadsDir tests the redirect following logic.
|
||||
func TestResolveBeadsDir(t *testing.T) {
|
||||
// Create temp directory structure
|
||||
|
||||
@@ -21,6 +21,7 @@ type AttachmentFields struct {
|
||||
AttachedAt string // ISO 8601 timestamp when attached
|
||||
AttachedArgs string // Natural language args passed via gt sling --args (no-tmux mode)
|
||||
DispatchedBy string // Agent ID that dispatched this work (for completion notification)
|
||||
NoMerge bool // If true, gt done skips merge queue (for upstream PRs/human review)
|
||||
}
|
||||
|
||||
// ParseAttachmentFields extracts attachment fields from an issue's description.
|
||||
@@ -65,6 +66,9 @@ func ParseAttachmentFields(issue *Issue) *AttachmentFields {
|
||||
case "dispatched_by", "dispatched-by", "dispatchedby":
|
||||
fields.DispatchedBy = value
|
||||
hasFields = true
|
||||
case "no_merge", "no-merge", "nomerge":
|
||||
fields.NoMerge = strings.ToLower(value) == "true"
|
||||
hasFields = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +99,9 @@ func FormatAttachmentFields(fields *AttachmentFields) string {
|
||||
if fields.DispatchedBy != "" {
|
||||
lines = append(lines, "dispatched_by: "+fields.DispatchedBy)
|
||||
}
|
||||
if fields.NoMerge {
|
||||
lines = append(lines, "no_merge: true")
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -117,6 +124,9 @@ func SetAttachmentFields(issue *Issue, fields *AttachmentFields) string {
|
||||
"dispatched_by": true,
|
||||
"dispatched-by": true,
|
||||
"dispatchedby": true,
|
||||
"no_merge": true,
|
||||
"no-merge": true,
|
||||
"nomerge": true,
|
||||
}
|
||||
|
||||
// Collect non-attachment lines from existing description
|
||||
|
||||
Reference in New Issue
Block a user