#!/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 parameter to just show one modification state # # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked # -------------------------------------------------------------------- @project_root = File.exist?(".git") ? Dir.pwd : `git rev-parse --git-dir 2> /dev/null`.sub(/\/\.git$/, '').strip @git_status = `git status --porcelain 2> /dev/null` git_branch = `git branch -v 2> /dev/null` @branch = git_branch[/^\* (\(no branch\)|[^ ]*)/, 1] @ahead = git_branch[/^\* [^ ]* *[^ ]* *\[ahead ?(\d+)\]/, 1] @changes = @git_status.split("\n") # Exit if too many changes exit if @changes.size > ENV["gs_max_changes"].to_i # Colors @c = { :rst => "\e[0m", :del => "\e[0;31m", :mod => "\e[0;32m", :new => "\e[0;33m", :ren => "\e[0;34m", :cpy => "\e[0;33m", :typ => "\e[0;35m", :unt => "\e[0;36m", :dark => "\e[2;37m", :branch => "\e[1m", :header => "\e[0m" } # Following colors must be prepended with modifiers e.g. '\e[1;', '\e[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 ahead we are from origin ahead = @ahead ? " #{@c[:dark]}| #{@c[:new]}+#{@ahead}#{@c[:rst]}" : "" # If no changes, just display green no changes message and exit here if @git_status == "" puts "%s#%s On branch: %s#{@branch}#{ahead} %s| \e[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}#{ahead} %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(/\e\[[^m]*m/, "") # (strip colors) end 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] 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} 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 = "\e[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') else # Else, strip file h[:file].strip 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="\e[1;#{@group_c[group]}" c_hash="\e[0;#{@group_c[group]}" puts "#{c_arrow}➤#{@c[:header]} #{heading}\n#{c_hash}##{@c[:rst]}" output_file_group(group) end 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("|")