Last active
July 19, 2025 06:53
-
-
Save pvdb/6c6e8868ae48210657834c230b61be22 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
# | |
# INSTALLATION (2 aliases) | |
# | |
# ln -s ${PWD}/git-branch---stray $(brew --prefix)/bin/git-branch--delete--stray | |
# sudo ln -s ${PWD}/git-branch---stray /usr/local/bin/git-branch--delete--stray | |
# | |
# ln -s ${PWD}/git-branch---stray $(brew --prefix)/bin/git-branch--list--stray | |
# sudo ln -s ${PWD}/git-branch---stray /usr/local/bin/git-branch--list--stray | |
# | |
# CONFIGURATION (2 aliases) | |
# | |
# git config --global alias.bls branch--list--stray | |
# git config --global alias.bds branch--delete--stray | |
# | |
%w[TERM INT].each do |signal| | |
Signal.trap(signal) do | |
exit(0) | |
end | |
end | |
require 'English' | |
require 'consenter' | |
class String # :nodoc: | |
def colorize(color_code) | |
"\e[#{color_code}m#{self}\e[0m" | |
end | |
def red | |
colorize('31') | |
end | |
def yellow | |
colorize('33') | |
end | |
end | |
module Kernel # :nodoc: | |
def git(command) | |
`git #{command}`.split($RS).each(&:strip!) | |
end | |
end | |
module Git # :nodoc: | |
module_function | |
def local_head_refs | |
git('for-each-ref --format="%(refname)" refs/heads') | |
end | |
def head_refs_for(remote) | |
# alternative implementation that requires | |
# access to the remote tracking repository | |
# | |
# git("ls-remote --quiet --refs --heads #{remote}") | |
# .map { |sha_and_ref| sha_and_ref.split(/\s+/).last } | |
# | |
# but we assume that this utility will mainly be | |
# used as part of a "git workflow" that includes | |
# regular pruning of remote tracking references, | |
# which means we don't have to go over the wire! | |
git("for-each-ref --format=\"%(refname)\" refs/remotes/#{remote}") | |
end | |
def upstream_reference_for(branch) | |
git("rev-parse --verify --abbrev-ref #{branch}@{upstream}").first | |
end | |
def remote_for(branch) | |
git("config --local --get branch.#{branch}.remote").first | |
end | |
def merge_ref_for(branch) | |
git("config --local --get branch.#{branch}.merge").first | |
end | |
def branch_name_from(symbolic_ref) | |
# extract the branch name from a given symbolic ref | |
# local: e.g. "refs/heads/master" | |
# remote: e.g. "refs/remotes/origin/master" | |
symbolic_ref.sub(%r{\Arefs/(heads|remotes/[^/]+)/}, '') | |
end | |
# rubocop:disable Metrics/AbcSize | |
# rubocop:disable Metrics/MethodLength | |
# rubocop:disable Style/BlockDelimiters | |
def stray_branches | |
to_branch_name = method(:branch_name_from) | |
local_branch_names = local_head_refs.map(&to_branch_name) | |
remote_branch_names = Hash.new { |branch_names, remote| | |
branch_names[remote] = head_refs_for(remote).map(&to_branch_name) | |
} | |
local_branch_names.find_all { |local_branch| | |
(remote = remote_for(local_branch)) && | |
(merge_ref = merge_ref_for(local_branch)) && | |
(upstream_branch = to_branch_name[merge_ref]) && | |
!remote_branch_names[remote].include?(upstream_branch) | |
} | |
end | |
# rubocop:enable Style/BlockDelimiters | |
# rubocop:enable Metrics/MethodLength | |
# rubocop:enable Metrics/AbcSize | |
def delete_stray_branches | |
stray_branches.each_consented('Delete stray branch "%s"', inspector: :yellow) do |stray| | |
system("git branch -d #{stray}") | |
next if $CHILD_STATUS.success? | |
Array(stray).each_consented('Delete unmerged branch "%s"', inspector: :red) do |unmerged| | |
system("git branch -D #{unmerged}") | |
end | |
end | |
end | |
def list_stray_branches | |
stray_branches.each do |stray| | |
system("git --no-pager branch -vv --list #{stray}") | |
end | |
end | |
end | |
if git('rev-parse --show-toplevel').first | |
case File.basename($PROGRAM_NAME) | |
when 'git-branch--list--stray' then Git.list_stray_branches | |
when 'git-branch--delete--stray' then Git.delete_stray_branches | |
end | |
exit(0) | |
else | |
puts "Please run #{File.basename(__FILE__)} from inside a git repo!" | |
exit(-1) | |
end | |
# That's all Folks! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment