aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/git-balai/main.go213
1 files changed, 213 insertions, 0 deletions
diff --git a/cmd/git-balai/main.go b/cmd/git-balai/main.go
new file mode 100644
index 0000000..4de5cdb
--- /dev/null
+++ b/cmd/git-balai/main.go
@@ -0,0 +1,213 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "regexp"
+ "strings"
+)
+
+var preferredRemotes = []string{"origin", "github", "work"}
+
+type GitRepository struct {
+ WorkingDirectory string
+ RemoteName string
+ RemoteURL string
+ PrimaryBranch string
+}
+
+func NewGitRepository(workingDir string) (*GitRepository, error) {
+ repo := &GitRepository{
+ WorkingDirectory: workingDir,
+ }
+
+ if err := repo.isGitRepository(); err != nil {
+ return nil, err
+ }
+
+ if err := repo.workdirIsClean(); err != nil {
+ return nil, err
+ }
+
+ if err := repo.guessRemote(); err != nil {
+ return nil, err
+ }
+
+ if err := repo.guessPrimaryBranch(); err != nil {
+ return nil, err
+ }
+
+ return repo, nil
+}
+
+func (r *GitRepository) runGitCommand(args ...string) ([]byte, error) {
+ cmd := exec.Command("git", append([]string{"-C", r.WorkingDirectory}, args...)...)
+ return cmd.Output()
+}
+
+func (r *GitRepository) guessPrimaryBranch() error {
+ output, err := r.runGitCommand("ls-remote", "--symref", r.RemoteName, "HEAD")
+ if err != nil {
+ return fmt.Errorf("failed to run git ls-remote: %w", err)
+ }
+
+ re := regexp.MustCompile(`ref: refs/heads/(\S+)\s+HEAD`)
+ matches := re.FindStringSubmatch(string(output))
+
+ if len(matches) < 2 {
+ return fmt.Errorf("could not determine primary branch")
+ }
+
+ r.PrimaryBranch = matches[1]
+ return nil
+}
+
+func (r *GitRepository) guessRemote() error {
+ output, err := r.runGitCommand("config", "--get-regexp", `remote\.[a-z0-9]+\.url`)
+ if err != nil {
+ return fmt.Errorf("failed to run git config: %w", err)
+ }
+
+ remotes := make(map[string]string)
+ scanner := bufio.NewScanner(strings.NewReader(string(output)))
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ parts := strings.Fields(line)
+ if len(parts) < 2 {
+ continue
+ }
+
+ configParts := strings.Split(parts[0], ".")
+ if len(configParts) < 2 {
+ continue
+ }
+
+ remoteName := configParts[1]
+ remoteURL := parts[1]
+ remotes[remoteName] = remoteURL
+ }
+
+ for _, preferred := range preferredRemotes {
+ if url, exists := remotes[preferred]; exists {
+ r.RemoteName = preferred
+ r.RemoteURL = url
+ return nil
+ }
+ }
+
+ return fmt.Errorf("no preferred remote configured")
+}
+
+func (r *GitRepository) isGitRepository() error {
+ _, err := r.runGitCommand("rev-parse", "--show-toplevel")
+ if err != nil {
+ return fmt.Errorf("%s is not a git repository", r.WorkingDirectory)
+ }
+ return nil
+}
+
+func (r *GitRepository) workdirIsClean() error {
+ _, err := r.runGitCommand("diff", "--quiet", "--exit-code")
+ if err != nil {
+ return fmt.Errorf("%s has unstaged changes, commit or stash them first", r.WorkingDirectory)
+ }
+ return nil
+}
+
+// getMergedBranches returns a list of local branches that have been merged into the primary branch
+func (r *GitRepository) getMergedBranches() ([]string, error) {
+ output, err := r.runGitCommand("branch", "--merged", r.PrimaryBranch)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get merged branches: %w", err)
+ }
+
+ var branches []string
+ scanner := bufio.NewScanner(strings.NewReader(string(output)))
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ // Skip current branch indicator and the primary branch itself
+ if strings.HasPrefix(line, "*") {
+ line = strings.TrimSpace(line[1:])
+ }
+
+ // Don't delete the primary branch
+ if line != r.PrimaryBranch && line != "" {
+ branches = append(branches, line)
+ }
+ }
+
+ return branches, scanner.Err()
+}
+
+// deleteBranch deletes a local branch
+func (r *GitRepository) deleteBranch(branch string) error {
+ _, err := r.runGitCommand("branch", "-d", branch)
+ if err != nil {
+ // Try force delete if normal delete fails
+ _, forceErr := r.runGitCommand("branch", "-D", branch)
+ if forceErr != nil {
+ return fmt.Errorf("failed to delete branch %s: %w", branch, err)
+ }
+ log.Printf("Force deleted branch: %s", branch)
+ return nil
+ }
+ log.Printf("Deleted branch: %s", branch)
+ return nil
+}
+
+// cleanupMergedBranches deletes all local branches that have been merged into the primary branch
+func (r *GitRepository) cleanupMergedBranches(dryRun bool) error {
+ mergedBranches, err := r.getMergedBranches()
+ if err != nil {
+ return err
+ }
+
+ if len(mergedBranches) == 0 {
+ log.Println("No merged branches to clean up")
+ return nil
+ }
+
+ log.Printf("Found %d merged branches:", len(mergedBranches))
+ for _, branch := range mergedBranches {
+ if dryRun {
+ log.Printf("Would delete: %s", branch)
+ } else {
+ if err := r.deleteBranch(branch); err != nil {
+ log.Printf("Error deleting branch %s: %v", branch, err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func main() {
+ wd, err := os.Getwd()
+ if err != nil {
+ log.Fatalf("Failed to get working directory: %v", err)
+ }
+
+ repo, err := NewGitRepository(wd)
+ if err != nil {
+ log.Fatalf("Error: %v", err)
+ }
+
+ log.Printf("Working in: %s", repo.WorkingDirectory)
+ log.Printf("Main branch is: %s", repo.PrimaryBranch)
+ log.Printf("Remote is named %s and is at %s", repo.RemoteName, repo.RemoteURL)
+
+ dryRun := len(os.Args) > 1 && os.Args[1] == "--dry-run"
+
+ if dryRun {
+ log.Println("Running in dry-run mode")
+ }
+
+ if err := repo.cleanupMergedBranches(dryRun); err != nil {
+ log.Fatalf("Failed to cleanup merged branches: %v", err)
+ }
+}