Refactor, commenting, polishing.

This commit is contained in:
LFDM
2014-01-29 22:53:20 +01:00
parent 33b561ebc1
commit fe1d4435a7

View File

@@ -1,54 +1,55 @@
#!/usr/bin/env ruby #!/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)
# ------------------------------------------------------------------------------
#
# Original work by Nathan Broadbent
# Rewritten by LFDM
#
# A much faster implementation of git_status_shortcuts() in ruby
# (original benchmarks - bash: 0m0.549s, ruby: 0m0.045s, the updated
# version is twice as fast, especially noticable in big repos)
#
#
# 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', 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' require 'strscan'
class GitStatus class GitStatus
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"
}
# 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"
}
GROUPS = {
staged: 'Changes to be committed',
unmerged: 'Unmerged paths',
unstaged: 'Changes not staged for commit',
untracked: 'Untracked files'
}
def initialize(request = nil) def initialize(request = nil)
@request = request @request = request.to_s # capture nils
@status = get_status @status = get_status
@ahead, @behind = parse_remote_stat @ahead, @behind = parse_remote_stat
@grouped_changes = parse_changes @grouped_changes = parse_changes
@index = 0 @index = 0
end end
def raise_index! def report
@index += 1 print_header
print_groups
puts filelist if @grouped_changes.any?
end end
def branch
@status.lines.first.strip[/^On branch (.*)$/, 1] ######### Parsing methods #########
def get_status
`git status 2>/dev/null`
end end
# Remote info is always on the second line of git status
def parse_remote_stat def parse_remote_stat
remote_line = @status.lines[1].strip remote_line = @status.lines[1].strip
if remote_line.match(/diverged/) if remote_line.match(/diverged/)
@@ -58,6 +59,21 @@ class GitStatus
end end
end end
# We have to resort to the StringScanner to stay away from overly complex
# regular expressions.
# The individual blocks are always formatted the same
#
# identifier Changes not staged for commit
# helper text (use "git add <file>..." ...
# empty line
# changed files, leaded by a tab modified: file
# deleted: other_file
# empty line
# next identifier Untracked files
# ...
#
# We parse each requested group and return a grouped hash, its values are
# arrays of GitChange objects.
def parse_changes def parse_changes
scanner = StringScanner.new(@status) scanner = StringScanner.new(@status)
requested_groups.each_with_object({}) do |(type, identifier), h| requested_groups.each_with_object({}) do |(type, identifier), h|
@@ -74,6 +90,10 @@ class GitStatus
scanner.scan_until(/\n\n/) scanner.scan_until(/\n\n/)
end end
# Matches
# modified: file # usual output in git status
# modified: file (untracked content) # output for submodules
# file # untracked files have no additional info
def extract_changes(str) def extract_changes(str)
str.lines.map do |line| str.lines.map do |line|
new_git_change(*Regexp.last_match.captures) if line.match(/\t(.*:)?(.*)/) new_git_change(*Regexp.last_match.captures) if line.match(/\t(.*:)?(.*)/)
@@ -81,10 +101,19 @@ class GitStatus
end end
def new_git_change(status, file_and_message) def new_git_change(status, file_and_message)
status = 'untracked' unless status status = 'untracked:' unless status
GitChange.new(file_and_message, status) GitChange.new(file_and_message, status)
end end
GROUPS = {
staged: 'Changes to be committed',
unmerged: 'Unmerged paths',
unstaged: 'Changes not staged for commit',
untracked: 'Untracked files'
}
# Returns all possible groups when there was no request at all,
# otherwise selects groups by name or integer
def requested_groups def requested_groups
@request.empty? ? GROUPS : select_groups @request.empty? ? GROUPS : select_groups
end end
@@ -102,21 +131,14 @@ class GitStatus
end end
end end
def report
print_header ######### Outputting methods #########
print_groups
puts filelist if anything_changed?
end
def print_header def print_header
puts delimiter(:header) + header puts delimiter(:header) + header
puts delimiter(:header) if anything_changed? puts delimiter(:header) if anything_changed?
end end
def filelist
"@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}"
end
def print_groups def print_groups
@grouped_changes.each do |type, changes| @grouped_changes.each do |type, changes|
print_group_header(type) print_group_header(type)
@@ -134,12 +156,11 @@ class GitStatus
puts "#{gmu('➤', type, 1)} #{GROUPS[type]}" puts "#{gmu('➤', type, 1)} #{GROUPS[type]}"
end end
def all_changes
@all_changes ||= @grouped_changes.values.flatten
end
def padding ######### Items of interest #########
@padding ||= all_changes.map { |change| change.status.size }.max + 5
def branch
@status.lines.first.strip[/^On branch (.*)$/, 1]
end end
def ahead def ahead
@@ -154,17 +175,6 @@ class GitStatus
[behind, ahead].compact.join('/') [behind, ahead].compact.join('/')
end 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 def header
parts = [[:branch, :branch], [:difference, :new]] parts = [[:branch, :branch], [:difference, :new]]
parts << (anything_changed? ? [:hotkey, :dark] : [:clean_state, :mod]) # mod is green parts << (anything_changed? ? [:hotkey, :dark] : [:clean_state, :mod]) # mod is green
@@ -172,14 +182,6 @@ class GitStatus
"On branch: #{parts.map { |meth, col| mu(send(meth), col) }.compact.join(' | ')}" "On branch: #{parts.map { |meth, col| mu(send(meth), col) }.compact.join(' | ')}"
end 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 def clean_state
"No changes (working directory clean)" "No changes (working directory clean)"
end end
@@ -188,62 +190,137 @@ class GitStatus
"[*] => $#{ENV['git_env_char']}" "[*] => $#{ENV['git_env_char']}"
end end
# used to delimit the left side of the screen - looks nice
def delimiter(col) def delimiter(col)
gmu("# ", col) gmu("# ", col)
end end
def gc(color) def filelist
"@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}"
end end
def get_status
`git status 2>/dev/null` ######### Helper Methods #########
# To know about changes we could ask if there are any parsing results, as in
# @grouped_changes.any?, but that is not a good idea, since
# we might have selected a requested group before parsing already.
# Groups could be empty while there are in fact changes present,
# there we look into the original status string once
def anything_changed?
@any_changes ||=
! @status.match(/nothing to commit.*working directory clean/)
end
# needed by hotkey filelist
def raise_index!
@index += 1
end
def all_changes
@all_changes ||= @grouped_changes.values.flatten
end
# Dynamic padding, always looks for the longest status string present
# and adds a little whitespace
def padding
@padding ||= all_changes.map { |change| change.status.size }.max + 5
end
######### Markup/Color methods #########
COL = {
:rst => "0",
:header => "0",
:branch => "1",
:del => "0;31",
:mod => "0;32",
:new => "0;33",
:ren => "0;34",
:cpy => "0;33",
:typ => "0;35",
:unt => "0;36",
:dark => "2;37",
}
GR_COL = {
:staged => "33",
:unmerged => "31",
:unstaged => "32",
:untracked => "36",
}
# markup
def mu(str, col_in, col_out = :rst)
return if str.empty?
col_in = "\033[#{COL[col_in]}m"
col_out = "\033[#{COL[col_out]}m"
with_color(str, col_in, col_out)
end
# group markup
def gmu(str, group, boldness = 0, col_out = :rst)
group_col = "\033[#{boldness};#{GR_COL[group]}m"
col_out = "\033[#{COL[col_out]}m"
with_color(str, group_col, col_out)
end
def with_color(str, col_in, col_out)
"#{col_in}#{str}#{col_out}"
end end
end end
class GitChange < GitStatus class GitChange < GitStatus
attr_reader :status attr_reader :status
# Restructively singles out the submodules message and
# strips the remaining string to get rid of padding
def initialize(file_and_message, 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!(/\(.*\)/) @message = file_and_message.slice!(/\(.*\)/)
@file = file_and_message.strip @file = file_and_message.strip
@status = status.strip @status = status.strip
end end
def absolute_path
File.expand_path(@file, Dir.pwd)
end
STATUS_COLORS = { STATUS_COLORS = {
"copied" => :cpy,
"both deleted" => :del, "both deleted" => :del,
"added by us" => :new,
"deleted by them" => :del,
"added by them" => :new,
"deleted by us" => :del, "deleted by us" => :del,
"both added" => :new, "deleted by them" => :del,
"deleted" => :del,
"both modified" => :mod, "both modified" => :mod,
"modified" => :mod, "modified" => :mod,
"added by them" => :new,
"added by us" => :new,
"both added" => :new,
"new file" => :new, "new file" => :new,
"deleted" => :del,
"renamed" => :ren, "renamed" => :ren,
"copied" => :cpy,
"typechange" => :typ, "typechange" => :typ,
"untracked" => :unt, "untracked" => :unt,
} }
def color_key # Looks like this
# we most likely have a : with us #
STATUS_COLORS[@status.chomp(':')] # PADDING STATUS INDEX FILE MESSAGE (optional)
end # modified: [1] changed_file (untracked content)
#
def report_with_index(index, type, padding = 0) def report_with_index(index, type, padding = 0)
"#{pad(padding)}#{mu(@status, color_key)} " + "#{pad(padding)}#{mu(@status, color_key)} " +
"#{mu("[#{index}]", :dark)} #{gmu(@file, type)} #{@message}" "#{mu("[#{index}]", :dark)} #{gmu(@file, type)} #{@message}"
end end
# we most likely have a : with us which we don't need here
def color_key
STATUS_COLORS[@status.chomp(':')]
end
def pad(padding) def pad(padding)
' ' * (padding - @status.size) ' ' * (padding - @status.size)
end end
def absolute_path
File.expand_path(@file, Dir.pwd)
end
end end
GitStatus.new('').report GitStatus.new(ARGV.first).report