Skip to content

Instantly share code, notes, and snippets.

@pariser
Forked from jaredatron/git-branch-details.rb
Last active May 10, 2016 22:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pariser/ccec91f52a76dec0935f9216bdcf759a to your computer and use it in GitHub Desktop.
Save pariser/ccec91f52a76dec0935f9216bdcf759a to your computer and use it in GitHub Desktop.
git-branch-details
#!/usr/bin/env ruby
require 'time'
require 'colored'
require 'parallel'
require 'optparse'
class GitBranchDetails
NotAGitRepository = Class.new(StandardError)
GOOD_TRAVIS_STATES = %w( created passed started ).freeze
NEUTRAL_TRAVIS_STATES = [ "not found", "canceled" ].freeze
BAD_TRAVIS_STATES = %w( failed errored ).freeze
EXPECTED_TRAVIS_STATES = (GOOD_TRAVIS_STATES + NEUTRAL_TRAVIS_STATES + BAD_TRAVIS_STATES).freeze
SPECIAL_BRANCHES = %w(
master
production
staging
beta
integration
).freeze
def self.parse_arguments(arguments)
branch_names = []
options = {}
args = arguments.dup
until args.empty?
break if arguments.first.start_with?('-')
branch_names << arguments.shift
end
option_parser = OptionParser.new do |opts|
opts.banner = "Usage: git branch-details [branch_name, ...] [options]"
opts.on('--[no-]travis', 'Check status of each branch on travis (can be slow)') do |should_run_travis|
options[:travis] = should_run_travis
end
end
option_parser.parse! args
[ branch_names, options ]
end
def initialize(branch_names=nil, options={})
@branch_names = branch_names unless branch_names.empty?
@check_travis = options.key?(:travis) ? options[:travis] : true
end
def print_report
ensure_git_repository!
branches
column_widths = columns.each_with_object({}) do |column, hash|
row_sizes = branches.map do |branch|
value = branch[column]
value.nil? ? 0 : value.length
end
hash[column] = (row_sizes + [column.length]).max + 5
end
print_format = column_widths.map { |_, width| "%-#{width}s" }.join(" ") + "\n"
x = print_format % columns
puts ""
puts x
puts '-' * x.length
branches.each do |branch|
format_strings = []
values = []
columns.each do |column|
unformatted_value = branch[column]
unformatted_width = unformatted_value.nil? ? 0 : branch[column].length
color = color_of_value(column, unformatted_value)
formatted_value = unformatted_value.nil? ? nil : unformatted_value.send(color)
formatted_width = formatted_value.nil? ? 0 : formatted_value.length
column_width = column_widths[column] + formatted_width - unformatted_width
format_strings << "%-#{column_width}s"
values << formatted_value
end
print_format = format_strings.join(" ") + "\n"
printf print_format, *values
end
puts ""
true
rescue NotAGitRepository
puts ""
puts "This directory is not a git repository".red
puts ""
false
end
private
def columns
@columns ||= begin
cols = [
'name',
'latest commit',
'message',
'author name',
'upstream',
'merged o/m?',
]
cols << 'travis' if @check_travis
cols
end
@columns
end
def color_of_value(column, value)
color = begin
case column
when 'name'
case value
when current_branch_name
:green
when *SPECIAL_BRANCHES
:yellow
end
when 'travis'
case value
when *GOOD_TRAVIS_STATES
:green
when *NEUTRAL_TRAVIS_STATES
:yellow
when *BAD_TRAVIS_STATES
:red
end
when 'upstream'
case value
when / gone/
:red
when / behind \d+/
:yellow
when / ahead \d+/
:green
end
end
end
color ||= :to_s
color
end
def ensure_git_repository!
git_status = `git status 2>&1`
raise NotAGitRepository, "#{self.class.name} must be run from a git repository" if git_status =~ /^fatal: Not a git repository/
end
def current_branch_name
@current_branch_name ||= `git rev-parse --abbrev-ref HEAD`.chomp
@current_branch_name
end
def branch_names
@branch_names ||= begin
git_branch_result = `git branch`.chomp.gsub(/^../, '').split("\n")
git_branch_result.reject! { |branch_name| branch_name =~ %r{/} }
git_branch_result
end
@branch_names
end
GIT_BRANCH_VV_PARSE_REGEX = /^(?<name>\S+)\s+(?<sha>[0-9a-f]+)\s+(\[(?<upstream>.+)\] )?(?<message>.*)$/
def branch_verbose_details
@branch_verbose_details ||= begin
git_branch_result = `git branch -vv`.chomp.gsub(/^../, '').split("\n")
git_branch_result.each_with_object({}) do |result, hash|
match = GIT_BRANCH_VV_PARSE_REGEX.match(result)
next if match.nil?
hash[match['name']] = {
'sha' => match['sha'],
'message' => match['message'],
'upstream' => (match['upstream'] unless match.names.index('upstream').nil?),
}
end
end
@branch_verbose_details
end
def get_branch_info(branch_name)
data = {
'name' => branch_name,
}
if branch_verbose_details.key?(branch_name)
data.merge!(branch_verbose_details[branch_name])
end
git_log_response = `git log -1 --format="%h,%an,%ae,%ci,%cr" #{branch_name}`.chomp.split(',')
["sha", "author name", "author email", "latest commit time", "latest commit"].zip(git_log_response).each do |field, value|
data[field] = value
end
data['latest commit time'] = Time.parse(data['latest commit time'])
data['merged o/m?'] = `git branch -r --contains #{branch_name}`.chomp.gsub(/^../, '').split("\n").include?('origin/master') ? 'yes' : 'no'
data['travis'] = get_travis_state(branch_name) if @check_travis
data
end
def get_travis_state(branch_name)
# puts "travis show #{branch_name} 2>&1"
travis_show = `travis show #{branch_name} 2>&1`
if travis_show =~ /^resource not found/i
return "not found"
end
travis_state = travis_show[/State:\s+(.+)\n/, 1]
travis_state = "unknown state #{travis_state.inspect}" unless EXPECTED_TRAVIS_STATES.include?(travis_state)
travis_state
end
def branches
@branches ||= begin
message =
if @check_travis
"* Loading branch information and travis status"
else
"* Loading branch information"
end
print message
data = Parallel.map(branch_names, in_threads: 3) do |branch_name|
print "."
get_branch_info(branch_name)
end
puts ""
data.sort_by! { |b| b['latest commit time'] }.reverse!
end
end
end
if __FILE__ == $PROGRAM_NAME
branch_names, options = GitBranchDetails.parse_arguments(ARGV)
success = GitBranchDetails.new(branch_names, options).print_report
exit success ? 0 : 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment