summaryrefslogtreecommitdiff
path: root/internal/sync
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-12 13:51:48 +0300
committerPaul Buetow <paul@buetow.org>2025-07-12 13:51:48 +0300
commitceb164c6a8826db1e763aecca49098a8c2584b7f (patch)
treea228040641666f4a6567e2d2e4544fd7daecc2e7 /internal/sync
parent9d171e6b30a589240a16d8fef15e2195189e52a9 (diff)
feat: add automatic release checking and creation
- Add --check-releases flag for manual release checking - Enable automatic release checking after sync operations by default - Add --no-check-releases flag to disable automatic checking - Add --auto-create-releases flag for unattended release creation - Generate release notes from commit history - Support version tag formats: vX.Y.Z, vX.Y, vX, X.Y.Z, X.Y, X - Use tokens from gitsyncer config (with fallback to env vars and files) - Show release notes preview before creating releases - Group commits by type (features, fixes, other) in release notes
Diffstat (limited to 'internal/sync')
-rw-r--r--internal/sync/branch_analyzer.go401
1 files changed, 384 insertions, 17 deletions
diff --git a/internal/sync/branch_analyzer.go b/internal/sync/branch_analyzer.go
index f684f60..95827db 100644
--- a/internal/sync/branch_analyzer.go
+++ b/internal/sync/branch_analyzer.go
@@ -2,7 +2,9 @@ package sync
import (
"fmt"
+ "os"
"os/exec"
+ "path/filepath"
"strconv"
"strings"
"time"
@@ -15,6 +17,7 @@ type BranchInfo struct {
Remote string
IsAbandoned bool
AbandonReason string
+ RemotesWithBranch []string // List of remotes that have this branch
}
// AbandonedBranchReport holds the analysis results
@@ -22,13 +25,16 @@ type AbandonedBranchReport struct {
MainBranchUpdated bool
MainBranchLastCommit time.Time
AbandonedBranches []BranchInfo
+ AbandonedIgnoredBranches []BranchInfo // Abandoned branches that match exclusion patterns
TotalBranches int
+ TotalIgnoredBranches int
}
// analyzeAbandonedBranches analyzes branches to find abandoned ones
func (s *Syncer) analyzeAbandonedBranches() (*AbandonedBranchReport, error) {
report := &AbandonedBranchReport{
AbandonedBranches: []BranchInfo{},
+ AbandonedIgnoredBranches: []BranchInfo{},
}
// Get all branches
@@ -40,6 +46,10 @@ func (s *Syncer) analyzeAbandonedBranches() (*AbandonedBranchReport, error) {
// Filter branches based on exclusion patterns
branches := s.branchFilter.FilterBranches(allBranches)
report.TotalBranches = len(branches)
+
+ // Get excluded branches for separate analysis
+ excludedBranches := s.branchFilter.GetExcludedBranches(allBranches)
+ report.TotalIgnoredBranches = len(excludedBranches)
// Check main/master branch status
mainBranch := s.findMainBranch(branches)
@@ -79,6 +89,27 @@ func (s *Syncer) analyzeAbandonedBranches() (*AbandonedBranchReport, error) {
report.AbandonedBranches = append(report.AbandonedBranches, *branchInfo)
}
}
+
+ // Also analyze ignored branches for abandonment
+ for _, branch := range excludedBranches {
+ // Skip main/master branches even if they match exclusion patterns
+ if branch == "main" || branch == "master" {
+ continue
+ }
+
+ branchInfo, err := s.getBranchInfo(branch)
+ if err != nil {
+ continue
+ }
+
+ // Check if branch is abandoned (no commits for 6+ months)
+ if branchInfo.LastCommit.Before(sixMonthsAgo) {
+ branchInfo.IsAbandoned = true
+ daysSinceCommit := int(time.Since(branchInfo.LastCommit).Hours() / 24)
+ branchInfo.AbandonReason = fmt.Sprintf("No commits for %d days (ignored branch)", daysSinceCommit)
+ report.AbandonedIgnoredBranches = append(report.AbandonedIgnoredBranches, *branchInfo)
+ }
+ }
return report, nil
}
@@ -97,6 +128,7 @@ func (s *Syncer) findMainBranch(branches []string) string {
func (s *Syncer) getBranchInfo(branch string) (*BranchInfo, error) {
info := &BranchInfo{
Name: branch,
+ RemotesWithBranch: []string{},
}
// Find which remote has this branch and get the latest commit
@@ -105,9 +137,18 @@ func (s *Syncer) getBranchInfo(branch string) (*BranchInfo, error) {
for i := range s.config.Organizations {
org := &s.config.Organizations[i]
+
+ // Skip backup locations if backup is not enabled
+ if org.BackupLocation && !s.backupEnabled {
+ continue
+ }
+
remoteName := s.getRemoteName(org)
if s.remoteBranchExists(remoteName, branch) {
+ // Add this remote to the list
+ info.RemotesWithBranch = append(info.RemotesWithBranch, remoteName)
+
// Get last commit date for this branch on this remote
commitTime, err := s.getLastCommitTime(remoteName, branch)
if err == nil && (latestCommit.IsZero() || commitTime.After(latestCommit)) {
@@ -163,20 +204,32 @@ func formatAbandonedBranchReport(report *AbandonedBranchReport, repoName string)
return "" // Don't report on inactive repos
}
- if len(report.AbandonedBranches) == 0 {
+ if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
return "" // No abandoned branches
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("\nšŸ” Abandoned branches in %s:\n", repoName))
sb.WriteString(fmt.Sprintf(" Main branch last updated: %s\n", report.MainBranchLastCommit.Format("2006-01-02")))
- sb.WriteString(fmt.Sprintf(" Found %d abandoned branches (no commits for 6+ months):\n\n", len(report.AbandonedBranches)))
-
- for _, branch := range report.AbandonedBranches {
- sb.WriteString(fmt.Sprintf(" - %s (last commit: %s, %s)\n",
- branch.Name,
- branch.LastCommit.Format("2006-01-02"),
- branch.AbandonReason))
+
+ if len(report.AbandonedBranches) > 0 {
+ sb.WriteString(fmt.Sprintf(" Found %d abandoned branches (no commits for 6+ months):\n\n", len(report.AbandonedBranches)))
+ for _, branch := range report.AbandonedBranches {
+ sb.WriteString(fmt.Sprintf(" - %s (last commit: %s, %s)\n",
+ branch.Name,
+ branch.LastCommit.Format("2006-01-02"),
+ branch.AbandonReason))
+ }
+ }
+
+ if len(report.AbandonedIgnoredBranches) > 0 {
+ sb.WriteString(fmt.Sprintf("\n Found %d abandoned IGNORED branches (no commits for 6+ months):\n\n", len(report.AbandonedIgnoredBranches)))
+ for _, branch := range report.AbandonedIgnoredBranches {
+ sb.WriteString(fmt.Sprintf(" - %s (last commit: %s, %s)\n",
+ branch.Name,
+ branch.LastCommit.Format("2006-01-02"),
+ branch.AbandonReason))
+ }
}
return sb.String()
@@ -189,16 +242,18 @@ func (s *Syncer) GenerateAbandonedBranchSummary() string {
}
totalAbandoned := 0
+ totalAbandonedIgnored := 0
reposWithAbandoned := 0
for _, report := range s.abandonedReports {
- if len(report.AbandonedBranches) > 0 {
+ if len(report.AbandonedBranches) > 0 || len(report.AbandonedIgnoredBranches) > 0 {
totalAbandoned += len(report.AbandonedBranches)
+ totalAbandonedIgnored += len(report.AbandonedIgnoredBranches)
reposWithAbandoned++
}
}
- if totalAbandoned == 0 {
+ if totalAbandoned == 0 && totalAbandonedIgnored == 0 {
return ""
}
@@ -208,20 +263,41 @@ func (s *Syncer) GenerateAbandonedBranchSummary() string {
sb.WriteString("\nšŸ“Š ABANDONED BRANCHES SUMMARY\n")
sb.WriteString(strings.Repeat("=", 70))
sb.WriteString("\n\n")
- sb.WriteString(fmt.Sprintf("Found %d abandoned branches across %d repositories\n\n", totalAbandoned, reposWithAbandoned))
+ sb.WriteString(fmt.Sprintf("Found %d abandoned branches", totalAbandoned))
+ if totalAbandonedIgnored > 0 {
+ sb.WriteString(fmt.Sprintf(" + %d ignored branches", totalAbandonedIgnored))
+ }
+ sb.WriteString(fmt.Sprintf(" across %d repositories\n\n", reposWithAbandoned))
// Group by repository
for repoName, report := range s.abandonedReports {
- if len(report.AbandonedBranches) == 0 {
+ if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
continue
}
- sb.WriteString(fmt.Sprintf("šŸ“ %s (%d branches):\n", repoName, len(report.AbandonedBranches)))
- for _, branch := range report.AbandonedBranches {
- sb.WriteString(fmt.Sprintf(" - %s (last commit: %s)\n",
- branch.Name,
- branch.LastCommit.Format("2006-01-02")))
+ totalBranches := len(report.AbandonedBranches) + len(report.AbandonedIgnoredBranches)
+ sb.WriteString(fmt.Sprintf("šŸ“ %s (%d branches):\n", repoName, totalBranches))
+
+ // Regular abandoned branches
+ if len(report.AbandonedBranches) > 0 {
+ sb.WriteString(" Regular branches:\n")
+ for _, branch := range report.AbandonedBranches {
+ sb.WriteString(fmt.Sprintf(" - %s (last commit: %s)\n",
+ branch.Name,
+ branch.LastCommit.Format("2006-01-02")))
+ }
+ }
+
+ // Ignored abandoned branches
+ if len(report.AbandonedIgnoredBranches) > 0 {
+ sb.WriteString(" Ignored branches:\n")
+ for _, branch := range report.AbandonedIgnoredBranches {
+ sb.WriteString(fmt.Sprintf(" - %s (last commit: %s)\n",
+ branch.Name,
+ branch.LastCommit.Format("2006-01-02")))
+ }
}
+
sb.WriteString("\n")
}
@@ -231,4 +307,295 @@ func (s *Syncer) GenerateAbandonedBranchSummary() string {
sb.WriteString("\n")
return sb.String()
+}
+
+// GenerateDeleteCommands generates shell commands to delete abandoned branches
+func (s *Syncer) GenerateDeleteCommands(report *AbandonedBranchReport, repoName string) string {
+ if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
+ return ""
+ }
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("\n# Delete commands for abandoned branches in %s\n", repoName))
+ sb.WriteString("# Review these commands carefully before running them!\n\n")
+
+ // Process regular abandoned branches
+ if len(report.AbandonedBranches) > 0 {
+ sb.WriteString("# === REGULAR BRANCHES ===\n")
+ for _, branch := range report.AbandonedBranches {
+ sb.WriteString(fmt.Sprintf("# Branch: %s (last commit: %s)\n", branch.Name, branch.LastCommit.Format("2006-01-02")))
+
+ // Delete from all remotes that have this branch
+ if len(branch.RemotesWithBranch) > 0 {
+ sb.WriteString("# Delete from remotes:\n")
+ for _, remote := range branch.RemotesWithBranch {
+ sb.WriteString(fmt.Sprintf("git push %s --delete %s\n", remote, branch.Name))
+ }
+ }
+
+ // Delete local branch
+ sb.WriteString("# Delete local branch:\n")
+ sb.WriteString(fmt.Sprintf("git branch -D %s\n\n", branch.Name))
+ }
+ }
+
+ // Process ignored abandoned branches
+ if len(report.AbandonedIgnoredBranches) > 0 {
+ sb.WriteString("# === IGNORED BRANCHES ===\n")
+ for _, branch := range report.AbandonedIgnoredBranches {
+ sb.WriteString(fmt.Sprintf("# Branch: %s (last commit: %s) [IGNORED]\n", branch.Name, branch.LastCommit.Format("2006-01-02")))
+
+ // Delete from all remotes that have this branch
+ if len(branch.RemotesWithBranch) > 0 {
+ sb.WriteString("# Delete from remotes:\n")
+ for _, remote := range branch.RemotesWithBranch {
+ sb.WriteString(fmt.Sprintf("git push %s --delete %s\n", remote, branch.Name))
+ }
+ }
+
+ // Delete local branch
+ sb.WriteString("# Delete local branch:\n")
+ sb.WriteString(fmt.Sprintf("git branch -D %s\n\n", branch.Name))
+ }
+ }
+
+ return sb.String()
+}
+
+// GenerateDeleteScript generates a shell script file to delete all abandoned branches
+func (s *Syncer) GenerateDeleteScript() (string, error) {
+ if len(s.abandonedReports) == 0 {
+ return "", nil
+ }
+
+ // Count total abandoned branches
+ totalAbandoned := 0
+ totalIgnored := 0
+ for _, report := range s.abandonedReports {
+ totalAbandoned += len(report.AbandonedBranches)
+ totalIgnored += len(report.AbandonedIgnoredBranches)
+ }
+
+ if totalAbandoned == 0 && totalIgnored == 0 {
+ return "", nil
+ }
+
+ // Generate script filename with timestamp
+ timestamp := time.Now().Format("20060102_150405")
+ scriptPath := filepath.Join(s.workDir, fmt.Sprintf("delete_abandoned_branches_%s.sh", timestamp))
+
+ // Create the script file
+ file, err := os.Create(scriptPath)
+ if err != nil {
+ return "", fmt.Errorf("failed to create script file: %w", err)
+ }
+ defer file.Close()
+
+ // Write script header
+ fmt.Fprintf(file, "#!/bin/bash\n")
+ fmt.Fprintf(file, "# Gitsyncer - Delete Abandoned Branches Script\n")
+ fmt.Fprintf(file, "# Generated on: %s\n", time.Now().Format("2006-01-02 15:04:05"))
+ fmt.Fprintf(file, "# Total branches to delete: %d regular + %d ignored = %d total\n", totalAbandoned, totalIgnored, totalAbandoned+totalIgnored)
+ fmt.Fprintf(file, "#\n")
+ fmt.Fprintf(file, "# āš ļø WARNING: This script will permanently delete branches!\n")
+ fmt.Fprintf(file, "# Review carefully before executing.\n")
+ fmt.Fprintf(file, "#\n")
+ fmt.Fprintf(file, "# Usage:\n")
+ fmt.Fprintf(file, "# bash %s # Delete branches (with confirmation)\n", filepath.Base(scriptPath))
+ fmt.Fprintf(file, "# bash %s --dry-run # Preview what will be deleted\n", filepath.Base(scriptPath))
+ fmt.Fprintf(file, "# bash %s --review # Review diffs before deletion\n", filepath.Base(scriptPath))
+ fmt.Fprintf(file, "# bash %s --review-full # Review full diffs\n", filepath.Base(scriptPath))
+ fmt.Fprintf(file, "\n")
+
+ // Add mode detection
+ fmt.Fprintf(file, "# Parse command line arguments\n")
+ fmt.Fprintf(file, "MODE=\"delete\"\n")
+ fmt.Fprintf(file, "if [[ \"$1\" == \"--dry-run\" ]]; then\n")
+ fmt.Fprintf(file, " MODE=\"dry-run\"\n")
+ fmt.Fprintf(file, "elif [[ \"$1\" == \"--review\" ]]; then\n")
+ fmt.Fprintf(file, " MODE=\"review\"\n")
+ fmt.Fprintf(file, "elif [[ \"$1\" == \"--review-full\" ]]; then\n")
+ fmt.Fprintf(file, " MODE=\"review-full\"\n")
+ fmt.Fprintf(file, "fi\n\n")
+
+ // Add color support for review mode
+ fmt.Fprintf(file, "# Color codes for better readability\n")
+ fmt.Fprintf(file, "RED='\\033[0;31m'\n")
+ fmt.Fprintf(file, "GREEN='\\033[0;32m'\n")
+ fmt.Fprintf(file, "YELLOW='\\033[0;33m'\n")
+ fmt.Fprintf(file, "BLUE='\\033[0;34m'\n")
+ fmt.Fprintf(file, "PURPLE='\\033[0;35m'\n")
+ fmt.Fprintf(file, "CYAN='\\033[0;36m'\n")
+ fmt.Fprintf(file, "NC='\\033[0m' # No Color\n\n")
+
+ // Add helper functions
+ fmt.Fprintf(file, "# Helper function to execute or print commands\n")
+ fmt.Fprintf(file, "execute_cmd() {\n")
+ fmt.Fprintf(file, " if [[ \"$MODE\" == \"dry-run\" ]]; then\n")
+ fmt.Fprintf(file, " echo \" [DRY RUN] $@\"\n")
+ fmt.Fprintf(file, " else\n")
+ fmt.Fprintf(file, " echo \" Executing: $@\"\n")
+ fmt.Fprintf(file, " \"$@\"\n")
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, "}\n\n")
+
+ // Add function to find main branch
+ fmt.Fprintf(file, "# Function to find main/master branch\n")
+ fmt.Fprintf(file, "find_main_branch() {\n")
+ fmt.Fprintf(file, " if git rev-parse --verify main >/dev/null 2>&1; then\n")
+ fmt.Fprintf(file, " echo \"main\"\n")
+ fmt.Fprintf(file, " elif git rev-parse --verify master >/dev/null 2>&1; then\n")
+ fmt.Fprintf(file, " echo \"master\"\n")
+ fmt.Fprintf(file, " else\n")
+ fmt.Fprintf(file, " echo \"\"\n")
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, "}\n\n")
+
+ // Add review function
+ fmt.Fprintf(file, "# Function to review branch diff\n")
+ fmt.Fprintf(file, "review_branch() {\n")
+ fmt.Fprintf(file, " local branch=\"$1\"\n")
+ fmt.Fprintf(file, " local main_branch=\"$2\"\n")
+ fmt.Fprintf(file, " local last_commit=\"$3\"\n")
+ fmt.Fprintf(file, " local branch_type=\"$4\"\n")
+ fmt.Fprintf(file, " \n")
+ fmt.Fprintf(file, " echo -e \"${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n")
+ fmt.Fprintf(file, " echo -e \"${YELLOW}Branch:${NC} $branch ${PURPLE}[$branch_type]${NC}\"\n")
+ fmt.Fprintf(file, " echo -e \"${YELLOW}Last commit:${NC} $last_commit\"\n")
+ fmt.Fprintf(file, " echo -e \"${YELLOW}Comparing against:${NC} $main_branch\"\n")
+ fmt.Fprintf(file, " echo -e \"${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n")
+ fmt.Fprintf(file, " \n")
+ fmt.Fprintf(file, " # Check if branch exists locally\n")
+ fmt.Fprintf(file, " if ! git rev-parse --verify \"$branch\" >/dev/null 2>&1; then\n")
+ fmt.Fprintf(file, " echo -e \"${RED}āš ļø Branch '$branch' not found locally${NC}\"\n")
+ fmt.Fprintf(file, " return\n")
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, " \n")
+ fmt.Fprintf(file, " echo -e \"${GREEN}šŸ“Š Diff statistics:${NC}\"\n")
+ fmt.Fprintf(file, " git diff --stat \"$main_branch\"...\"$branch\"\n")
+ fmt.Fprintf(file, " echo\n")
+ fmt.Fprintf(file, " echo -e \"${GREEN}šŸ“ Commits in this branch:${NC}\"\n")
+ fmt.Fprintf(file, " git log --oneline --graph \"$main_branch\"..\"$branch\" | head -20\n")
+ fmt.Fprintf(file, " \n")
+ fmt.Fprintf(file, " if [[ \"$MODE\" == \"review-full\" ]]; then\n")
+ fmt.Fprintf(file, " echo\n")
+ fmt.Fprintf(file, " echo -e \"${GREEN}šŸ” Full diff:${NC}\"\n")
+ fmt.Fprintf(file, " git diff \"$main_branch\"...\"$branch\"\n")
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, " echo\n")
+ fmt.Fprintf(file, "}\n\n")
+
+ // Start main logic
+ fmt.Fprintf(file, "# Main script logic\n")
+ fmt.Fprintf(file, "case \"$MODE\" in\n")
+ fmt.Fprintf(file, " \"dry-run\")\n")
+ fmt.Fprintf(file, " echo \"šŸ” DRY RUN MODE - No branches will be deleted\"\n")
+ fmt.Fprintf(file, " echo\n")
+ fmt.Fprintf(file, " ;;\n")
+ fmt.Fprintf(file, " \"review\"|\"review-full\")\n")
+ fmt.Fprintf(file, " echo -e \"${CYAN}šŸ” Gitsyncer - Abandoned Branch Review${NC}\"\n")
+ fmt.Fprintf(file, " echo -e \"${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n")
+ fmt.Fprintf(file, " echo -e \"Found ${YELLOW}%d${NC} abandoned branches to review\"\n", totalAbandoned+totalIgnored)
+ fmt.Fprintf(file, " echo\n")
+ fmt.Fprintf(file, " ;;\n")
+ fmt.Fprintf(file, " \"delete\")\n")
+ fmt.Fprintf(file, " echo \"āš ļø This script will delete %d abandoned branches across %d repositories.\"\n", totalAbandoned+totalIgnored, len(s.abandonedReports))
+ fmt.Fprintf(file, " read -p \"Are you sure you want to continue? (yes/no): \" confirm\n")
+ fmt.Fprintf(file, " if [[ \"$confirm\" != \"yes\" ]]; then\n")
+ fmt.Fprintf(file, " echo \"Aborted.\"\n")
+ fmt.Fprintf(file, " exit 0\n")
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, " echo\n")
+ fmt.Fprintf(file, " ;;\n")
+ fmt.Fprintf(file, "esac\n\n")
+
+ // Process each repository
+ for repoName, report := range s.abandonedReports {
+ if len(report.AbandonedBranches) == 0 && len(report.AbandonedIgnoredBranches) == 0 {
+ continue
+ }
+
+ fmt.Fprintf(file, "# ======================================\n")
+ fmt.Fprintf(file, "# Repository: %s\n", repoName)
+ fmt.Fprintf(file, "# ======================================\n")
+ fmt.Fprintf(file, "echo\n")
+ fmt.Fprintf(file, "echo \"šŸ“ Processing repository: %s\"\n", repoName)
+ fmt.Fprintf(file, "cd \"%s/%s\" || { echo \"Failed to change to repository directory\"; exit 1; }\n\n", s.workDir, repoName)
+
+ // Find main branch for review mode
+ fmt.Fprintf(file, "if [[ \"$MODE\" == \"review\" || \"$MODE\" == \"review-full\" ]]; then\n")
+ fmt.Fprintf(file, " main_branch=$(find_main_branch)\n")
+ fmt.Fprintf(file, " if [[ -z \"$main_branch\" ]]; then\n")
+ fmt.Fprintf(file, " echo -e \"${RED}āš ļø No main/master branch found in %s${NC}\"\n", repoName)
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, "fi\n\n")
+
+ // Process regular abandoned branches
+ if len(report.AbandonedBranches) > 0 {
+ fmt.Fprintf(file, "# Regular abandoned branches\n")
+ for _, branch := range report.AbandonedBranches {
+ fmt.Fprintf(file, "if [[ \"$MODE\" == \"review\" || \"$MODE\" == \"review-full\" ]]; then\n")
+ fmt.Fprintf(file, " if [[ -n \"$main_branch\" ]]; then\n")
+ fmt.Fprintf(file, " review_branch \"%s\" \"$main_branch\" \"%s\" \"regular\"\n", branch.Name, branch.LastCommit.Format("2006-01-02"))
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, "else\n")
+ fmt.Fprintf(file, " echo \" šŸ”ø Deleting branch: %s (last commit: %s)\"\n", branch.Name, branch.LastCommit.Format("2006-01-02"))
+
+ // Delete from remotes
+ for _, remote := range branch.RemotesWithBranch {
+ fmt.Fprintf(file, " execute_cmd git push %s --delete \"%s\"\n", remote, branch.Name)
+ }
+
+ // Delete local branch
+ fmt.Fprintf(file, " execute_cmd git branch -D \"%s\"\n", branch.Name)
+ fmt.Fprintf(file, "fi\n\n")
+ }
+ }
+
+ // Process ignored abandoned branches
+ if len(report.AbandonedIgnoredBranches) > 0 {
+ fmt.Fprintf(file, "# Ignored abandoned branches\n")
+ for _, branch := range report.AbandonedIgnoredBranches {
+ fmt.Fprintf(file, "if [[ \"$MODE\" == \"review\" || \"$MODE\" == \"review-full\" ]]; then\n")
+ fmt.Fprintf(file, " if [[ -n \"$main_branch\" ]]; then\n")
+ fmt.Fprintf(file, " review_branch \"%s\" \"$main_branch\" \"%s\" \"ignored\"\n", branch.Name, branch.LastCommit.Format("2006-01-02"))
+ fmt.Fprintf(file, " fi\n")
+ fmt.Fprintf(file, "else\n")
+ fmt.Fprintf(file, " echo \" šŸ”¹ Deleting ignored branch: %s (last commit: %s)\"\n", branch.Name, branch.LastCommit.Format("2006-01-02"))
+
+ // Delete from remotes
+ for _, remote := range branch.RemotesWithBranch {
+ fmt.Fprintf(file, " execute_cmd git push %s --delete \"%s\"\n", remote, branch.Name)
+ }
+
+ // Delete local branch
+ fmt.Fprintf(file, " execute_cmd git branch -D \"%s\"\n", branch.Name)
+ fmt.Fprintf(file, "fi\n\n")
+ }
+ }
+ }
+
+ // Add completion message
+ fmt.Fprintf(file, "echo\n")
+ fmt.Fprintf(file, "echo \"āœ… Script completed!\"\n")
+ fmt.Fprintf(file, "case \"$MODE\" in\n")
+ fmt.Fprintf(file, " \"dry-run\")\n")
+ fmt.Fprintf(file, " echo \"This was a dry run. No branches were deleted.\"\n")
+ fmt.Fprintf(file, " echo \"To actually delete branches, run: bash %s\"\n", filepath.Base(scriptPath))
+ fmt.Fprintf(file, " ;;\n")
+ fmt.Fprintf(file, " \"review\"|\"review-full\")\n")
+ fmt.Fprintf(file, " echo \"Review completed. No branches were deleted.\"\n")
+ fmt.Fprintf(file, " echo \"To delete branches, run: bash %s\"\n", filepath.Base(scriptPath))
+ fmt.Fprintf(file, " ;;\n")
+ fmt.Fprintf(file, " \"delete\")\n")
+ fmt.Fprintf(file, " echo \"All abandoned branches have been deleted.\"\n")
+ fmt.Fprintf(file, " ;;\n")
+ fmt.Fprintf(file, "esac\n")
+
+ // Make the script executable
+ if err := os.Chmod(scriptPath, 0755); err != nil {
+ return scriptPath, fmt.Errorf("failed to make script executable: %w", err)
+ }
+
+ return scriptPath, nil
} \ No newline at end of file