From fe1d4435a7fa3333ca7198f8638d8b788a4d9421 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Wed, 29 Jan 2014 22:53:20 +0100 Subject: [PATCH] Refactor, commenting, polishing. --- lib/git/status_shortcuts.rb | 259 +++++++++++++++++++++++------------- 1 file changed, 168 insertions(+), 91 deletions(-) diff --git a/lib/git/status_shortcuts.rb b/lib/git/status_shortcuts.rb index 71eeacf..f76f1a5 100755 --- a/lib/git/status_shortcuts.rb +++ b/lib/git/status_shortcuts.rb @@ -1,54 +1,55 @@ #!/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 parameter to just show one modification state +# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked +# -------------------------------------------------------------------- +# require 'strscan' 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) - @request = request + @request = request.to_s # capture nils @status = get_status @ahead, @behind = parse_remote_stat @grouped_changes = parse_changes @index = 0 end - def raise_index! - @index += 1 + def report + print_header + print_groups + puts filelist if @grouped_changes.any? end - def branch - @status.lines.first.strip[/^On branch (.*)$/, 1] + + ######### Parsing methods ######### + + def get_status + `git status 2>/dev/null` end + # Remote info is always on the second line of git status def parse_remote_stat remote_line = @status.lines[1].strip if remote_line.match(/diverged/) @@ -58,6 +59,21 @@ class GitStatus 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 ..." ... + # 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 scanner = StringScanner.new(@status) requested_groups.each_with_object({}) do |(type, identifier), h| @@ -74,6 +90,10 @@ class GitStatus scanner.scan_until(/\n\n/) 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) str.lines.map do |line| new_git_change(*Regexp.last_match.captures) if line.match(/\t(.*:)?(.*)/) @@ -81,10 +101,19 @@ class GitStatus end def new_git_change(status, file_and_message) - status = 'untracked' unless status + status = 'untracked:' unless status GitChange.new(file_and_message, status) 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 @request.empty? ? GROUPS : select_groups end @@ -102,21 +131,14 @@ class GitStatus end end - def report - print_header - print_groups - puts filelist if anything_changed? - end + + ######### Outputting methods ######### 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) @@ -134,12 +156,11 @@ class GitStatus 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 + ######### Items of interest ######### + + def branch + @status.lines.first.strip[/^On branch (.*)$/, 1] end def ahead @@ -154,17 +175,6 @@ class GitStatus [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 @@ -172,14 +182,6 @@ class GitStatus "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 @@ -188,62 +190,137 @@ class GitStatus "[*] => $#{ENV['git_env_char']}" end + # used to delimit the left side of the screen - looks nice def delimiter(col) gmu("# ", col) end - def gc(color) + def filelist + "@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}" 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 class GitChange < GitStatus 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) - # 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 + def absolute_path + File.expand_path(@file, Dir.pwd) + end + STATUS_COLORS = { + "copied" => :cpy, "both deleted" => :del, - "added by us" => :new, - "deleted by them" => :del, - "added by them" => :new, "deleted by us" => :del, - "both added" => :new, + "deleted by them" => :del, + "deleted" => :del, "both modified" => :mod, "modified" => :mod, + "added by them" => :new, + "added by us" => :new, + "both added" => :new, "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 - + # Looks like this + # + # PADDING STATUS INDEX FILE MESSAGE (optional) + # modified: [1] changed_file (untracked content) + # def report_with_index(index, type, padding = 0) "#{pad(padding)}#{mu(@status, color_key)} " + "#{mu("[#{index}]", :dark)} #{gmu(@file, type)} #{@message}" 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) ' ' * (padding - @status.size) end - - def absolute_path - File.expand_path(@file, Dir.pwd) - end end -GitStatus.new('').report +GitStatus.new(ARGV.first).report