Last active
July 25, 2019 15:53
-
-
Save brand-it/ea4a09f55504a080672feca8f7d7b57c to your computer and use it in GitHub Desktop.
This lets you destroy branches that are older then a given amount of time.
This file contains 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 | |
require File.expand_path('ruby_bash', __dir__).to_s | |
require 'date' | |
class ProcessData | |
attr_accessor( | |
:all, :debug, :changes_stashed, :primary_remote_name, :remotes, :current_branch, :git_logs, | |
:min_print_length, :since, :list_branches, :dry_run, :remote_branch_list, :local_branches | |
) | |
def initialize | |
self.dry_run = false | |
self.list_branches = false | |
self.since = system!("gdate '+%s' -d '2 weeks ago'").stdout_str.to_i | |
self.min_print_length = 0 | |
self.git_logs = [] | |
self.current_branch = system!('git rev-parse --abbrev-ref HEAD').stdout_str.strip | |
self.remotes = system!('git remote').stdout_str.split("\n").map(&:strip) | |
self.primary_remote_name = remotes.first | |
self.changes_stashed = false | |
self.debug = false | |
self.all = false # all the branches including the remote branches | |
self.remote_branch_list = {} | |
self.debug = false | |
self.local_branches = system!('git branch').stdout_str.split("\n").map { |b| b.gsub('* ', '').strip } | |
OptionParser.new do |opts| | |
opts.banner = 'Usage: branch_cleaning [options]' | |
opts.on('-d', '--dry-run', 'Display the output of what it is gong to delete without actually doing it') do | |
self.dry_run = true | |
self.list_branches = true | |
end | |
opts.on( | |
'-l', '--list-branches', 'Show all the brances and there time stamps. Will display what will be deleted' | |
) do | |
self.list_branches = true | |
end | |
opts.on( | |
'-s', | |
"--since #{since}", | |
String, | |
'How long ago you want to go back in time. --since="2 weeks ago"' | |
) do |value| | |
self.since = system!("gdate '+%s' -d '#{value}'").stdout_str | |
puts "Destroy anything older then #{since_to_datetime}" | |
end | |
opts.on( | |
'-r', "--remote-name #{primary_remote_name}", | |
String, | |
'The Primary Remote branch. The primary will not append the remote name' | |
) do |value| | |
self.primary_remote_name = value | |
end | |
opts.on('-b', '--debug', 'Output all the console commands that are being executed') do |_value| | |
self.debug = true | |
end | |
opts.on( | |
'-a', '--all', 'Include remote branches as well as local branches, remote branches will not be deleted' | |
) do | |
self.all = true | |
end | |
opts.on('-h', '--help', 'Prints this help') do | |
puts opts | |
exit | |
end | |
end.parse! | |
end | |
def since=(value) | |
@since = value.to_i | |
end | |
def remote_branch_list | |
return @remote_branch_list unless @remote_branch_list.empty? | |
remotes.each do |key| | |
@remote_branch_list[key] = system!("git ls-remote --heads #{key}").stdout_str.split("\n").map { |x| x.split("\t")[1] } | |
end | |
@remote_branch_list | |
end | |
def since_to_datetime | |
DateTime.strptime(since.to_s, '%s').strftime('%F %R') | |
end | |
def stash_changes | |
if capture3('git diff-index --quiet HEAD --').status.success? | |
system! 'git add --all' | |
system! 'git stash save backup' | |
changes_stashed = true | |
end | |
end | |
def create_git_log(branch) | |
git_log = GitLog.new(self, branch) | |
return if git_logs.any? { |git| git.key == git_log.key } | |
puts "Getting Branch Data for #{git_log.key}" | |
self.min_print_length = git_log.branch_name.length if git_log.branch_name.length > min_print_length | |
git_log.switch_branch! | |
git_log.update! | |
git_logs.push(git_log) | |
end | |
def fixed_width(string, length) | |
string = string.to_s | |
string_length = string.length | |
append_size = length.to_i - string_length | |
(0..append_size).to_a.each { string += ' ' } | |
string | |
end | |
def display_header | |
puts "min_print_length set to #{min_print_length}" if debug | |
puts "#{fixed_width('', 8)}#{fixed_width('remote', 10)}#{fixed_width('Branch Name', min_print_length)}#{fixed_width('SHA', 40)}#{fixed_width('Date', 30)}#{fixed_width('Time Ago', 30)}" | |
end | |
def display_delete_preview | |
return unless list_branches | |
display_header | |
git_logs.each(&:display_preview) | |
end | |
def cleanup | |
system! 'git add --all' | |
system! 'git stash' | |
system! "git checkout #{current_branch}" | |
system! 'git stash pop stash^{/backup}' if changes_stashed | |
end | |
def branches | |
system_response = all ? system!('git branch -vva') : system!('git branch -vv') | |
system_response.stdout_str.split("\n").map { |b| resolve_git_branch(b) }.compact | |
end | |
def resolve_git_branch(string) | |
sha = string.match(/\s\S*\s(\w{10})\s/).to_a[1] | |
return if sha.nil? | |
data = string.split(sha) | |
from_remote = data.first.include?('remotes/') | |
if from_remote | |
remote = remotes.find { |r| data[0].include?("/#{r}/") } | |
{ | |
name: data.first.strip.gsub("remotes/#{remote}/", ''), | |
sha: sha, | |
remote: remote, | |
from_remote: from_remote | |
} | |
else | |
remote = remotes.find { |r| data[1].include?("[#{r}") } | |
{ | |
name: data.first.strip.gsub('* ', ''), | |
sha: sha, | |
remote: remote, | |
from_remote: from_remote | |
} | |
end | |
end | |
end | |
class GitLog | |
class CheckoutError < StandardError; end | |
attr_accessor :remote, :branch_name, :sha, :time_ago, :subject, :data, :debug, :new_branch, :from_remote | |
attr_reader :date | |
def initialize(data, branch_info = {}) | |
self.branch_name = branch_info[:name] | |
self.data = data | |
self.remote = branch_info[:remote] | |
self.sha = branch_info[:sha] | |
self.debug = data.debug | |
self.from_remote = branch_info[:from_remote] | |
end | |
def key | |
[remote, branch_name].compact.join('/') | |
end | |
def display_preview | |
status = "#{data.fixed_width(remote, 10)}#{data.fixed_width(branch_name, data.min_print_length)}#{data.fixed_width(sha, 40)}#{data.fixed_width(frendly_date, 30)}#{data.fixed_width(time_ago, 30)}" | |
if can_delete? | |
puts "\033[0;31m#{data.fixed_width('delete', 8)}#{status}\033[0m" | |
elsif remote_branch_exists? && !local_branch_exists? | |
puts "\033[0;33m#{data.fixed_width('deleted', 8)}#{status}\033[0m" | |
else | |
puts "#{data.fixed_width('keep', 8)}#{status}" | |
end | |
end | |
def checkout_branch_name | |
return branch_name if local_branch_exists? | |
return "#{remote}-#{branch_name}" if data.primary_remote_name != remote | |
branch_name | |
end | |
def switch_branch! | |
if remote_branch_exists? && local_branch_exists? | |
system! "git checkout -B #{checkout_branch_name} #{remote}/#{branch_name}" | |
elsif remote_branch_exists? | |
system! "git checkout -B #{checkout_branch_name} #{remote}/#{branch_name}" | |
puts " \033[0;33mCreated New Branch #{checkout_branch_name}\033[0m" | |
data.local_branches.push(checkout_branch_name) | |
elsif local_branch_exists? | |
system! "git checkout #{checkout_branch_name}" | |
else | |
raise CheckoutError, "could not checkout branch #{remote} #{branch_name} does not exist" | |
end | |
end | |
def date=(value) | |
@date = value.to_i | |
end | |
def update! | |
self.sha, self.date, self.time_ago, self.subject = system!( | |
"git log --pretty=format:'%H,%ct,%cr,%s' -n 1" | |
).stdout_str.split(',') | |
end | |
def delete! | |
return unless can_delete? | |
system!("git branch -D #{checkout_branch_name}") | |
end | |
def friendly_date | |
DateTime.strptime(date.to_s, '%s').strftime('%F %R') | |
end | |
def can_delete? | |
data.since > date && branch_name != 'master' && local_branch_exists? | |
end | |
def local_branch_exists? | |
data.local_branches.include?(branch_name) | |
end | |
def remote_branch_exists? | |
data.remotes.include?(remote) && data.remote_branch_list[remote].include?("refs/heads/#{branch_name}") | |
end | |
end | |
process_data = ProcessData.new | |
process_data.remotes.each do |remote| | |
system! "git fetch #{remote}" | |
end | |
begin | |
process_data.branches.each do |branch| | |
process_data.create_git_log(branch) | |
end | |
process_data.git_logs.sort_by!(&:date) | |
process_data.display_delete_preview | |
puts 'This was a dry run' if process_data.dry_run | |
exit if process_data.dry_run | |
system!('git checkout -B master origin/master') | |
process_data.display_header | |
process_data.git_logs.each do |git_log| | |
git_log.display_preview | |
git_log.delete! | |
end | |
rescue GitLog::CheckoutError => e | |
abort e.message | |
ensure | |
process_data.cleanup | |
end |
This file contains 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
# frozen_string_literal: true | |
# This is some top level systems for creating bash scripts in ruby | |
require 'pathname' | |
require 'optparse' | |
require 'open3' | |
require 'timeout' | |
def escape_path(path) | |
path.gsub(/ /, '\ ') | |
end | |
def text(message) | |
auth_token = ENV['TWILIO_AUTH_TOKEN'] | |
user_id = ENV['TWILIO_USER_ID'] | |
params = [ | |
'-X POST', | |
"--data-urlencode 'To=+12105353532'", | |
"--data-urlencode 'From=+15592060364'", | |
"--data-urlencode 'Body=#{message}'", | |
"-u #{user_id}:#{auth_token}" | |
] | |
system!( | |
"curl 'https://api.twilio.com/2010-04-01/"\ | |
"Accounts/#{user_id}/Messages.json'"\ | |
" #{params.join(' ')}" | |
) | |
end | |
begin | |
require 'pry' | |
rescue LoadError => exception | |
# puts "\033[0;33m#{exception.message}\033[0m" | |
end | |
begin | |
require 'awesome_print' | |
rescue LoadError => exception | |
# puts "\033[0;33m#{exception.message}\033[0m" | |
end | |
class Errors | |
@messages = [] | |
class << self | |
attr_reader :messages | |
end | |
def self.add(value) | |
@messages << value | |
end | |
def self.reset | |
@messages = [] | |
end | |
end | |
# If there become to many arguments move into a initalize | |
Open3::Response = Struct.new(:stdout_str, :stderr_str, :status, :cmd) | |
def capture3(*cmd) | |
puts " \033[0;33mrunning #{cmd}\033[0m" if @debug | |
stdout_str, stderr_str, status = Open3.capture3(*cmd) | |
Open3::Response.new(stdout_str, stderr_str, status, *cmd) | |
end | |
def system!(*cmd) | |
response = capture3(*cmd) | |
puts caller unless response.status.success? | |
abort "\033[0;33mFailure #{cmd}\n\033[0;31m #{response.stderr_str}\033[0m" unless response.status.success? | |
response | |
end | |
def print_error(message) | |
puts "\033[0;31mError: #{message}\033[0m" | |
end | |
def print_warning(message) | |
puts "\033[0;33mWarning: #{message}\033[0m" | |
end | |
def puts_success(message) | |
puts "\033[1;32mSuccess: #{message}\033[0m" | |
end | |
def display_errors | |
Errors.messages.each do |message| | |
print_error(message) | |
end | |
end | |
def abort_and_display_errors | |
return if Errors.messages.empty? | |
display_errors | |
abort "\033[0;33mResolve Error message above before you continue\033[0m" | |
end |
This little tool is super helpful for removing branches that have not been changed in a given amount of time. It only destroys branches on your local computer and does not push those change up to GitHub or whatever git service your using.
Reminder to myself. /
slashes in branch name creates some odd problems... got to fix that.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this requires
gdate
fromcoreutils
. To install runbrew install coreutils
after that the gdate command should be available