summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/sync/branch_analyzer.go56
-rw-r--r--internal/sync/branch_analyzer_test.go89
2 files changed, 142 insertions, 3 deletions
diff --git a/internal/sync/branch_analyzer.go b/internal/sync/branch_analyzer.go
index 04a60d7..6c07f77 100644
--- a/internal/sync/branch_analyzer.go
+++ b/internal/sync/branch_analyzer.go
@@ -29,6 +29,50 @@ type AbandonedBranchReport struct {
TotalIgnoredBranches int
}
+var defaultAutoDeleteProtectedBranches = map[string]map[string]struct{}{
+ "xerl": {
+ "hosts": {},
+ },
+}
+
+func isProtectedFromAutoDelete(repoName, branchName string) bool {
+ branches, ok := defaultAutoDeleteProtectedBranches[repoName]
+ if !ok {
+ return false
+ }
+
+ _, ok = branches[branchName]
+ return ok
+}
+
+func filterProtectedBranchInfos(repoName string, branches []BranchInfo) []BranchInfo {
+ if len(branches) == 0 {
+ return branches
+ }
+
+ filtered := make([]BranchInfo, 0, len(branches))
+ for _, branch := range branches {
+ if isProtectedFromAutoDelete(repoName, branch.Name) {
+ continue
+ }
+ filtered = append(filtered, branch)
+ }
+
+ return filtered
+}
+
+func filterProtectedAbandonedBranchReport(repoName string, report *AbandonedBranchReport) *AbandonedBranchReport {
+ if report == nil {
+ return nil
+ }
+
+ filtered := *report
+ filtered.AbandonedBranches = filterProtectedBranchInfos(repoName, report.AbandonedBranches)
+ filtered.AbandonedIgnoredBranches = filterProtectedBranchInfos(repoName, report.AbandonedIgnoredBranches)
+
+ return &filtered
+}
+
// analyzeAbandonedBranches analyzes branches to find abandoned ones
func (s *Syncer) analyzeAbandonedBranches() (*AbandonedBranchReport, error) {
report := &AbandonedBranchReport{
@@ -110,7 +154,7 @@ func (s *Syncer) analyzeAbandonedBranches() (*AbandonedBranchReport, error) {
}
}
- return report, nil
+ return filterProtectedAbandonedBranchReport(s.repoName, report), nil
}
// findMainBranch finds the main or master branch
@@ -199,6 +243,7 @@ func (s *Syncer) getLastCommitTime(remoteName, branch string) (time.Time, error)
// formatAbandonedBranchReport formats the report for display
func formatAbandonedBranchReport(report *AbandonedBranchReport, repoName string) string {
+ report = filterProtectedAbandonedBranchReport(repoName, report)
if !report.MainBranchUpdated {
return "" // Don't report on inactive repos
}
@@ -244,7 +289,8 @@ func (s *Syncer) GenerateAbandonedBranchSummary() string {
totalAbandonedIgnored := 0
reposWithAbandoned := 0
- for _, report := range s.abandonedReports {
+ for repoName, report := range s.abandonedReports {
+ report = filterProtectedAbandonedBranchReport(repoName, report)
if len(report.AbandonedBranches) > 0 || len(report.AbandonedIgnoredBranches) > 0 {
totalAbandoned += len(report.AbandonedBranches)
totalAbandonedIgnored += len(report.AbandonedIgnoredBranches)
@@ -270,6 +316,7 @@ func (s *Syncer) GenerateAbandonedBranchSummary() string {
// Group by repository
for repoName, report := range s.abandonedReports {
+ report = filterProtectedAbandonedBranchReport(repoName, report)
if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
continue
}
@@ -310,6 +357,7 @@ func (s *Syncer) GenerateAbandonedBranchSummary() string {
// GenerateDeleteCommands generates shell commands to delete abandoned branches
func (s *Syncer) GenerateDeleteCommands(report *AbandonedBranchReport, repoName string) string {
+ report = filterProtectedAbandonedBranchReport(repoName, report)
if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
return ""
}
@@ -370,7 +418,8 @@ func (s *Syncer) GenerateDeleteScript() (string, error) {
// Count total abandoned branches
totalAbandoned := 0
totalIgnored := 0
- for _, report := range s.abandonedReports {
+ for repoName, report := range s.abandonedReports {
+ report = filterProtectedAbandonedBranchReport(repoName, report)
totalAbandoned += len(report.AbandonedBranches)
totalIgnored += len(report.AbandonedIgnoredBranches)
}
@@ -510,6 +559,7 @@ func (s *Syncer) GenerateDeleteScript() (string, error) {
// Process each repository
for repoName, report := range s.abandonedReports {
+ report = filterProtectedAbandonedBranchReport(repoName, report)
if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
continue
}
diff --git a/internal/sync/branch_analyzer_test.go b/internal/sync/branch_analyzer_test.go
new file mode 100644
index 0000000..2a51bc7
--- /dev/null
+++ b/internal/sync/branch_analyzer_test.go
@@ -0,0 +1,89 @@
+package sync
+
+import (
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestFilterProtectedAbandonedBranchReport_SkipsProtectedBranches(t *testing.T) {
+ report := &AbandonedBranchReport{
+ AbandonedBranches: []BranchInfo{
+ {Name: "hosts"},
+ {Name: "feature/still-delete"},
+ },
+ AbandonedIgnoredBranches: []BranchInfo{
+ {Name: "hosts"},
+ {Name: "ignored/still-delete"},
+ },
+ }
+
+ filtered := filterProtectedAbandonedBranchReport("xerl", report)
+
+ if len(filtered.AbandonedBranches) != 1 || filtered.AbandonedBranches[0].Name != "feature/still-delete" {
+ t.Fatalf("expected protected abandoned branch to be filtered, got %#v", filtered.AbandonedBranches)
+ }
+
+ if len(filtered.AbandonedIgnoredBranches) != 1 || filtered.AbandonedIgnoredBranches[0].Name != "ignored/still-delete" {
+ t.Fatalf("expected protected ignored branch to be filtered, got %#v", filtered.AbandonedIgnoredBranches)
+ }
+
+ if len(report.AbandonedBranches) != 2 || len(report.AbandonedIgnoredBranches) != 2 {
+ t.Fatalf("expected original report to remain unchanged, got %#v", report)
+ }
+}
+
+func TestGenerateDeleteCommands_SkipsProtectedXerlHostsBranchOnly(t *testing.T) {
+ syncer := &Syncer{}
+ report := &AbandonedBranchReport{
+ AbandonedBranches: []BranchInfo{
+ {
+ Name: "hosts",
+ LastCommit: time.Date(2024, time.January, 2, 0, 0, 0, 0, time.UTC),
+ RemotesWithBranch: []string{"origin"},
+ },
+ {
+ Name: "feature/still-delete",
+ LastCommit: time.Date(2024, time.January, 3, 0, 0, 0, 0, time.UTC),
+ RemotesWithBranch: []string{"origin"},
+ },
+ },
+ }
+
+ commands := syncer.GenerateDeleteCommands(report, "xerl")
+
+ if strings.Contains(commands, "hosts") {
+ t.Fatalf("expected protected branch to be omitted from delete commands, got %q", commands)
+ }
+
+ if !strings.Contains(commands, "feature/still-delete") {
+ t.Fatalf("expected non-protected branch to remain in delete commands, got %q", commands)
+ }
+}
+
+func TestGenerateDeleteScript_ReturnsEmptyWhenOnlyProtectedBranchesRemain(t *testing.T) {
+ syncer := &Syncer{
+ workDir: t.TempDir(),
+ abandonedReports: map[string]*AbandonedBranchReport{
+ "xerl": {
+ MainBranchUpdated: true,
+ AbandonedBranches: []BranchInfo{
+ {
+ Name: "hosts",
+ LastCommit: time.Date(2024, time.January, 2, 0, 0, 0, 0, time.UTC),
+ RemotesWithBranch: []string{"origin"},
+ },
+ },
+ },
+ },
+ }
+
+ scriptPath, err := syncer.GenerateDeleteScript()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if scriptPath != "" {
+ t.Fatalf("expected no delete script for protected branches, got %q", scriptPath)
+ }
+}