feat: add MR ID generation for merge queue
Implement GenerateMRID(prefix, branch) to generate merge request IDs following the convention: <prefix>-mr-<hash> - Hash derived from branch name + timestamp + random bytes for uniqueness - Example output: gt-mr-abc123 - Includes deterministic variant for testing (GenerateMRIDWithTime) Closes: gt-h5n.2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
49
internal/mq/id.go
Normal file
49
internal/mq/id.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Package mq provides merge queue functionality.
|
||||||
|
package mq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateMRID generates a merge request ID following the convention: <prefix>-mr-<hash>
|
||||||
|
//
|
||||||
|
// The hash is derived from the branch name + current timestamp + random bytes to ensure uniqueness.
|
||||||
|
// Example: gt-mr-abc123 for a gastown merge request.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - prefix: The project prefix (e.g., "gt" for gastown)
|
||||||
|
// - branch: The source branch name (e.g., "polecat/Nux/gt-xyz")
|
||||||
|
//
|
||||||
|
// Returns a string in the format "<prefix>-mr-<6-char-hash>"
|
||||||
|
func GenerateMRID(prefix, branch string) string {
|
||||||
|
// Generate 8 random bytes for additional uniqueness
|
||||||
|
randomBytes := make([]byte, 8)
|
||||||
|
rand.Read(randomBytes)
|
||||||
|
|
||||||
|
return generateMRIDInternal(prefix, branch, time.Now(), randomBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMRIDWithTime generates a merge request ID using a specific timestamp.
|
||||||
|
// This is primarily useful for testing to ensure deterministic output.
|
||||||
|
// Note: Without randomness, two calls with identical inputs will produce the same ID.
|
||||||
|
func GenerateMRIDWithTime(prefix, branch string, timestamp time.Time) string {
|
||||||
|
return generateMRIDInternal(prefix, branch, timestamp, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMRIDInternal is the internal implementation that combines all inputs.
|
||||||
|
func generateMRIDInternal(prefix, branch string, timestamp time.Time, randomBytes []byte) string {
|
||||||
|
// Combine branch, timestamp, and optional random bytes for uniqueness
|
||||||
|
input := fmt.Sprintf("%s:%d:%x", branch, timestamp.UnixNano(), randomBytes)
|
||||||
|
|
||||||
|
// Generate SHA256 hash
|
||||||
|
hash := sha256.Sum256([]byte(input))
|
||||||
|
|
||||||
|
// Take first 6 characters of hex-encoded hash
|
||||||
|
hashStr := hex.EncodeToString(hash[:])[:6]
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s-mr-%s", prefix, hashStr)
|
||||||
|
}
|
||||||
143
internal/mq/id_test.go
Normal file
143
internal/mq/id_test.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package mq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateMRIDWithTime(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
prefix string
|
||||||
|
branch string
|
||||||
|
timestamp time.Time
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic gastown MR",
|
||||||
|
prefix: "gt",
|
||||||
|
branch: "polecat/Nux/gt-xyz",
|
||||||
|
timestamp: time.Date(2025, 12, 17, 10, 0, 0, 0, time.UTC),
|
||||||
|
want: "gt-mr-", // Will verify prefix, actual hash varies
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different prefix",
|
||||||
|
prefix: "hop",
|
||||||
|
branch: "feature/auth",
|
||||||
|
timestamp: time.Date(2025, 12, 17, 10, 0, 0, 0, time.UTC),
|
||||||
|
want: "hop-mr-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty prefix",
|
||||||
|
prefix: "",
|
||||||
|
branch: "main",
|
||||||
|
timestamp: time.Date(2025, 12, 17, 10, 0, 0, 0, time.UTC),
|
||||||
|
want: "-mr-",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := GenerateMRIDWithTime(tt.prefix, tt.branch, tt.timestamp)
|
||||||
|
|
||||||
|
// Verify prefix format
|
||||||
|
if !strings.HasPrefix(got, tt.want) {
|
||||||
|
t.Errorf("GenerateMRIDWithTime() = %q, want prefix %q", got, tt.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify total format: prefix-mr-XXXXXX (6 hex chars)
|
||||||
|
parts := strings.Split(got, "-mr-")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
t.Errorf("GenerateMRIDWithTime() = %q, expected format <prefix>-mr-<hash>", got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] != tt.prefix {
|
||||||
|
t.Errorf("GenerateMRIDWithTime() prefix = %q, want %q", parts[0], tt.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts[1]) != 6 {
|
||||||
|
t.Errorf("GenerateMRIDWithTime() hash length = %d, want 6", len(parts[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash is valid hex
|
||||||
|
for _, c := range parts[1] {
|
||||||
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
||||||
|
t.Errorf("GenerateMRIDWithTime() hash contains invalid hex char: %c", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMRIDWithTime_Deterministic(t *testing.T) {
|
||||||
|
// Same inputs should produce same output
|
||||||
|
prefix := "gt"
|
||||||
|
branch := "polecat/Nux/gt-xyz"
|
||||||
|
ts := time.Date(2025, 12, 17, 10, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
id1 := GenerateMRIDWithTime(prefix, branch, ts)
|
||||||
|
id2 := GenerateMRIDWithTime(prefix, branch, ts)
|
||||||
|
|
||||||
|
if id1 != id2 {
|
||||||
|
t.Errorf("Same inputs produced different outputs: %q != %q", id1, id2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMRIDWithTime_DifferentTimestamps(t *testing.T) {
|
||||||
|
// Different timestamps should produce different IDs
|
||||||
|
prefix := "gt"
|
||||||
|
branch := "polecat/Nux/gt-xyz"
|
||||||
|
ts1 := time.Date(2025, 12, 17, 10, 0, 0, 0, time.UTC)
|
||||||
|
ts2 := time.Date(2025, 12, 17, 10, 0, 0, 1, time.UTC) // 1 nanosecond later
|
||||||
|
|
||||||
|
id1 := GenerateMRIDWithTime(prefix, branch, ts1)
|
||||||
|
id2 := GenerateMRIDWithTime(prefix, branch, ts2)
|
||||||
|
|
||||||
|
if id1 == id2 {
|
||||||
|
t.Errorf("Different timestamps produced same ID: %q", id1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMRIDWithTime_DifferentBranches(t *testing.T) {
|
||||||
|
// Different branches should produce different IDs
|
||||||
|
prefix := "gt"
|
||||||
|
ts := time.Date(2025, 12, 17, 10, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
id1 := GenerateMRIDWithTime(prefix, "branch-a", ts)
|
||||||
|
id2 := GenerateMRIDWithTime(prefix, "branch-b", ts)
|
||||||
|
|
||||||
|
if id1 == id2 {
|
||||||
|
t.Errorf("Different branches produced same ID: %q", id1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMRID(t *testing.T) {
|
||||||
|
// GenerateMRID uses current time, so we just verify format
|
||||||
|
id := GenerateMRID("gt", "polecat/Nux/gt-xyz")
|
||||||
|
|
||||||
|
if !strings.HasPrefix(id, "gt-mr-") {
|
||||||
|
t.Errorf("GenerateMRID() = %q, want prefix gt-mr-", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(id, "-mr-")
|
||||||
|
if len(parts) != 2 || len(parts[1]) != 6 {
|
||||||
|
t.Errorf("GenerateMRID() = %q, invalid format", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMRID_Uniqueness(t *testing.T) {
|
||||||
|
// Generate multiple IDs and verify they're unique
|
||||||
|
ids := make(map[string]bool)
|
||||||
|
prefix := "gt"
|
||||||
|
branch := "test-branch"
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
id := GenerateMRID(prefix, branch)
|
||||||
|
if ids[id] {
|
||||||
|
t.Errorf("Duplicate ID generated: %q", id)
|
||||||
|
}
|
||||||
|
ids[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user