status_shortcuts.rb completely rewritten
This commit is contained in:
421
lib/git/status_shortcuts.rb
Normal file → Executable file
421
lib/git/status_shortcuts.rb
Normal file → Executable file
@@ -1,212 +1,249 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: UTF-8
|
||||
# ------------------------------------------------------------------------------
|
||||
# SCM Breeze - Streamline your SCM workflow.
|
||||
# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
|
||||
# Released under the LGPL (GNU Lesser General Public License)
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# A much faster implementation of git_status_shortcuts() in ruby
|
||||
# (bash: 0m0.549s, ruby: 0m0.045s)
|
||||
#
|
||||
# Last line of output contains the ordered absolute file paths,
|
||||
# which need to be extracted by the shell and exported as numbered env variables.
|
||||
#
|
||||
# Processes 'git status --porcelain', and exports numbered
|
||||
# env variables that contain the path of each affected file.
|
||||
# Output is also more concise than standard 'git status'.
|
||||
#
|
||||
# Call with optional <group> parameter to just show one modification state
|
||||
# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
|
||||
# --------------------------------------------------------------------
|
||||
require 'strscan'
|
||||
|
||||
@project_root = File.exist?(".git") ? Dir.pwd : `\git rev-parse --show-toplevel 2> /dev/null`.strip
|
||||
class GitStatus
|
||||
|
||||
@git_status = `\git status --porcelain 2> /dev/null`
|
||||
COL = {
|
||||
:rst => "\033[0m",
|
||||
:del => "\033[0;31m",
|
||||
:mod => "\033[0;32m",
|
||||
:new => "\033[0;33m",
|
||||
:ren => "\033[0;34m",
|
||||
:cpy => "\033[0;33m",
|
||||
:typ => "\033[0;35m",
|
||||
:unt => "\033[0;36m",
|
||||
:dark => "\033[2;37m",
|
||||
:branch => "\033[1m",
|
||||
:header => "\033[0m"
|
||||
}
|
||||
|
||||
git_branch = `\git branch -v 2> /dev/null`
|
||||
@branch = git_branch[/^\* (\(no branch\)|[^ ]*)/, 1]
|
||||
@ahead = git_branch[/^\* [^ ]* *[^ ]* *\[ahead ?(\d+).*\]/, 1]
|
||||
@behind = git_branch[/^\* [^ ]* *[^ ]* *\[.*behind ?(\d+)\]/, 1]
|
||||
# following colors must be prepended with modifiers e.g. '\033[1;', '\033[0;'
|
||||
GR_COL = {
|
||||
:header => "\033[2;37m",
|
||||
:staged => "33m",
|
||||
:unmerged => "31m",
|
||||
:unstaged => "32m",
|
||||
:untracked => "36m"
|
||||
}
|
||||
|
||||
@changes = @git_status.split("\n")
|
||||
# Exit if too many changes
|
||||
exit if @changes.size > ENV["gs_max_changes"].to_i
|
||||
GROUPS = {
|
||||
staged: 'Changes to be committed',
|
||||
unmerged: 'Unmerged paths',
|
||||
unstaged: 'Changes not staged for commit',
|
||||
untracked: 'Untracked files'
|
||||
}
|
||||
|
||||
# Colors
|
||||
@c = {
|
||||
:rst => "\033[0m",
|
||||
:del => "\033[0;31m",
|
||||
:mod => "\033[0;32m",
|
||||
:new => "\033[0;33m",
|
||||
:ren => "\033[0;34m",
|
||||
:cpy => "\033[0;33m",
|
||||
:typ => "\033[0;35m",
|
||||
:unt => "\033[0;36m",
|
||||
:dark => "\033[2;37m",
|
||||
:branch => "\033[1m",
|
||||
:header => "\033[0m"
|
||||
}
|
||||
|
||||
|
||||
# Following colors must be prepended with modifiers e.g. '\033[1;', '\033[0;'
|
||||
@group_c = {
|
||||
:staged => "33m",
|
||||
:unmerged => "31m",
|
||||
:unstaged => "32m",
|
||||
:untracked => "36m"
|
||||
}
|
||||
|
||||
@stat_hash = {
|
||||
:staged => [],
|
||||
:unmerged => [],
|
||||
:unstaged => [],
|
||||
:untracked => []
|
||||
}
|
||||
|
||||
@output_files = []
|
||||
|
||||
# Counter for env variables
|
||||
@e = 0
|
||||
|
||||
# Show how many commits we are ahead and/or behind origin
|
||||
difference = ["-#{@behind}", "+#{@ahead}"].select{|d| d.length > 1}.join('/')
|
||||
difference = difference.length > 0 ? " #{@c[:dark]}| #{@c[:new]}#{difference}#{@c[:rst]}" : ""
|
||||
|
||||
|
||||
# If no changes, just display green no changes message and exit here
|
||||
if @git_status == ""
|
||||
puts "%s#%s On branch: %s#{@branch}#{difference} %s| \033[0;32mNo changes (working directory clean)%s" % [
|
||||
@c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst]
|
||||
]
|
||||
exit
|
||||
end
|
||||
|
||||
|
||||
puts "%s#%s On branch: %s#{@branch}#{difference} %s| [%s*%s]%s => $#{ENV["git_env_char"]}*\n%s#%s" % [
|
||||
@c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst], @c[:dark], @c[:rst], @c[:dark], @c[:rst]
|
||||
]
|
||||
|
||||
def has_modules?
|
||||
@has_modules ||= File.exists?(File.join(@project_root, '.gitmodules'))
|
||||
end
|
||||
|
||||
# Index modification states
|
||||
@changes.each do |change|
|
||||
x, y, file = change[0, 1], change[1, 1], change[3..-1]
|
||||
|
||||
# Fetch the long git status once, but only if any submodules have changed
|
||||
if not @git_status_long and has_modules?
|
||||
@gitmodules ||= File.read(File.join(@project_root, '.gitmodules'))
|
||||
# If changed 'file' is actually a git submodule
|
||||
if @gitmodules.include?(file)
|
||||
# Parse long git status for submodule summaries
|
||||
@git_status_long = `git status`.gsub(/\033\[[^m]*m/, "") # (strip colors)
|
||||
end
|
||||
def initialize(request = nil)
|
||||
@request = request
|
||||
@status = get_status
|
||||
@ahead, @behind = parse_remote_stat
|
||||
@grouped_changes = parse_changes
|
||||
@index = 0
|
||||
end
|
||||
|
||||
|
||||
msg, col, group = case change[0..1]
|
||||
when "DD"; [" both deleted", :del, :unmerged]
|
||||
when "AU"; [" added by us", :new, :unmerged]
|
||||
when "UD"; ["deleted by them", :del, :unmerged]
|
||||
when "UA"; [" added by them", :new, :unmerged]
|
||||
when "DU"; [" deleted by us", :del, :unmerged]
|
||||
when "AA"; [" both added", :new, :unmerged]
|
||||
when "UU"; [" both modified", :mod, :unmerged]
|
||||
when /M./; [" modified", :mod, :staged]
|
||||
when /A./; [" new file", :new, :staged]
|
||||
when /D./; [" deleted", :del, :staged]
|
||||
when /R./; [" renamed", :ren, :staged]
|
||||
when /C./; [" copied", :cpy, :staged]
|
||||
when /T./; ["typechange", :typ, :staged]
|
||||
when "??"; [" untracked", :unt, :untracked]
|
||||
def raise_index!
|
||||
@index += 1
|
||||
end
|
||||
|
||||
# Store data
|
||||
@stat_hash[group] << {:msg => msg, :col => col, :file => file} if msg
|
||||
|
||||
# Work tree modification states
|
||||
if y == "M"
|
||||
@stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => file}
|
||||
elsif y == "D" && x != "D" && x != "U"
|
||||
# Don't show deleted 'y' during a merge conflict.
|
||||
@stat_hash[:unstaged] << {:msg => " deleted", :col => :del, :file => file}
|
||||
elsif y == "T"
|
||||
@stat_hash[:unstaged] << {:msg => "typechange", :col => :typ, :file => file}
|
||||
def branch
|
||||
@status.lines.first.strip[/^On branch (.*)$/, 1]
|
||||
end
|
||||
end
|
||||
|
||||
def relative_path(base, target)
|
||||
back = ""
|
||||
while target.sub(base,'') == target
|
||||
base = base.sub(/\/[^\/]*$/, '')
|
||||
back = "../#{back}"
|
||||
end
|
||||
"#{back}#{target.sub("#{base}/",'')}"
|
||||
end
|
||||
|
||||
|
||||
# Output files
|
||||
def output_file_group(group)
|
||||
# Print colored hashes & files based on modification groups
|
||||
c_group = "\033[0;#{@group_c[group]}"
|
||||
|
||||
@stat_hash[group].each do |h|
|
||||
@e += 1
|
||||
padding = (@e < 10 && @changes.size >= 10) ? " " : ""
|
||||
|
||||
# Find relative path, i.e. ../../lib/path/to/file
|
||||
rel_file = relative_path(Dir.pwd, File.join(@project_root, h[:file]))
|
||||
|
||||
# If some submodules have changed, parse their summaries from long git status
|
||||
sub_stat = nil
|
||||
if @git_status_long && (sub_stat = @git_status_long[/#{h[:file]} \((.*)\)/, 1])
|
||||
# Format summary with parantheses
|
||||
sub_stat = "(#{sub_stat})"
|
||||
end
|
||||
|
||||
puts "#{c_group}##{@c[:rst]} #{@c[h[:col]]}#{h[:msg]}:\
|
||||
#{padding}#{@c[:dark]} [#{@c[:rst]}#{@e}#{@c[:dark]}] #{c_group}#{rel_file}#{@c[:rst]} #{sub_stat}"
|
||||
# Save the ordered list of output files
|
||||
# fetch first file (in the case of oldFile -> newFile) and remove quotes
|
||||
@output_files << if h[:msg] == "typechange"
|
||||
# Only use relative paths for 'typechange' modifications.
|
||||
"~#{rel_file}"
|
||||
elsif h[:file] =~ /^"([^\\"]*(\\.[^"]*)*)"/
|
||||
# Handle the regex above..
|
||||
$1.gsub(/\\(.)/,'\1')
|
||||
def parse_remote_stat
|
||||
remote_line = @status.lines[1].strip
|
||||
if remote_line.match(/diverged/)
|
||||
remote_line.match(/.*(\d*).*(\d*)/).captures
|
||||
else
|
||||
# Else, strip file
|
||||
h[:file].strip
|
||||
[remote_line[/is ahead of.*by (\d*).*/, 1], remote_line[/is behind.*by (\d*).*/, 1]]
|
||||
end
|
||||
end
|
||||
|
||||
puts "#{c_group}##{@c[:rst]}" # Extra '#'
|
||||
end
|
||||
|
||||
|
||||
[[:staged, 'Changes to be committed'],
|
||||
[:unmerged, 'Unmerged paths'],
|
||||
[:unstaged, 'Changes not staged for commit'],
|
||||
[:untracked, 'Untracked files']
|
||||
].each_with_index do |data, i|
|
||||
group, heading = *data
|
||||
|
||||
# Allow filtering by specific group (by string or integer)
|
||||
if !ARGV[0] || ARGV[0] == group.to_s || ARGV[0] == (i+1).to_s; then
|
||||
if !@stat_hash[group].empty?
|
||||
c_arrow="\033[1;#{@group_c[group]}"
|
||||
c_hash="\033[0;#{@group_c[group]}"
|
||||
puts "#{c_arrow}➤#{@c[:header]} #{heading}\n#{c_hash}##{@c[:rst]}"
|
||||
output_file_group(group)
|
||||
def parse_changes
|
||||
scanner = StringScanner.new(@status)
|
||||
requested_groups.each_with_object({}) do |(type, identifier), h|
|
||||
if scanner.scan_until(/#{identifier}/)
|
||||
scan_until_next_empty_line(scanner)
|
||||
file_block = scan_until_next_empty_line(scanner)
|
||||
h[type] = extract_changes(file_block)
|
||||
end
|
||||
scanner.reset
|
||||
end
|
||||
end
|
||||
|
||||
def scan_until_next_empty_line(scanner)
|
||||
scanner.scan_until(/\n\n/)
|
||||
end
|
||||
|
||||
def extract_changes(str)
|
||||
str.lines.map do |line|
|
||||
new_git_change(*Regexp.last_match.captures) if line.match(/\t(.*:)?(.*)/)
|
||||
end.compact # in case there were any non matching lines left
|
||||
end
|
||||
|
||||
def new_git_change(status, file_and_message)
|
||||
status = 'untracked' unless status
|
||||
GitChange.new(file_and_message, status)
|
||||
end
|
||||
|
||||
def requested_groups
|
||||
@request.empty? ? GROUPS : select_groups
|
||||
end
|
||||
|
||||
def select_groups
|
||||
req = parse_request
|
||||
GROUPS.select { |k, _| k == req }
|
||||
end
|
||||
|
||||
def parse_request
|
||||
if @request.match(/\d/)
|
||||
GROUPS.keys[@request.to_i - 1]
|
||||
else
|
||||
@request.to_sym
|
||||
end
|
||||
end
|
||||
|
||||
def report
|
||||
print_header
|
||||
print_groups
|
||||
puts filelist if anything_changed?
|
||||
end
|
||||
|
||||
def print_header
|
||||
puts delimiter(:header) + header
|
||||
puts delimiter(:header) if anything_changed?
|
||||
end
|
||||
|
||||
def filelist
|
||||
"@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}"
|
||||
end
|
||||
|
||||
def print_groups
|
||||
@grouped_changes.each do |type, changes|
|
||||
print_group_header(type)
|
||||
puts delimiter(type)
|
||||
changes.each do |change|
|
||||
raise_index!
|
||||
print delimiter(type)
|
||||
puts change.report_with_index(@index, type, padding)
|
||||
end
|
||||
puts delimiter(type)
|
||||
end
|
||||
end
|
||||
|
||||
def print_group_header(type)
|
||||
puts "#{gmu('➤', type, 1)} #{GROUPS[type]}"
|
||||
end
|
||||
|
||||
def all_changes
|
||||
@all_changes ||= @grouped_changes.values.flatten
|
||||
end
|
||||
|
||||
def padding
|
||||
@padding ||= all_changes.map { |change| change.status.size }.max + 5
|
||||
end
|
||||
|
||||
def ahead
|
||||
"+#{@ahead}" if @ahead
|
||||
end
|
||||
|
||||
def behind
|
||||
"-#{@behind}" if @behind
|
||||
end
|
||||
|
||||
def difference
|
||||
[behind, ahead].compact.join('/')
|
||||
end
|
||||
|
||||
# markup
|
||||
def mu(str, col_in, col_out = :rst)
|
||||
return if str.empty?
|
||||
"#{COL[col_in]}#{str}#{COL[col_out]}"
|
||||
end
|
||||
|
||||
# group markup
|
||||
def gmu(str, group, boldness = 0, col_out = :rst)
|
||||
"\033[#{boldness};#{GR_COL[group]}#{str}#{COL[:rst]}"
|
||||
end
|
||||
|
||||
def header
|
||||
parts = [[:branch, :branch], [:difference, :new]]
|
||||
parts << (anything_changed? ? [:hotkey, :dark] : [:clean_state, :mod]) # mod is green
|
||||
# compact because difference might return nil
|
||||
"On branch: #{parts.map { |meth, col| mu(send(meth), col) }.compact.join(' | ')}"
|
||||
end
|
||||
|
||||
def anything_changed?
|
||||
@grouped_changes.any?
|
||||
end
|
||||
|
||||
def branch_difference_and_hotkey
|
||||
[mu(branch, :branch), mu(difference, :new), mu(hotkey, :dark)].compact.join(' | ')
|
||||
end
|
||||
|
||||
def clean_state
|
||||
"No changes (working directory clean)"
|
||||
end
|
||||
|
||||
def hotkey
|
||||
"[*] => $#{ENV['git_env_char']}"
|
||||
end
|
||||
|
||||
def delimiter(col)
|
||||
gmu("# ", col)
|
||||
end
|
||||
|
||||
def gc(color)
|
||||
end
|
||||
|
||||
def get_status
|
||||
`git status 2>/dev/null`
|
||||
end
|
||||
end
|
||||
|
||||
print "@@filelist@@::"
|
||||
puts @output_files.map {|f|
|
||||
# If file starts with a '~', treat it as a relative path.
|
||||
# This is important when dealing with symlinks
|
||||
f.start_with?("~") ? f.sub(/~/, '') : File.join(@project_root, f)
|
||||
}.join("|")
|
||||
class GitChange < GitStatus
|
||||
attr_reader :status
|
||||
def initialize(file_and_message, status)
|
||||
# destructively cut out the submodules message
|
||||
# strip the remaining for to get rid of padding
|
||||
@message = file_and_message.slice!(/\(.*\)/)
|
||||
@file = file_and_message.strip
|
||||
@status = status.strip
|
||||
end
|
||||
|
||||
STATUS_COLORS = {
|
||||
"both deleted" => :del,
|
||||
"added by us" => :new,
|
||||
"deleted by them" => :del,
|
||||
"added by them" => :new,
|
||||
"deleted by us" => :del,
|
||||
"both added" => :new,
|
||||
"both modified" => :mod,
|
||||
"modified" => :mod,
|
||||
"new file" => :new,
|
||||
"deleted" => :del,
|
||||
"renamed" => :ren,
|
||||
"copied" => :cpy,
|
||||
"typechange" => :typ,
|
||||
"untracked" => :unt,
|
||||
}
|
||||
|
||||
def color_key
|
||||
# we most likely have a : with us
|
||||
STATUS_COLORS[@status.chomp(':')]
|
||||
end
|
||||
|
||||
def report_with_index(index, type, padding = 0)
|
||||
"#{pad(padding)}#{mu(@status, color_key)} " +
|
||||
"#{mu("[#{index}]", :dark)} #{gmu(@file, type)} #{@message}"
|
||||
end
|
||||
|
||||
def pad(padding)
|
||||
' ' * (padding - @status.size)
|
||||
end
|
||||
|
||||
def absolute_path
|
||||
File.expand_path(@file, Dir.pwd)
|
||||
end
|
||||
end
|
||||
|
||||
GitStatus.new('').report
|
||||
|
||||
Reference in New Issue
Block a user