Skip to content

Instantly share code, notes, and snippets.

@robhurring
Created October 8, 2014 23:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robhurring/9359bc6f00e2a6c36192 to your computer and use it in GitHub Desktop.
Save robhurring/9359bc6f00e2a6c36192 to your computer and use it in GitHub Desktop.
Fuzzy git checkout. With super awesome branch selection!
#!/usr/bin/env ruby
require 'optparse'
module CLI
COLORS = {
black: "\e[30;1m",
red: "\e[31m",
green: "\e[32m",
blue: "\e[34m",
white: "\e[37m",
magenta: "\e[35m",
clear: "\e[0m"
}
def prompt_user_selection(choices = [], banner = nil, &callback)
say banner unless banner.nil?
choices.each.with_index do |match, i|
if block_given?
match = yield match
end
say "%{magenta}#{i + 1})%{clear} #{match}"
end
selection = prompt_user "\n%{white}Selection>%{clear} "
if selection.zero? || selection > choices.size
nil
else
choices[selection - 1]
end
end
def prompt_user(message = ">")
say message
STDIN.gets.to_i
end
def say(what)
output = what % COLORS
# ending in a space will just print
unless what[-1] == ' '
output << "\n"
end
print output
end
end
class FuzzyCheckout
include CLI
class << self
def run(args=ARGV)
options = {
interactive: false
}
parser = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename $0} [options]"
opts.on("-i", "--interactive", "Run interactively") do |i|
options[:interactive] = i
end
end
parser.parse!
search_term = ARGV[0]
if search_term.nil?
puts parser
exit 1
else
new(options).checkout(search_term)
end
end
end
attr_reader :options
def initialize(options = {})
@options = options
end
def checkout(term)
matches = []
regexp = Regexp.new(term)
branches.each do |branch|
next if branch.to_s == 'HEAD'
if branch =~ regexp
matches << branch
end
end
if matches.any?
checkout_proper_branch(term, matches)
else
say "%{red}No branches matched: %{white}#{term}%{clear}"
exit 1
end
end
private
def checkout_proper_branch(term, matches)
case matches.size
when 1
switch_branch matches.first
exit 0
else
say "%{green}Found #{matches.size} matches for: %{white}#{term}%{clear}\n"
if options[:interactive]
selected_branch = prompt_user_selection(matches) do |match|
info = branch_times[match]
if info
info = "%{black}(#{info})%{clear}"
end
"#{match} %{black}#{info}%{clear}"
end
if selected_branch
switch_branch selected_branch
else
say "%{red}Invalid selection%{clear}"
exit 1
end
else
say "%{red}Cannot determine which branch to switch to.\nTry running with the '-i' flag.%{clear}"
exit 1
end
end
end
def switch_branch(name)
%x{git checkout #{name}}
end
def branches
@_branches ||= %x{git branch -a}.split.map{ |b| b.split('/').last }.uniq.sort_by{ |b| normalize(b) }
end
def branch_times
@_branch_times ||= begin
Hash[%x{git for-each-ref --sort='-authordate:iso8601' --format='%(refname:short)~%(authordate:relative) by %(authorname)' refs/heads}.split("\n").map{ |b| b.split("~") }]
end
end
# keeps semantic versioning in proper sort order
def normalize(name)
name.to_s.scan(%r/\d+/o).map(&:to_i)
end
end
FuzzyCheckout.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment