feat(mq): add configurable integration branch naming (#104)
Enterprise teams can now customize integration branch names to match
their conventions (e.g., username/TICKET-123/feature-name).
- Add integration_branch_template to MergeQueueConfig
- Add --branch CLI override for gt mq integration create
- Support {epic}, {prefix}, {user} template variables
- Validate branch names for git-safe characters
- Store actual branch name in epic metadata at create time
- Read stored branch name in land/status (fallback for old epics)
Also fixes unrelated build error in polecat/manager.go (polecatPath
variable was undefined).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
f19ddc5400
commit
358fcaf935
@@ -47,6 +47,9 @@ var (
|
|||||||
|
|
||||||
// Integration status flags
|
// Integration status flags
|
||||||
mqIntegrationStatusJSON bool
|
mqIntegrationStatusJSON bool
|
||||||
|
|
||||||
|
// Integration create flags
|
||||||
|
mqIntegrationCreateBranch string
|
||||||
)
|
)
|
||||||
|
|
||||||
var mqCmd = &cobra.Command{
|
var mqCmd = &cobra.Command{
|
||||||
@@ -190,18 +193,31 @@ var mqIntegrationCreateCmd = &cobra.Command{
|
|||||||
Short: "Create an integration branch for an epic",
|
Short: "Create an integration branch for an epic",
|
||||||
Long: `Create an integration branch for batch work on an epic.
|
Long: `Create an integration branch for batch work on an epic.
|
||||||
|
|
||||||
Creates a branch named integration/<epic-id> from main and pushes it
|
Creates a branch from main and pushes it to origin. Future MRs for this
|
||||||
to origin. Future MRs for this epic's children can target this branch.
|
epic's children can target this branch.
|
||||||
|
|
||||||
|
Branch naming:
|
||||||
|
Default: integration/<epic-id>
|
||||||
|
Config: Set merge_queue.integration_branch_template in rig settings
|
||||||
|
Override: Use --branch flag for one-off customization
|
||||||
|
|
||||||
|
Template variables:
|
||||||
|
{epic} - Full epic ID (e.g., "RA-123")
|
||||||
|
{prefix} - Epic prefix before first hyphen (e.g., "RA")
|
||||||
|
{user} - Git user.name (e.g., "klauern")
|
||||||
|
|
||||||
Actions:
|
Actions:
|
||||||
1. Verify epic exists
|
1. Verify epic exists
|
||||||
2. Create branch integration/<epic-id> from main
|
2. Create branch from main (using template or --branch)
|
||||||
3. Push to origin
|
3. Push to origin
|
||||||
4. Store integration branch info in epic metadata
|
4. Store actual branch name in epic metadata
|
||||||
|
|
||||||
Example:
|
Examples:
|
||||||
gt mq integration create gt-auth-epic
|
gt mq integration create gt-auth-epic
|
||||||
# Creates integration/gt-auth-epic from main`,
|
# Creates integration/gt-auth-epic (default)
|
||||||
|
|
||||||
|
gt mq integration create RA-123 --branch "klauern/PROJ-1234/{epic}"
|
||||||
|
# Creates klauern/PROJ-1234/RA-123`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: runMqIntegrationCreate,
|
RunE: runMqIntegrationCreate,
|
||||||
}
|
}
|
||||||
@@ -287,6 +303,7 @@ func init() {
|
|||||||
mqCmd.AddCommand(mqStatusCmd)
|
mqCmd.AddCommand(mqStatusCmd)
|
||||||
|
|
||||||
// Integration branch subcommands
|
// Integration branch subcommands
|
||||||
|
mqIntegrationCreateCmd.Flags().StringVar(&mqIntegrationCreateBranch, "branch", "", "Override branch name template (supports {epic}, {prefix}, {user})")
|
||||||
mqIntegrationCmd.AddCommand(mqIntegrationCreateCmd)
|
mqIntegrationCmd.AddCommand(mqIntegrationCreateCmd)
|
||||||
|
|
||||||
// Integration land flags
|
// Integration land flags
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -16,6 +17,141 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Integration branch template constants
|
||||||
|
const defaultIntegrationBranchTemplate = "integration/{epic}"
|
||||||
|
|
||||||
|
// invalidBranchCharsRegex matches characters that are invalid in git branch names.
|
||||||
|
// Git branch names cannot contain: ~ ^ : \ space, .., @{, or end with .lock
|
||||||
|
var invalidBranchCharsRegex = regexp.MustCompile(`[~^:\s\\]|\.\.|\.\.|@\{`)
|
||||||
|
|
||||||
|
// buildIntegrationBranchName expands an integration branch template with variables.
|
||||||
|
// Variables supported:
|
||||||
|
// - {epic}: Full epic ID (e.g., "RA-123")
|
||||||
|
// - {prefix}: Epic prefix before first hyphen (e.g., "RA")
|
||||||
|
// - {user}: Git user.name (e.g., "klauern")
|
||||||
|
//
|
||||||
|
// If template is empty, uses defaultIntegrationBranchTemplate.
|
||||||
|
func buildIntegrationBranchName(template, epicID string) string {
|
||||||
|
if template == "" {
|
||||||
|
template = defaultIntegrationBranchTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
result := template
|
||||||
|
result = strings.ReplaceAll(result, "{epic}", epicID)
|
||||||
|
result = strings.ReplaceAll(result, "{prefix}", extractEpicPrefix(epicID))
|
||||||
|
|
||||||
|
// Git user (optional - leaves placeholder if not available)
|
||||||
|
if user := getGitUserName(); user != "" {
|
||||||
|
result = strings.ReplaceAll(result, "{user}", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractEpicPrefix extracts the prefix from an epic ID (before the first hyphen).
|
||||||
|
// Examples: "RA-123" -> "RA", "PROJ-456" -> "PROJ", "abc" -> "abc"
|
||||||
|
func extractEpicPrefix(epicID string) string {
|
||||||
|
if idx := strings.Index(epicID, "-"); idx > 0 {
|
||||||
|
return epicID[:idx]
|
||||||
|
}
|
||||||
|
return epicID
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGitUserName returns the git user.name config value, or empty if not set.
|
||||||
|
func getGitUserName() string {
|
||||||
|
cmd := exec.Command("git", "config", "user.name")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateBranchName checks if a branch name is valid for git.
|
||||||
|
// Returns an error if the branch name contains invalid characters.
|
||||||
|
func validateBranchName(branchName string) error {
|
||||||
|
if branchName == "" {
|
||||||
|
return fmt.Errorf("branch name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid characters
|
||||||
|
if invalidBranchCharsRegex.MatchString(branchName) {
|
||||||
|
return fmt.Errorf("branch name %q contains invalid characters (~ ^ : \\ space, .., or @{)", branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for .lock suffix
|
||||||
|
if strings.HasSuffix(branchName, ".lock") {
|
||||||
|
return fmt.Errorf("branch name %q cannot end with .lock", branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for leading/trailing slashes or dots
|
||||||
|
if strings.HasPrefix(branchName, "/") || strings.HasSuffix(branchName, "/") {
|
||||||
|
return fmt.Errorf("branch name %q cannot start or end with /", branchName)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(branchName, ".") || strings.HasSuffix(branchName, ".") {
|
||||||
|
return fmt.Errorf("branch name %q cannot start or end with .", branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for consecutive slashes
|
||||||
|
if strings.Contains(branchName, "//") {
|
||||||
|
return fmt.Errorf("branch name %q cannot contain consecutive slashes", branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIntegrationBranchField extracts the integration_branch field from an epic's description.
|
||||||
|
// Returns empty string if the field is not found.
|
||||||
|
func getIntegrationBranchField(description string) string {
|
||||||
|
if description == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(description, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(strings.ToLower(trimmed), "integration_branch:") {
|
||||||
|
value := strings.TrimPrefix(trimmed, "integration_branch:")
|
||||||
|
value = strings.TrimPrefix(value, "Integration_branch:")
|
||||||
|
value = strings.TrimPrefix(value, "INTEGRATION_BRANCH:")
|
||||||
|
// Handle case variations
|
||||||
|
for _, prefix := range []string{"integration_branch:", "Integration_branch:", "INTEGRATION_BRANCH:"} {
|
||||||
|
if strings.HasPrefix(trimmed, prefix) {
|
||||||
|
value = strings.TrimPrefix(trimmed, prefix)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Re-parse properly - the prefix removal above is messy
|
||||||
|
parts := strings.SplitN(trimmed, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIntegrationBranchTemplate returns the integration branch template to use.
|
||||||
|
// Priority: CLI flag > rig config > default
|
||||||
|
func getIntegrationBranchTemplate(rigPath, cliOverride string) string {
|
||||||
|
if cliOverride != "" {
|
||||||
|
return cliOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load rig settings
|
||||||
|
settingsPath := filepath.Join(rigPath, "settings", "config.json")
|
||||||
|
settings, err := config.LoadRigSettings(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
return defaultIntegrationBranchTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.MergeQueue != nil && settings.MergeQueue.IntegrationBranchTemplate != "" {
|
||||||
|
return settings.MergeQueue.IntegrationBranchTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultIntegrationBranchTemplate
|
||||||
|
}
|
||||||
|
|
||||||
// IntegrationStatusOutput is the JSON output structure for integration status.
|
// IntegrationStatusOutput is the JSON output structure for integration status.
|
||||||
type IntegrationStatusOutput struct {
|
type IntegrationStatusOutput struct {
|
||||||
Epic string `json:"epic"`
|
Epic string `json:"epic"`
|
||||||
@@ -66,8 +202,14 @@ func runMqIntegrationCreate(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("'%s' is a %s, not an epic", epicID, epic.Type)
|
return fmt.Errorf("'%s' is a %s, not an epic", epicID, epic.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build integration branch name
|
// Build integration branch name from template
|
||||||
branchName := "integration/" + epicID
|
template := getIntegrationBranchTemplate(r.Path, mqIntegrationCreateBranch)
|
||||||
|
branchName := buildIntegrationBranchName(template, epicID)
|
||||||
|
|
||||||
|
// Validate the branch name
|
||||||
|
if err := validateBranchName(branchName); err != nil {
|
||||||
|
return fmt.Errorf("invalid branch name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize git for the rig
|
// Initialize git for the rig
|
||||||
g := git.NewGit(r.Path)
|
g := git.NewGit(r.Path)
|
||||||
@@ -185,9 +327,6 @@ func runMqIntegrationLand(cmd *cobra.Command, args []string) error {
|
|||||||
bd := beads.New(r.Path)
|
bd := beads.New(r.Path)
|
||||||
g := git.NewGit(r.Path)
|
g := git.NewGit(r.Path)
|
||||||
|
|
||||||
// Build integration branch name
|
|
||||||
branchName := "integration/" + epicID
|
|
||||||
|
|
||||||
// Show what we're about to do
|
// Show what we're about to do
|
||||||
if mqIntegrationLandDryRun {
|
if mqIntegrationLandDryRun {
|
||||||
fmt.Printf("%s Dry run - no changes will be made\n\n", style.Bold.Render("🔍"))
|
fmt.Printf("%s Dry run - no changes will be made\n\n", style.Bold.Render("🔍"))
|
||||||
@@ -206,6 +345,13 @@ func runMqIntegrationLand(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("'%s' is a %s, not an epic", epicID, epic.Type)
|
return fmt.Errorf("'%s' is a %s, not an epic", epicID, epic.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get integration branch name from epic metadata (stored at create time)
|
||||||
|
// Fall back to default template for backward compatibility with old epics
|
||||||
|
branchName := getIntegrationBranchField(epic.Description)
|
||||||
|
if branchName == "" {
|
||||||
|
branchName = buildIntegrationBranchName(defaultIntegrationBranchTemplate, epicID)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Landing integration branch for epic: %s\n", epicID)
|
fmt.Printf("Landing integration branch for epic: %s\n", epicID)
|
||||||
fmt.Printf(" Title: %s\n\n", epic.Title)
|
fmt.Printf(" Title: %s\n\n", epic.Title)
|
||||||
|
|
||||||
@@ -455,8 +601,21 @@ func runMqIntegrationStatus(cmd *cobra.Command, args []string) error {
|
|||||||
// Initialize beads for the rig
|
// Initialize beads for the rig
|
||||||
bd := beads.New(r.Path)
|
bd := beads.New(r.Path)
|
||||||
|
|
||||||
// Build integration branch name
|
// Fetch epic to get stored branch name
|
||||||
branchName := "integration/" + epicID
|
epic, err := bd.Show(epicID)
|
||||||
|
if err != nil {
|
||||||
|
if err == beads.ErrNotFound {
|
||||||
|
return fmt.Errorf("epic '%s' not found", epicID)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("fetching epic: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get integration branch name from epic metadata (stored at create time)
|
||||||
|
// Fall back to default template for backward compatibility with old epics
|
||||||
|
branchName := getIntegrationBranchField(epic.Description)
|
||||||
|
if branchName == "" {
|
||||||
|
branchName = buildIntegrationBranchName(defaultIntegrationBranchTemplate, epicID)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize git for the rig
|
// Initialize git for the rig
|
||||||
g := git.NewGit(r.Path)
|
g := git.NewGit(r.Path)
|
||||||
@@ -492,8 +651,8 @@ func runMqIntegrationStatus(cmd *cobra.Command, args []string) error {
|
|||||||
aheadCount = 0 // Non-fatal
|
aheadCount = 0 // Non-fatal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for MRs targeting this integration branch
|
// Query for MRs targeting this integration branch (use resolved name)
|
||||||
targetBranch := "integration/" + epicID
|
targetBranch := branchName
|
||||||
|
|
||||||
// Get all merge-request issues
|
// Get all merge-request issues
|
||||||
allMRs, err := bd.List(beads.ListOptions{
|
allMRs, err := bd.List(beads.ListOptions{
|
||||||
|
|||||||
@@ -434,3 +434,247 @@ func TestFilterMRsByTarget_NoMRFields(t *testing.T) {
|
|||||||
t.Errorf("filterMRsByTarget() should filter out issues without MR fields, got %d", len(got))
|
t.Errorf("filterMRsByTarget() should filter out issues without MR fields, got %d", len(got))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for configurable integration branch naming (Issue #104)
|
||||||
|
|
||||||
|
func TestBuildIntegrationBranchName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
template string
|
||||||
|
epicID string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default template",
|
||||||
|
template: "",
|
||||||
|
epicID: "RA-123",
|
||||||
|
want: "integration/RA-123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit default template",
|
||||||
|
template: "integration/{epic}",
|
||||||
|
epicID: "PROJ-456",
|
||||||
|
want: "integration/PROJ-456",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom template with prefix",
|
||||||
|
template: "{prefix}/{epic}",
|
||||||
|
epicID: "RA-123",
|
||||||
|
want: "RA/RA-123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex template",
|
||||||
|
template: "feature/{prefix}/work/{epic}",
|
||||||
|
epicID: "PROJ-789",
|
||||||
|
want: "feature/PROJ/work/PROJ-789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "epic without hyphen",
|
||||||
|
template: "{prefix}/{epic}",
|
||||||
|
epicID: "epicname",
|
||||||
|
want: "epicname/epicname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user variable left as-is without git config",
|
||||||
|
template: "{user}/{epic}",
|
||||||
|
epicID: "RA-123",
|
||||||
|
// Note: {user} is replaced with git user.name if available,
|
||||||
|
// otherwise left as placeholder. In tests, it depends on git config.
|
||||||
|
want: "", // We'll check pattern instead
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := buildIntegrationBranchName(tt.template, tt.epicID)
|
||||||
|
if tt.want == "" {
|
||||||
|
// For user variable test, just check {epic} was replaced
|
||||||
|
if stringContains(got, "{epic}") {
|
||||||
|
t.Errorf("buildIntegrationBranchName() = %q, should have replaced {epic}", got)
|
||||||
|
}
|
||||||
|
} else if got != tt.want {
|
||||||
|
t.Errorf("buildIntegrationBranchName() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractEpicPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
epicID string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"RA-123", "RA"},
|
||||||
|
{"PROJ-456", "PROJ"},
|
||||||
|
{"gt-auth-epic", "gt"},
|
||||||
|
{"epicname", "epicname"},
|
||||||
|
{"X-1", "X"},
|
||||||
|
{"-123", "-123"}, // No prefix before hyphen, return full string
|
||||||
|
{"", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.epicID, func(t *testing.T) {
|
||||||
|
got := extractEpicPrefix(tt.epicID)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("extractEpicPrefix(%q) = %q, want %q", tt.epicID, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateBranchName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
branchName string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid simple branch",
|
||||||
|
branchName: "integration/gt-epic",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid nested branch",
|
||||||
|
branchName: "user/project/feature",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid with hyphens and underscores",
|
||||||
|
branchName: "user-name/feature_branch",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty branch name",
|
||||||
|
branchName: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains tilde",
|
||||||
|
branchName: "branch~1",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains caret",
|
||||||
|
branchName: "branch^2",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains colon",
|
||||||
|
branchName: "branch:ref",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains space",
|
||||||
|
branchName: "branch name",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains backslash",
|
||||||
|
branchName: "branch\\name",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains double dot",
|
||||||
|
branchName: "branch..name",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contains at-brace",
|
||||||
|
branchName: "branch@{name}",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ends with .lock",
|
||||||
|
branchName: "branch.lock",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "starts with slash",
|
||||||
|
branchName: "/branch",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ends with slash",
|
||||||
|
branchName: "branch/",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "starts with dot",
|
||||||
|
branchName: ".branch",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ends with dot",
|
||||||
|
branchName: "branch.",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "consecutive slashes",
|
||||||
|
branchName: "branch//name",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateBranchName(tt.branchName)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateBranchName(%q) error = %v, wantErr %v", tt.branchName, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIntegrationBranchField(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty description",
|
||||||
|
description: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field at beginning",
|
||||||
|
description: "integration_branch: klauern/PROJ-123/RA-epic\nSome description",
|
||||||
|
want: "klauern/PROJ-123/RA-epic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field in middle",
|
||||||
|
description: "Some text\nintegration_branch: custom/branch\nMore text",
|
||||||
|
want: "custom/branch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field with extra whitespace",
|
||||||
|
description: " integration_branch: spaced/branch \nOther content",
|
||||||
|
want: "spaced/branch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no integration_branch field",
|
||||||
|
description: "Just a plain description\nWith multiple lines",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed case field name",
|
||||||
|
description: "Integration_branch: CamelCase/branch",
|
||||||
|
want: "CamelCase/branch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default format",
|
||||||
|
description: "integration_branch: integration/gt-epic\nEpic for auth work",
|
||||||
|
want: "integration/gt-epic",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := getIntegrationBranchField(tt.description)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getIntegrationBranchField() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -605,6 +605,14 @@ type MergeQueueConfig struct {
|
|||||||
// IntegrationBranches enables integration branch workflow for epics.
|
// IntegrationBranches enables integration branch workflow for epics.
|
||||||
IntegrationBranches bool `json:"integration_branches"`
|
IntegrationBranches bool `json:"integration_branches"`
|
||||||
|
|
||||||
|
// IntegrationBranchTemplate is the pattern for integration branch names.
|
||||||
|
// Supports variables: {epic}, {prefix}, {user}
|
||||||
|
// - {epic}: Full epic ID (e.g., "RA-123")
|
||||||
|
// - {prefix}: Epic prefix before first hyphen (e.g., "RA")
|
||||||
|
// - {user}: Git user.name (e.g., "klauern")
|
||||||
|
// Default: "integration/{epic}"
|
||||||
|
IntegrationBranchTemplate string `json:"integration_branch_template,omitempty"`
|
||||||
|
|
||||||
// OnConflict specifies conflict resolution strategy: "assign_back" or "auto_rebase".
|
// OnConflict specifies conflict resolution strategy: "assign_back" or "auto_rebase".
|
||||||
OnConflict string `json:"on_conflict"`
|
OnConflict string `json:"on_conflict"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user