From 2dd0bc6b2b118df876ece5a09df33f9044e16f23 Mon Sep 17 00:00:00 2001 From: Eric Hodel Date: Sun, 3 Dec 2023 14:25:07 -0800 Subject: [PATCH] Improve git branch cleanup script This implementation prunes local branches that have been merged and optionally prunes remote branches that have been merged. The script may be configured to keep branches through local git configuration. The remote name can be autocompleted. --- modules/git/git_branch_cleanup.md | 21 +++- modules/git/git_branch_cleanup.nu | 171 +++++++++++++++++++++++++++++- 2 files changed, 189 insertions(+), 3 deletions(-) diff --git a/modules/git/git_branch_cleanup.md b/modules/git/git_branch_cleanup.md index 26d70623..f0c91619 100644 --- a/modules/git/git_branch_cleanup.md +++ b/modules/git/git_branch_cleanup.md @@ -1,3 +1,22 @@ # Git branch cleanup -Remove any local git branch no longer being tracked in origin / remote repo. +Remove any local git branches that have been merged and optionally remove any +remote branches that have been merged. + +Load with: +```nushell +source modules/git/git_branch_cleanup.nu +``` + +To remove merged branches: +```nushell +git branch-cleanup +``` + +To keep merged branches that start with "releases/": + +```nushell +git config --local --add branch-cleanup.keep 'releases/.*' +``` + +Keep branch patterns are space-separated diff --git a/modules/git/git_branch_cleanup.nu b/modules/git/git_branch_cleanup.nu index 4817bc8b..9dc3967b 100644 --- a/modules/git/git_branch_cleanup.nu +++ b/modules/git/git_branch_cleanup.nu @@ -1,4 +1,171 @@ -#!/usr/bin/env nu +# Adapted from original by Yorick Sijsling +# +# Adapted from bash version `git cleanup-repo` by Rob Miller +# https://gist.github.com/robmiller/5133264 +# Delete local (and optionally remote) merged branches +# +# Set branch-cleanup.keep locally to a space-separated list of branch patterns to keep +export def "git branch-cleanup" [ + upstream: string@remotes = "origin" # Upstream remote repository +] { + let current_branch = current_branch + let default_branch = get_default_branch $"refs/remotes/($upstream)/HEAD" + let keep = get_keep -git branch -vl | lines | split column " " BranchName Hash Status --collapse-empty | where Status == '[gone]' | each { |it| git branch -D $it.BranchName } + # Switch to the default branch + switch_branch $default_branch + + # Make sure we're working with the most up-to-date version of the default + # branch. + run-external "git" "fetch" + + # Prune obsolete remote tracking branches. These are branches that we + # once tracked, but have since been deleted on the remote. + run-external "git" "remote" "prune" $upstream + + # Delete local branches that have been fully merged into the default branch + list_merged $upstream $default_branch $keep + | each {|branch| + delete_local $branch + } + + # Again with remote branches + let merged_on_remote = list_merged --remote $upstream $default_branch $keep + + if not ( $merged_on_remote | is-empty ) { + print "The following remote branches are fully merged and will be removed:" + + $merged_on_remote + | each {|| + print $"\t($in)" + } + + print "" + + if ( input --suppress-output "Continue (y/N)? " | str trim ) == "y" { + $merged_on_remote + | each {|branch| + delete_remote $upstream $branch + } + } + } + + switch_branch $current_branch +} + +# The current branch name +def current_branch [] { + run-external --redirect-stdout "git" "branch" "--show-current" + | into string + | str trim +} + +# Delete a local branch +def delete_local [ + branch: string # Branch to delete +] { + run-external "git" "branch" "--delete" $branch +} + +# Delete a remote branch +def delete_remote [ + upstream: string # Repository to delete from + branch: string # branch to delete +] { + run-external "git" "push" "--quiet" "--delete" $upstream $branch +} + +# Get the default branch +def get_default_branch [ + upstream: string # Repository to get the upstream branch name from +] { + let args = [ + "--short" + $upstream + ] + + run-external --redirect-stdout "git" "symbolic-ref" $args + | str trim + | path basename +} + +# Get the local set of branches to always keep +def get_keep [] { + let keep = run-external --redirect-stdout "git" "config" "--local" "--get" "branch-cleanup.keep" + + if ( $keep | is-empty ) { + return [] + } + + $keep + | str trim + | split column " " + | get column1 +} + +# List all the branches that have been merged fully into the default branch. +# +# We use the remote default branch here, just in case our local default branch is out of date. +def list_merged [ + --remote # List remote branches (local default) + upstream: string # Upstream repository + branch: string # Default branch + keep: list # Patterns to keep. The default branch and HEAD will be added +] { + mut args = [ + "--list" + "--merged" $"($upstream)/($branch)" + ] + + if $remote { + $args = ( $args | append [ + "--format" "%(refname:lstrip=3)" + "--remote" + ]) + } else { + $args = ( $args | append [ + "--format" "%(refname:lstrip=2)" + ]) + } + + let args = $args + + let keep = ( + $keep + | append [ + "HEAD", + $branch, + ] + ) + + run-external --redirect-stdout "git" "branch" $args + | lines + | filter {|branch| + $keep + | all {|pattern| + $branch !~ $'\A($pattern)\z' + } + } +} + +def remotes [] { + run-external --redirect-stdout "git" "remote" "-v" + | parse "{value}\t{description} ({operation})" + | select value description + | uniq + | sort +} + +# Switch to a different branch +def switch_branch [ + branch: string +] { + let args = [ + "--quiet" + "--no-guess" + $branch + ] + + run-external "git" "switch" $args +}