Skip to content

Instantly share code, notes, and snippets.

@andlrc
Created February 19, 2025 14:39
Show Gist options
  • Select an option

  • Save andlrc/1e9bc0e30c530f1a02a3e73139b62574 to your computer and use it in GitHub Desktop.

Select an option

Save andlrc/1e9bc0e30c530f1a02a3e73139b62574 to your computer and use it in GitHub Desktop.
#!/usr/bin/env perl
# Author: Andreas Louv <andreas@louv.dk>
# Date: Sep 19 2023
use strict;
use warnings;
use Getopt::Long qw(:config no_ignore_case bundling);
use Pod::Usage;
my $FFLAG_CONFIG_KEY = "gc-branch.requireForce";
my $nflag = 0;
my $fflag = qx{ git config $FFLAG_CONFIG_KEY } =~ m{ false }x;
my $iflag = 0;
my $qflag = 0;
sub main
{
GetOptions(
"h|help" => sub { pod2usage(1); },
"man" => sub { pod2usage(-exitval => 0, -verbose => 2); },
"n|dry-run!" => \$nflag,
"f|force!" => \$fflag,
"i|Interactive!" => \$iflag,
"q|quiet!" => \$qflag,
) or pod2usage(2);
if (!$nflag && !$fflag && !$iflag) {
print STDERR "fatal: $FFLAG_CONFIG_KEY defaults to true and neither -i, -n, nor -f given; refusing to delete\n";
exit 128
}
my @local_branches = get_local_branches();
for my $branch (@local_branches) {
next unless $branch->{gone};
my $local_branch = $branch->{local_branch};
chomp(my $latest_commit_subject = qx{ git log -1 --format=%s $local_branch });
if ($nflag) {
print "Would delete branch $local_branch: $latest_commit_subject\n" unless $qflag;
next;
}
if ($iflag) {
print "Delete branch $local_branch: $latest_commit_subject [y/N/q]: ";
my $answer = <>;
exit if $answer =~ m{^ (?: q | quit ) $}msxi;
next unless $answer =~ m{^ (?: y | yes ) $}msxi;
}
my $output = qx{ git branch -D $local_branch };
print $output unless $qflag;
}
}
sub get_local_branches
{
my @branches;
open(my $gone_fh, "-|", "git branch -vv");
while (<$gone_fh>) {
next unless m{
^
.. # "* " indices current branch, otherwise " "
(?<local_branch>\S+) # local branch
\s+
(?<sha>[A-Fa-f0-9]+) # short sha
\s+
\[ # "["
(?<remote_branch>\S+) # remote branch
(?<gone>:\sgone)? # the text ": gone" is present the branch has been deleted on the remote
\] # "]"
\s+
(?<commit_subject>.*) # commit subject
$
}xms;
push(@branches, {
local_branch => $+{local_branch},
sha => $+{sha},
remote_branch => $+{remote_branch},
gone => defined $+{gone} && $+{gone} eq ": gone",
commit_subject => $+{commit_subjcet},
})
}
close($gone_fh);
return @branches;
}
main();
__END__
=head1 NAME
git gc-branch Delete remote closed branches
=head1 SYNOPSIS
git gc-branch [-q] [-f] [-i] [-n]
Options:
-q, --quiet do not print names of files removed
-i, --interactive enable interactive removal
-n, --dry-run dry run
-f, --force force
=head1 OPTIONS
=over 8
=item B<-f>, B<--force>
If the Git configuration variable gc-branch.requireForce is not set to false,
B<git> B<gc-branch> will refuse to delete branches given -f.
=item B<-i>, B<--interactive>
Show what would be done and remove branches interactively.
=item B<-n>, B<--dry-run>
Don't actually remove anything, just show what would be done.
=item B<-q>, B<--quiet>
Be quiet, only report errors, but not the files that are successfully removed.
=back
=head1 DESCRIPTION
Deletes all local branches that are reported in the commit messages to be closed.
=cut
@Konfekt
Copy link

Konfekt commented Sep 15, 2025

Does

        (?<gone>:\sgone)?     # the text ": gone" is present the branch has been deleted on the remote

mean that some commit message in that branch has to contain the word gone for it to be deleted? If so, is that a (custom) convention ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment