From 33b561ebc1785b6c324c310468d7203dca0d70f3 Mon Sep 17 00:00:00 2001 From: LFDM <1986gh@gmail.com> Date: Wed, 29 Jan 2014 21:05:24 +0100 Subject: [PATCH 01/79] status_shortcuts.rb completely rewritten --- lib/git/status_shortcuts.rb | 421 ++++++++++++++++++++---------------- 1 file changed, 229 insertions(+), 192 deletions(-) mode change 100644 => 100755 lib/git/status_shortcuts.rb diff --git a/lib/git/status_shortcuts.rb b/lib/git/status_shortcuts.rb old mode 100644 new mode 100755 index d5c8886..71eeacf --- a/lib/git/status_shortcuts.rb +++ b/lib/git/status_shortcuts.rb @@ -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 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 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 02/79] 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 From 802f4987c36730d6b9496c7a96374bfee5b1695e Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Wed, 15 Oct 2014 12:57:28 -0400 Subject: [PATCH 03/79] set up multi-os testing in travis Requires beta access, thankfully the travis gods have blessed us for the main repo and my fork! This should allow us to make sure scm_breeze operates reliably in both Linux and BSD/Darwin, because there are small shell differences (especially with default tools) that are causing errors I noticed on MacOSX. --- .travis.yml | 12 +++++++++--- test/support/travisci_deps.sh | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100755 test/support/travisci_deps.sh diff --git a/.travis.yml b/.travis.yml index 3d40388..30f5fa2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,9 @@ -script: ./run_tests.sh -before_script: - - sudo apt-get install zsh +os: + - linux + - osx + +install: + - ./test/support/travisci_deps.sh + +script: + - ./run_tests.sh diff --git a/test/support/travisci_deps.sh b/test/support/travisci_deps.sh new file mode 100755 index 0000000..d4523ee --- /dev/null +++ b/test/support/travisci_deps.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Installs dependencies for travis-ci environments. + +# Install dependencies, which looks to be just bash & zsh. +# +# Darwin has zsh preinstalled already, so only need to install on Ubuntu. +# +# Note: $TRAVIS_OS_NAME will only be set on text boxes with multi-os enabled, +# so use negation test so it will fail gracefully on normal Travis linux setup. +# +# TODO: also perhaps later only on ZSH test box if we split those +if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then + sudo apt-get install zsh +fi From b6fd7ae829e2060a251fbe5cd06c7ab759f92e75 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Wed, 15 Oct 2014 14:12:24 -0400 Subject: [PATCH 04/79] split tests up by shell for concurrency More parallelism = faster tests. Also better isolation for changes that might only break on zsh or bash respectively. This is defined via env variable, so someone running locally will have all tests run sequentially as before. --- .travis.yml | 6 ++++++ run_tests.sh | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30f5fa2..7abf0e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,12 @@ os: - linux - osx +env: + - TEST_SHELLS="zsh bash" + - TEST_SHELLS=bash + - TEST_SHELLS=zsh + - HI=mom + install: - ./test/support/travisci_deps.sh diff --git a/run_tests.sh b/run_tests.sh index fff4438..877f9ed 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -3,8 +3,15 @@ failed=false +# allow list of shells to run tests in to be overriden by environment variable +# if empty or null, use defaults +if [ -z "$TEST_SHELLS" ]; then + TEST_SHELLS="bash zsh" +fi + +echo "== Will run all tests with following shells: ${TEST_SHELLS}" for test in $(find test/lib -name *_test.sh); do - for shell in bash zsh; do + for shell in $TEST_SHELLS; do echo "== Running tests with [$shell]: $test" $shell $test || failed=true done @@ -16,4 +23,4 @@ if [ "$failed" = "true" ]; then else echo "All tests passed!" return 0; -fi \ No newline at end of file +fi From e7c56c7647ea491da3ecd13c80456c6f8b11d76a Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Wed, 15 Oct 2014 16:36:43 -0400 Subject: [PATCH 05/79] only install zsh on boxes that need it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit don’t bother on macosx (preinstalled) or if the test matrix for that box isn’t going to be testing in zsh. this should speed up test runs for most cases, and later we can define fast_failure and not have to wait for the longer ones. --- test/support/travisci_deps.sh | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test/support/travisci_deps.sh b/test/support/travisci_deps.sh index d4523ee..83cc32e 100755 --- a/test/support/travisci_deps.sh +++ b/test/support/travisci_deps.sh @@ -7,8 +7,26 @@ # # Note: $TRAVIS_OS_NAME will only be set on text boxes with multi-os enabled, # so use negation test so it will fail gracefully on normal Travis linux setup. -# -# TODO: also perhaps later only on ZSH test box if we split those if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then - sudo apt-get install zsh + + # okay, so we know we're probably on a linux box (or at least not an osx box) + # at this point. do we need to install zsh? let's say the default case is no: + needs_zsh=false + + # check if zsh is listed in the TEST_SHELLS environment variable, set by + # our travis-ci build matrix. + if [[ $TEST_SHELLS =~ zsh ]]; then needs_zsh=true; fi + + # if there is NO $TEST_SHELLS env variable persent (which should never happen, + # but maybe someone has been monkeying with the .travis.yml), run_tests.sh is + # going to fall back onto the default of testing everything, so we need zsh. + if [[ -z "$TEST_SHELLS" ]]; then needs_zsh=true; fi + + # finally, we install zsh if needed! + if $needs_zsh ; then + sudo apt-get install zsh + else + echo "No deps required." + fi + fi From f056d28b2eb1f20793119a5c48f75bef7f9954d7 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 11:39:08 -0400 Subject: [PATCH 06/79] remove redundant test envs they were just there when we were making sure the env variable shell selection was working properly in all cases, now that we know it is, they can be removed. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7abf0e3..f2597de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,8 @@ os: - osx env: - - TEST_SHELLS="zsh bash" - TEST_SHELLS=bash - TEST_SHELLS=zsh - - HI=mom install: - ./test/support/travisci_deps.sh From b395596496373aeb90bbdd9fb3fddcfa7c01cb48 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 12:19:11 -0400 Subject: [PATCH 07/79] dont use grep -P for cross-platform compatibility `grep -P` gives access to the PCRE matching engine, which is nice, however, the version of grep shipped on many BSD systems (including Darwin) does not have this flag. Currently, `grep -P` was being used in the `_includes()` test helper. For cross platform compatibility in tests, do not rely upon this option. Luckily, all existing tests seem to work fine without it already! --- test/lib/git/shell_shortcuts_test.sh | 2 +- test/support/test_helper.sh | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 3a6843a..834f03b 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -82,7 +82,7 @@ test_ls_with_file_shortcuts() { ls_with_file_shortcuts > $temp_file ls_output=$(<$temp_file strip_colors) - # Compare as fixed strings (F), instead of regex (P) + # Compare as fixed strings (F), instead of normal grep behavior assertIncludes "$ls_output" '[1] a "b"' F assertIncludes "$ls_output" "[2] a 'b'" F assertIncludes "$ls_output" '[3] a [b]' F diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index 7407b59..cf211a6 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -34,7 +34,7 @@ verboseGitCommands() { #----------------------------------------------------------------------------- _includes() { - if [ -n "$3" ]; then regex="$3"; else regex=P; fi + if [ -n "$3" ]; then regex="$3"; else regex=''; fi if echo "$1" | grep -q$regex "$2"; then echo 0; else echo 1; fi } @@ -46,4 +46,3 @@ assertIncludes() { assertNotIncludes() { assertFalse "'$1' should not have contained '$2'" $(_includes "$@") } - From b2d86a16b2a0a53c8c0c176909fbdc0ec22ee732 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 15:31:12 -0400 Subject: [PATCH 08/79] use perl instead of sed -r to strip colors in test `sed -r` is not present on some BSD based systems, including MacOSX Darwin. perl is standard pretty much everywhere, so this a more reliable way to test. Since this is only used for tests, any performance differences should not matter significantly. --- test/support/test_helper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index cf211a6..ba9e472 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -15,7 +15,7 @@ fi # Strip color codes from a string strip_colors() { - sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" + perl -pe 's/\e\[[\d;]*m//g' } # Print space separated tab completion options From 1cd162434fb72c9050602703a76677707346a649 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 16:10:52 -0400 Subject: [PATCH 09/79] Make sure we have physical path back from mktemp Darwin actually symlinks /var inside /private, but mktemp reports back the logical pathat time of file creation. So make sure we always get the full physical path to be absolutely certain when doing comparisons later, because thats how the Ruby status_shortcuts.rb script is going to obtain them. --- test/lib/git/shell_shortcuts_test.sh | 8 ++++++++ test/lib/git/status_shortcuts_test.sh | 1 + 2 files changed, 9 insertions(+) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 834f03b..2ace560 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -71,7 +71,15 @@ test_ls_with_file_shortcuts() { export git_env_char="e" TEST_DIR=$(mktemp -d -t scm_breeze.XXXXXXXXXX) + + # Darwin actually symlinks /var inside /private, but mktemp reports back the + # logical pathat time of file creation. So make sure we always get the + # full physical path to be absolutely certain when doing comparisons later, + # because thats how the Ruby status_shortcuts.rb script is going to obtain + # them. cd $TEST_DIR + TEST_DIR=`pwd -P` + touch 'test file' 'test_file' mkdir -p "a [b]" 'a "b"' "a 'b'" touch "a \"b\"/c" diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index aa86d2e..818183f 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -29,6 +29,7 @@ oneTimeSetUp() { export ga_auto_remove="yes" testRepo=$(mktemp -d -t scm_breeze.XXXXXXXXXX) + testRepo=`cd $testRepo && pwd -P` } oneTimeTearDown() { From ae4b93f52f0d3fc17cbe178aaf0683b3b397926d Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 16:38:09 -0400 Subject: [PATCH 10/79] declare original location of cmds The previous tests assumed default locations for all commands, however they do not exist in the same place on all *nixes. I encountered this when the test was failing on MacOSX where it was expected hardcoded `/bin/sed` when in fact the code was identifying the correct local expansion of `/usr/bin/sed`. To make this test more robust, it now checks for the original location of all cmds and uses that in its expansion tests for `test_shell_command_wrapping()`. --- test/lib/git/shell_shortcuts_test.sh | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 2ace560..1f8954f 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -31,6 +31,14 @@ oneTimeSetUp() { # Test functions function ln() { ln $@; } + + # Before aliasing, get original locations so we can compare them in the test + unalias mv rm sed cat 2>/dev/null + orig_mv=`which mv` + orig_rm=`which rm` + orig_sed=`which sed` + orig_cat=`which cat` + # Test aliases alias mv="nocorrect mv" alias rm="rm --option" @@ -58,11 +66,11 @@ assertAliasEquals(){ #----------------------------------------------------------------------------- test_shell_command_wrapping() { - assertAliasEquals "exec_scmb_expand_args /bin/rm --option" "rm" - assertAliasEquals "exec_scmb_expand_args nocorrect /bin/mv" "mv" - assertAliasEquals "exec_scmb_expand_args /bin/sed" "sed" - assertAliasEquals "exec_scmb_expand_args /bin/cat" "cat" - assertAliasEquals "exec_scmb_expand_args builtin cd" "cd" + assertAliasEquals "exec_scmb_expand_args $orig_rm --option" "rm" + assertAliasEquals "exec_scmb_expand_args nocorrect $orig_mv" "mv" + assertAliasEquals "exec_scmb_expand_args $orig_sed" "sed" + assertAliasEquals "exec_scmb_expand_args $orig_cat" "cat" + assertAliasEquals "exec_scmb_expand_args builtin cd" "cd" assertIncludes "$(declare -f ln)" "ln ()" assertIncludes "$(declare -f ln)" "exec_scmb_expand_args __original_ln" } From c234916b2bdb2d54b4ff7dc7c77e44029ee07c1e Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 17:08:29 -0400 Subject: [PATCH 11/79] scripts should exit rather than return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit some shells are pickier about this. darwin uses a newer version of sh that complains about it. this is the ‘proper’ behavior in either scenario. --- run_tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 877f9ed..2d5c061 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -19,8 +19,8 @@ done if [ "$failed" = "true" ]; then echo "Tests failed!" - return 1; + exit 1; else echo "All tests passed!" - return 0; + exit 0; fi From 9a9b6104e2e87a19854ac42418773a6dd51e0916 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Thu, 16 Oct 2014 17:20:49 -0400 Subject: [PATCH 12/79] Compatibility fix for Design Assets Management design.sh uses `readlink -m` to determine canonical path of git dir, which is not available on all *nix systems (including Darwin). Instead, use `pwd -P` to get canonical directory path in a more cross-platform compatible way. This is a change to an actual script rather than just a test, and it should make the Design Assets Management functionality of scm_breeze now function properly on MacOSX. --- lib/design.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/design.sh b/lib/design.sh index 5106247..d7e041a 100644 --- a/lib/design.sh +++ b/lib/design.sh @@ -23,7 +23,7 @@ # Add ignore rule to .git/info/exclude if not already present _design_add_git_exclude(){ - local git_dir="$(cd $1 && readlink -m $(git rev-parse --git-dir))" + local git_dir="$(cd $1 && cd `git rev-parse --git-dir` && pwd -P)" if [ -e "$git_dir/info/exclude" ] && ! $(grep -q "$project_design_dir" "$git_dir/info/exclude"); then echo "$project_design_dir" >> "$git_dir/info/exclude" fi From c6b9c398c89295bb97faebba4e2180fcf0ee31b3 Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Mon, 20 Oct 2014 15:16:04 -0400 Subject: [PATCH 13/79] temp before_script to debug sort order on travis I tested manually on OSX and linux, but lets make sure the same thing is true on travis-ci builds. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index f2597de..d038f60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,8 @@ env: install: - ./test/support/travisci_deps.sh +before_script: + - echo -e "test_repo_11\ntest_repo_1" | sort + script: - ./run_tests.sh From b5606ed897783d65f5995b747cd1f12ec8d7000d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Baumho=CC=88ver?= Date: Tue, 1 Nov 2016 13:33:50 +0100 Subject: [PATCH 14/79] changed alias for git_log_stat_alias from gls to glst due to issue with recent zsh update --- git.scmbrc.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git.scmbrc.example b/git.scmbrc.example index 2728b5a..e04e9b3 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -89,7 +89,7 @@ git_merge_only_fast_forward_alias="gmff" git_cherry_pick_alias="gcp" git_log_alias="gl" git_log_all_alias="gla" -git_log_stat_alias="gls" +git_log_stat_alias="glst" git_log_graph_alias="glg" git_show_alias="gsh" git_show_summary="gsm" # (gss taken by git status short) From ca5c15d4a0ee881fa8bc2b1923146adb93933b0c Mon Sep 17 00:00:00 2001 From: Nicolas Quiniou-Briand Date: Wed, 27 Jun 2018 08:44:57 -0400 Subject: [PATCH 15/79] replace 'ge' by `ge` --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 936281e..8843857 100644 --- a/README.markdown +++ b/README.markdown @@ -114,7 +114,7 @@ $ gco 5 You can use these shortcuts with system commands by passing your command -through `exec_scmb_expand_args` (default alias is 'ge'): +through `exec_scmb_expand_args` (default alias is `ge`): ```bash From 1f3b8a6b11ab1d60cdbdea56525b4aefee8326a5 Mon Sep 17 00:00:00 2001 From: Michael Mior Date: Fri, 13 Apr 2018 08:49:22 -0400 Subject: [PATCH 16/79] Use ^M instead of for newlines --- lib/git/keybindings.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/git/keybindings.sh b/lib/git/keybindings.sh index 8ec4066..11bb698 100644 --- a/lib/git/keybindings.sh +++ b/lib/git/keybindings.sh @@ -27,13 +27,13 @@ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then # Uses emacs style keybindings, so vi mode is not supported for now if ! set -o | grep -q '^vi .*on$'; then if [[ $shell == "zsh" ]]; then - _bind "$git_commit_all_keys" " git_commit_all""\n" - _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""\n" - _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""\n" + _bind "$git_commit_all_keys" " git_commit_all""^M" + _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""^M" + _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""^M" else - _bind "$git_commit_all_keys" "\" git_commit_all\n\"" - _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit \n\"" - _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all \n\"" + _bind "$git_commit_all_keys" "\" git_commit_all^M\"" + _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit ^M\"" + _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all ^M\"" fi fi From 77b1717008594e6a55fabb04ff4fcce7cbbc055f Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 17 Aug 2018 12:49:07 +0700 Subject: [PATCH 17/79] Avoid test with no arguments and unnecessary subshell --- lib/git/shell_shortcuts.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 6299537..4c427d8 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -5,9 +5,9 @@ # ------------------------------------------------------------------------------ -if test | sed -E 's///g' 2>/dev/null; then +if sed -E 's///g' /dev/null; then SED_REGEX_ARG="E" -elif test | sed -r 's///g' 2>/dev/null; then +elif sed -r 's///g' /dev/null; then SED_REGEX_ARG="r" else echo "Cannot determine extended regex argument for sed! (Doesn't respond to either -E or -r)" From 70b4e62bb04f3de8cb4f75793b9c4b1c48c6eb37 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 18 Aug 2018 14:35:43 +0700 Subject: [PATCH 18/79] Remove -- from end of aliases --- lib/git/aliases.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index 4cbdaed..9632c79 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -100,15 +100,15 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_checkout_alias" 'git' 'checkout' __git_alias "$git_commit_alias" 'git' 'commit' __git_alias "$git_commit_verbose_alias" 'git' 'commit' '--verbose' - __git_alias "$git_reset_alias" 'git' 'reset' '--' + __git_alias "$git_reset_alias" 'git' 'reset' __git_alias "$git_reset_hard_alias" 'git' 'reset' '--hard' __git_alias "$git_rm_alias" 'git' 'rm' __git_alias "$git_blame_alias" 'git' 'blame' - __git_alias "$git_diff_no_whitespace_alias" 'git' 'diff' '-w' '--' + __git_alias "$git_diff_no_whitespace_alias" 'git' 'diff' '-w' __git_alias "$git_diff_alias" 'git' 'diff' - __git_alias "$git_diff_file_alias" 'git' 'diff' '--' + __git_alias "$git_diff_file_alias" 'git' 'diff' __git_alias "$git_diff_word_alias" 'git' 'diff' '--word-diff' - __git_alias "$git_diff_cached_alias" 'git' 'diff' '--cached --' + __git_alias "$git_diff_cached_alias" 'git' 'diff' '--cached' __git_alias "$git_add_patch_alias" 'git' 'add' '-p' __git_alias "$git_add_updated_alias" 'git' 'add' '-u' __git_alias "$git_difftool_alias" 'git' 'difftool' From 92689b3a19d247f03b3e0229e44e3ec2d3835951 Mon Sep 17 00:00:00 2001 From: Wilhelmina Drengwitz Date: Fri, 31 Aug 2018 07:13:23 -0400 Subject: [PATCH 19/79] Add links to alternatives --- README.markdown | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.markdown b/README.markdown index 8843857..07abd94 100644 --- a/README.markdown +++ b/README.markdown @@ -406,3 +406,7 @@ SCMs. ***Enjoy!*** +## Alternative Projects + +1. https://github.com/shinriyo/breeze `fish` support +1. https://github.com/mroth/scmpuff static go binary From 744cae46d6f259d2076218df34a4f9648141ce69 Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 07:29:44 -0400 Subject: [PATCH 20/79] Fix uninstall for osx --- uninstall.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/uninstall.sh b/uninstall.sh index 0805a6e..f91235a 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,7 +1,15 @@ #!/bin/sh # uninstall by (github: bernardofire) # Remove line from bashrc and zshrc if present. + +sed="sed -i" +if [[ $OSTYPE == "Darwin" ]]; then + sed="sed -i ''" +fi + for rc in bashrc zshrc; do - sed -i '/scm_breeze/d' "$HOME/.$rc" - printf "Removed SCM Breeze from %s\n" "$HOME/.$rc" + if [ -f "$HOME/.$rc" ]; then + $sed '/scm_breeze/d' "$HOME/.$rc" && + printf "Removed SCM Breeze from %s\n" "$HOME/.$rc" + fi done From a3ff809cbebb593fc115e240b0c942653a302d12 Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 07:56:58 -0400 Subject: [PATCH 21/79] Fix ordering to match declare order --- test/lib/git/shell_shortcuts_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 37281d4..8bae83e 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -67,8 +67,8 @@ assertAliasEquals(){ #----------------------------------------------------------------------------- test_shell_command_wrapping() { - assertAliasEquals "exec_scmb_expand_args $rm_path --option" "rm" assertAliasEquals "exec_scmb_expand_args nocorrect $mv_path" "mv" + assertAliasEquals "exec_scmb_expand_args $rm_path --option" "rm" assertAliasEquals "exec_scmb_expand_args $sed_path" "sed" assertAliasEquals "exec_scmb_expand_args $cat_path" "cat" assertAliasEquals "exec_scmb_expand_args builtin cd" "cd" From d0c832913758ff7722b4975b9629e27b4f3ce74b Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 08:58:23 -0400 Subject: [PATCH 22/79] Fix sorting order --- lib/git/repo_index.sh | 8 ++++---- test/lib/git/repo_index_test.sh | 25 ++++++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index b197d49..3d2e9c7 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -68,8 +68,8 @@ function git_index() { elif [ "$1" = "--list" ] || [ "$1" = "-l" ]; then echo -e "$_bld_col$(_git_index_count)$_txt_col Git repositories in $_bld_col$GIT_REPO_DIR$_txt_col:\n" for repo in $(_git_index_dirs_without_home); do - echo $(basename $repo) : $repo - done | sort | column -t -s ':' + echo $(basename $repo | sed "s/ /_/g") : $repo + done | sort -t ":" -k1,1 | column -t -s ':' elif [ "$1" = "--count-by-host" ]; then echo -e "=== Producing a report of the number of repos per host...\n" _git_index_batch_cmd git remote -v | \grep "origin.*(fetch)" | @@ -148,8 +148,8 @@ function _rebuild_git_index() { # Get repos from src dir and custom dirs, then sort by basename IFS=$'\n' for repo in $(echo -e "$(_find_git_repos)\n$(echo $GIT_REPOS | sed "s/:/\\\\n/g")"); do - echo $(basename $repo | sed "s/ /_/g") $repo - done | sort | cut -d " " -f2- >| "$GIT_REPO_DIR/.git_index" + echo $(basename $repo | sed "s/ /_/g"):$repo + done | sort -t ":" -k1,1 | cut -d ":" -f2- >| "$GIT_REPO_DIR/.git_index" unset IFS if [ "$1" != "--silent" ]; then diff --git a/test/lib/git/repo_index_test.sh b/test/lib/git/repo_index_test.sh index f7006ed..c2d2571 100755 --- a/test/lib/git/repo_index_test.sh +++ b/test/lib/git/repo_index_test.sh @@ -94,16 +94,19 @@ test_repo_index_command() { git_index --rebuild > /dev/null # Test that all repos are detected, and sorted alphabetically - assertIncludes "$(index_no_newlines)" "bitbucket.*\ -blue_submodule.*\ -github.*\ -green_submodule.*\ -red_submodule.*\ -source_forge.*\ -submodules_everywhere.*\ -test_repo_11.*\ -test_repo_1" - + assertIncludes "$(index_no_newlines)" $( + cat < Date: Fri, 31 Aug 2018 08:58:41 -0400 Subject: [PATCH 23/79] Run `shfmt` --- test/lib/git/repo_index_test.sh | 70 ++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/test/lib/git/repo_index_test.sh b/test/lib/git/repo_index_test.sh index c2d2571..40879cd 100755 --- a/test/lib/git/repo_index_test.sh +++ b/test/lib/git/repo_index_test.sh @@ -7,10 +7,14 @@ # # Unit tests for git shell scripts -export scmbDir="$( cd -P "$( dirname "$0" )" && pwd )/../../.." +export scmbDir="$(cd -P "$(dirname "$0")" && pwd)/../../.." # Zsh compatibility -if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; SHUNIT_PARENT=$0; setopt shwordsplit; fi +if [ -n "${ZSH_VERSION:-}" ]; then + shell="zsh" + SHUNIT_PARENT=$0 + setopt shwordsplit +fi # Load test helpers source "$scmbDir/test/support/test_helper.sh" @@ -19,7 +23,6 @@ source "$scmbDir/test/support/test_helper.sh" source "$scmbDir/lib/scm_breeze.sh" source "$scmbDir/lib/git/repo_index.sh" - # Setup and tear down #----------------------------------------------------------------------------- oneTimeSetUp() { @@ -34,7 +37,10 @@ oneTimeSetUp() { cd $GIT_REPO_DIR # Setup test repos in temp repo dir for repo in github bitbucket source_forge TestCaps; do - mkdir $repo; cd $repo; git init; cd - > /dev/null + mkdir $repo + cd $repo + git init + cd - >/dev/null done # Add some nested dirs for testing resursive tab completion @@ -47,7 +53,7 @@ oneTimeSetUp() { mkdir submodules_everywhere cd submodules_everywhere git init - cat > .gitmodules <.gitmodules < /dev/null + mkdir $repo + cd $repo + git init + cd - >/dev/null done # Setup some custom repos outside the main repo dir IFS=":" for dir in $GIT_REPOS; do - mkdir -p $dir; cd $dir; git init; + mkdir -p $dir + cd $dir + git init done unset IFS @@ -85,13 +96,12 @@ index_no_newlines() { tr "\\n" " " < $git_index_file } - #----------------------------------------------------------------------------- # Unit tests #----------------------------------------------------------------------------- test_repo_index_command() { - git_index --rebuild > /dev/null + git_index --rebuild >/dev/null # Test that all repos are detected, and sorted alphabetically assertIncludes "$(index_no_newlines)" $( @@ -111,7 +121,7 @@ EXPECT test_check_git_index() { ensureIndex - echo "should not be regenerated" >> $git_index_file + echo "should not be regenerated" >>$git_index_file _check_git_index # Test that index is not rebuilt unless empty assertIncludes "$(index_no_newlines)" "should not be regenerated" @@ -128,7 +138,7 @@ test_git_index_count() { test_repo_list() { ensureIndex list=$(git_index --list) - assertIncludes "$list" "bitbucket" || return + assertIncludes "$list" "bitbucket" || return assertIncludes "$list" "blue_submodule" || return assertIncludes "$list" "test_repo_11" } @@ -136,16 +146,26 @@ test_repo_list() { # Test matching rules for changing directory test_git_index_changing_directory() { ensureIndex - git_index "github"; assertEquals "$GIT_REPO_DIR/github" "$PWD" - git_index "github/"; assertEquals "$GIT_REPO_DIR/github" "$PWD" - git_index "bucket"; assertEquals "$GIT_REPO_DIR/bitbucket" "$PWD" - git_index "testcaps"; assertEquals "$GIT_REPO_DIR/TestCaps" "$PWD" - git_index "green_sub"; assertEquals "$GIT_REPO_DIR/submodules_everywhere/very/nested/directory/green_submodule" "$PWD" - git_index "_submod"; assertEquals "$GIT_REPO_DIR/submodules_everywhere/very/nested/directory/blue_submodule" "$PWD" - git_index "test_repo_1"; assertEquals "/tmp/test_repo_1" "$PWD" - git_index "test_repo_11"; assertEquals "/tmp/test_repo_11" "$PWD" - git_index "test_repo_"; assertEquals "/tmp/test_repo_1" "$PWD" - git_index "github/videos/octocat/live_action"; assertEquals "$GIT_REPO_DIR/github/videos/octocat/live_action" "$PWD" + git_index "github" + assertEquals "$GIT_REPO_DIR/github" "$PWD" + git_index "github/" + assertEquals "$GIT_REPO_DIR/github" "$PWD" + git_index "bucket" + assertEquals "$GIT_REPO_DIR/bitbucket" "$PWD" + git_index "testcaps" + assertEquals "$GIT_REPO_DIR/TestCaps" "$PWD" + git_index "green_sub" + assertEquals "$GIT_REPO_DIR/submodules_everywhere/very/nested/directory/green_submodule" "$PWD" + git_index "_submod" + assertEquals "$GIT_REPO_DIR/submodules_everywhere/very/nested/directory/blue_submodule" "$PWD" + git_index "test_repo_1" + assertEquals "/tmp/test_repo_1" "$PWD" + git_index "test_repo_11" + assertEquals "/tmp/test_repo_11" "$PWD" + git_index "test_repo_" + assertEquals "/tmp/test_repo_1" "$PWD" + git_index "github/videos/octocat/live_action" + assertEquals "$GIT_REPO_DIR/github/videos/octocat/live_action" "$PWD" } test_git_index_tab_completion() { @@ -166,16 +186,15 @@ test_git_index_tab_completion() { # Test completion for project sub-directories when project ends with '/' COMP_WORDS="github/" _git_index_tab_completion - assertIncludes "$(tab_completions)" "github/videos/" + assertIncludes "$(tab_completions)" "github/videos/" # Check that '.git/' is filtered from completion, but other hidden dirs are available assertNotIncludes "$(tab_completions)" "github/.git/" - assertIncludes "$(tab_completions)" "github/.im_hidden/" + assertIncludes "$(tab_completions)" "github/.im_hidden/" COMP_WORDS="github/videos/" _git_index_tab_completion assertIncludes "$(tab_completions)" "github/videos/octocat/" - # Test that completion checks for other matching projects even if one matches perfectly COMP_WORDS="test_repo_1" _git_index_tab_completion @@ -183,7 +202,6 @@ test_git_index_tab_completion() { fi } - # Test changing to top-level directory (when arg begins with '/') test_changing_to_top_level_directory() { mkdir "$GIT_REPO_DIR/gems" @@ -191,8 +209,6 @@ test_changing_to_top_level_directory() { assertEquals "$GIT_REPO_DIR/gems" "$PWD" } - # load and run shUnit2 # Call this function to run tests source "$scmbDir/test/support/shunit2" - From f5850830fa5ea866ec563304e6072ee14cf9cef3 Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 09:01:06 -0400 Subject: [PATCH 24/79] Fix tr illegal command --- test/lib/git/repo_index_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/git/repo_index_test.sh b/test/lib/git/repo_index_test.sh index 40879cd..c866ec4 100755 --- a/test/lib/git/repo_index_test.sh +++ b/test/lib/git/repo_index_test.sh @@ -105,7 +105,7 @@ test_repo_index_command() { # Test that all repos are detected, and sorted alphabetically assertIncludes "$(index_no_newlines)" $( - cat < Date: Tue, 7 Feb 2017 21:20:17 -0500 Subject: [PATCH 25/79] Add bash & zsh compatible way to resolve abs exe paths --- test/lib/git/shell_shortcuts_test.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 8bae83e..f699085 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -21,6 +21,13 @@ fi source "$scmbDir/test/support/test_helper.sh" source "$scmbDir/lib/scm_breeze.sh" +bin_path() { + if [ -n "${ZSH_VERSION:-}" ]; + then where "$@" | tail -1 + else which "$@" + fi +} + # Setup #----------------------------------------------------------------------------- oneTimeSetUp() { @@ -35,10 +42,10 @@ oneTimeSetUp() { # Before aliasing, get original locations so we can compare them in the test unalias mv rm sed cat 2>/dev/null - export mv_path="$(which mv)" - export rm_path="$(which rm)" - export sed_path="$(which sed)" - export cat_path="$(which cat)" + export mv_path="$(bin_path mv)" + export rm_path="$(bin_path rm)" + export sed_path="$(bin_path sed)" + export cat_path="$(bin_path cat)" # Test aliases alias mv="nocorrect $mv_path" From 7189656854b87b062e7c6d36681fef2c9fbffbde Mon Sep 17 00:00:00 2001 From: David Lee Date: Fri, 31 Aug 2018 09:32:07 -0400 Subject: [PATCH 26/79] [PATCH] Add grep_shortcuts Closes #125, Thanks David Lee! --- git.scmbrc.example | 9 ++++---- lib/git/aliases.sh | 1 + lib/git/grep_shortcuts.rb | 47 +++++++++++++++++++++++++++++++++++++++ lib/git/grep_shortcuts.sh | 24 ++++++++++++++++++++ scm_breeze.sh | 1 + 5 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 lib/git/grep_shortcuts.rb create mode 100644 lib/git/grep_shortcuts.sh diff --git a/git.scmbrc.example b/git.scmbrc.example index 2728b5a..2637baa 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -34,10 +34,10 @@ git_alias="g" # 1. 'SCM Breeze' functions git_status_shortcuts_alias="gs" git_add_shortcuts_alias="ga" -git_add_patch_alias="gap" -git_add_updated_alias="gau" -git_show_files_alias="gsf" exec_scmb_expand_args_alias="ge" +git_show_files_alias="gsf" +git_commit_all_alias="gca" +git_grep_shortcuts_alias="gtrep" # 2. Commands that handle paths (with shortcut args expanded) git_checkout_alias="gco" git_checkout_branch_alias="gcb" @@ -68,7 +68,8 @@ git_status_short_alias="gss" git_clean_alias="gce" git_clean_force_alias="gcef" git_add_all_alias="gaa" -git_commit_all_alias="gca" +git_add_patch_alias="gap" +git_add_updated_alias="gau" git_commit_amend_alias="gcm" git_commit_amend_no_msg_alias="gcmh" git_commit_no_msg_alias="gch" diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index 9632c79..28b0e31 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -89,6 +89,7 @@ _alias "$git_add_shortcuts_alias" 'git_add_shortcuts' _alias "$exec_scmb_expand_args_alias" 'exec_scmb_expand_args' _alias "$git_show_files_alias" 'git_show_affected_files' _alias "$git_commit_all_alias" 'git_commit_all' +_alias "$git_grep_shortcuts_alias" 'git_grep_shortcuts' # Git Index alias _alias "$git_index_alias" 'git_index' diff --git a/lib/git/grep_shortcuts.rb b/lib/git/grep_shortcuts.rb new file mode 100644 index 0000000..9f5ecc4 --- /dev/null +++ b/lib/git/grep_shortcuts.rb @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby +# encoding: UTF-8 + +PROJECT_ROOT = File.exist?(".git") ? Dir.pwd : `\git rev-parse --show-toplevel 2> /dev/null`.strip + +COLORS = { + :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" +} + +COLOR_MATCH = /\e\[[0-9;]*[mK]/ + +output_files = [] + +stdin = STDIN.set_encoding(Encoding::ASCII_8BIT) + +while stdin.gets + if $. > 1000 + puts "Only showing first 1000 results. Please refine your search." + break + end + print "#{COLORS[:dark]}[#{COLORS[:rst]}#{$.}#{COLORS[:dark]}]#{COLORS[:rst]} " + matches = $_.match(/(^.+?)#{COLOR_MATCH}?:#{COLOR_MATCH}?(\d+)?/) + file = matches[1] + line = matches[2] + output_files << "#{file}#{line ? ":#{line}" : ""}" + puts $_ +end + +print "@@filelist@@::" + +output_files.each_with_index {|f,i| + # If file starts with a '~', treat it as a relative path. + # This is important when dealing with symlinks + print "|" unless i == 0 + print f.start_with?("~") ? f.sub(/~/, '') : File.join(PROJECT_ROOT, f) +} +puts diff --git a/lib/git/grep_shortcuts.sh b/lib/git/grep_shortcuts.sh new file mode 100644 index 0000000..d7024ea --- /dev/null +++ b/lib/git/grep_shortcuts.sh @@ -0,0 +1,24 @@ +git_grep_shortcuts() { + fail_if_not_git_repo || return 1 + git_clear_vars + # Run ruby script, store output + tmp_grep_results="$(git rev-parse --git-dir)/tmp_grep_results_$$" + git grep -n --color=always "$@" | + /usr/bin/env ruby "$scmbDir/lib/git/grep_shortcuts.rb" >"$tmp_grep_results" + + # Fetch list of files from last line of script output + files="$(tail -1 "$tmp_grep_results" | sed 's%@@filelist@@::%%g')" + + # Export numbered env variables for each file + IFS="|" + local e=1 + for file in ${=files}; do + export $git_env_char$e="$file" + let e++ + done + IFS=$' \t\n' + + # Print status + cat "$tmp_grep_results" | sed '$d' | less -SfRMXFi + rm -f "$tmp_grep_results" +} diff --git a/scm_breeze.sh b/scm_breeze.sh index 98957d5..4d83349 100644 --- a/scm_breeze.sh +++ b/scm_breeze.sh @@ -22,6 +22,7 @@ if [[ -s "$HOME/.git.scmbrc" ]]; then source "$scmbDir/lib/git/keybindings.sh" source "$scmbDir/lib/git/status_shortcuts.sh" source "$scmbDir/lib/git/branch_shortcuts.sh" + source "$scmbDir/lib/git/grep_shortcuts.sh" source "$scmbDir/lib/git/shell_shortcuts.sh" source "$scmbDir/lib/git/repo_index.sh" source "$scmbDir/lib/git/tools.sh" From f59a255c9c255d6e1ccb2451eb189d2e286886f8 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Fri, 31 Aug 2018 23:45:40 +0700 Subject: [PATCH 27/79] Remove video and GIF from README --- README.markdown | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.markdown b/README.markdown index 07abd94..338cc22 100644 --- a/README.markdown +++ b/README.markdown @@ -11,9 +11,6 @@ your interaction with git. It integrates with your shell to give you numbered file shortcuts, a repository index with tab completion, and many other useful features. -![SCM Breeze Example Gif](http://i.imgur.com/3fD8cpo.gif) - - - [Installation](#installation) - [Usage](#usage) - [File Shortcuts](#file-shortcuts) @@ -42,12 +39,6 @@ to your `.bashrc` or `.zshrc`: **Note:** SCM Breeze performs much faster if you have ruby installed. - -## Usage - -
- ### File Shortcuts SCM Breeze makes it really easy to work with changed files, and groups of From 7fcd70adc270272be4892b03c1463cce281c04df Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Fri, 31 Aug 2018 23:45:57 +0700 Subject: [PATCH 28/79] Rename README.markdown => README.md --- README.markdown => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.markdown => README.md (100%) diff --git a/README.markdown b/README.md similarity index 100% rename from README.markdown rename to README.md From e6b1785910eb6ec15b83d7cdfa6e8d9feed4acf7 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Fri, 31 Aug 2018 23:53:42 +0700 Subject: [PATCH 29/79] Revert "Use ^M instead of" - Stick with "\n" for now so that Ctrl + x + space works on bash This reverts commit 1f3b8a6b11ab1d60cdbdea56525b4aefee8326a5. --- lib/git/keybindings.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/git/keybindings.sh b/lib/git/keybindings.sh index 11bb698..8ec4066 100644 --- a/lib/git/keybindings.sh +++ b/lib/git/keybindings.sh @@ -27,13 +27,13 @@ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then # Uses emacs style keybindings, so vi mode is not supported for now if ! set -o | grep -q '^vi .*on$'; then if [[ $shell == "zsh" ]]; then - _bind "$git_commit_all_keys" " git_commit_all""^M" - _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""^M" - _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""^M" + _bind "$git_commit_all_keys" " git_commit_all""\n" + _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""\n" + _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""\n" else - _bind "$git_commit_all_keys" "\" git_commit_all^M\"" - _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit ^M\"" - _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all ^M\"" + _bind "$git_commit_all_keys" "\" git_commit_all\n\"" + _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit \n\"" + _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all \n\"" fi fi From 0acc0223a5be26065c61c52bd15b674bf9f49ea7 Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 13:22:16 -0400 Subject: [PATCH 30/79] Add legacy status script --- lib/git/status_shortcuts.rb.legacy | 218 +++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 lib/git/status_shortcuts.rb.legacy diff --git a/lib/git/status_shortcuts.rb.legacy b/lib/git/status_shortcuts.rb.legacy new file mode 100644 index 0000000..e121b8b --- /dev/null +++ b/lib/git/status_shortcuts.rb.legacy @@ -0,0 +1,218 @@ +#!/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 --show-toplevel 2> /dev/null`.strip + +@git_status = `\git status --porcelain -b 2> /dev/null` + +git_status_lines = @git_status.split("\n") +git_branch = git_status_lines[0] +@branch = git_branch[/^## (?:Initial commit on )?([^ \.]+)/, 1] +@ahead = git_branch[/\[ahead ?(\d+).*\]/, 1] +@behind = git_branch[/\[.*behind ?(\d+)\]/, 1] + +@changes = git_status_lines[1..-1] +# Exit if too many changes +exit if @changes.size > ENV["gs_max_changes"].to_i + +# 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 + 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 x == "R" && y == "M" + # Extract the second file name from the format x -> y + quoted, unquoted = /^(?:"(?:[^"\\]|\\.)*"|[^"].*) -> (?:"((?:[^"\\]|\\.)*)"|(.*[^"]))$/.match(file)[1..2] + renamed_file = quoted || unquoted + @stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => renamed_file} + elsif x != "R" && 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 = "\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') + 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="\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) + 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("|") From e8a5f775d1300714d026affffe28bd0c508ed416 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Sat, 1 Sep 2018 00:22:33 +0700 Subject: [PATCH 31/79] Use "^M" on Zsh and "\n" on Bash --- lib/git/keybindings.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/git/keybindings.sh b/lib/git/keybindings.sh index 8ec4066..b286075 100644 --- a/lib/git/keybindings.sh +++ b/lib/git/keybindings.sh @@ -24,16 +24,22 @@ _bind(){ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then case "$-" in *i*) + if [ -n "$ZSH_VERSION" ]; then + RETURN_CHAR="^M" + else + RETURN_CHAR="\n" + fi + # Uses emacs style keybindings, so vi mode is not supported for now if ! set -o | grep -q '^vi .*on$'; then if [[ $shell == "zsh" ]]; then - _bind "$git_commit_all_keys" " git_commit_all""\n" - _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""\n" - _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""\n" + _bind "$git_commit_all_keys" " git_commit_all""$RETURN_CHAR" + _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""$RETURN_CHAR" + _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""$RETURN_CHAR" else - _bind "$git_commit_all_keys" "\" git_commit_all\n\"" - _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit \n\"" - _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all \n\"" + _bind "$git_commit_all_keys" "\" git_commit_all$RETURN_CHAR\"" + _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit $RETURN_CHAR\"" + _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all $RETURN_CHAR\"" fi fi From f0ef9b06f63e274e6727ef3f4f57cf213eb4e353 Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 13:51:34 -0400 Subject: [PATCH 32/79] Add back too status max files check --- lib/git/status_shortcuts.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/git/status_shortcuts.rb b/lib/git/status_shortcuts.rb index f76f1a5..4c6efe9 100755 --- a/lib/git/status_shortcuts.rb +++ b/lib/git/status_shortcuts.rb @@ -37,6 +37,7 @@ class GitStatus end def report + exit if all_changes.length > ENV["gs_max_changes"].to_i print_header print_groups puts filelist if @grouped_changes.any? From fc679a06ac4e1409e1d5853910acdc87970e969c Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 13:53:51 -0400 Subject: [PATCH 33/79] Update strip color perl command --- test/support/test_helper.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index ba9e472..a021b03 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -15,7 +15,8 @@ fi # Strip color codes from a string strip_colors() { - perl -pe 's/\e\[[\d;]*m//g' + # Updated with info from: https://superuser.com/a/380778 + perl -pe 's/\x1b\[[0-9;]*[mG]//g' } # Print space separated tab completion options From 750697e9257b732954907aafd843c2327f2b6d95 Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Fri, 31 Aug 2018 16:02:39 -0400 Subject: [PATCH 34/79] Add quotes around files with spaces --- lib/git/status_shortcuts.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/git/status_shortcuts.rb b/lib/git/status_shortcuts.rb index 4c6efe9..619d145 100755 --- a/lib/git/status_shortcuts.rb +++ b/lib/git/status_shortcuts.rb @@ -280,6 +280,7 @@ class GitChange < GitStatus def initialize(file_and_message, status) @message = file_and_message.slice!(/\(.*\)/) @file = file_and_message.strip + @file = (@file.include? " ") ? "\"#{@file}\"" : @file @status = status.strip end From 5f286eaaaa587ad96de2d61aa03a17ddcf4c0395 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 24 Aug 2018 08:32:23 +0700 Subject: [PATCH 35/79] Quote arrays to avoid splitting by $IFS --- lib/git/aliases.sh | 2 +- lib/git/branch_shortcuts.sh | 2 +- lib/git/repo_index.sh | 14 +++++++------- lib/git/shell_shortcuts.sh | 4 ++-- lib/git/status_shortcuts.sh | 8 ++++---- lib/git/tools.sh | 4 ++-- test/lib/git/shell_shortcuts_test.sh | 2 +- test/support/shunit2 | 6 +++--- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index 28b0e31..e9c8f59 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -71,7 +71,7 @@ __git_alias () { alias_str="$1"; cmd_prefix="$2"; cmd="$3"; if [ $# -gt 2 ]; then shift 3 2>/dev/null - cmd_args=$@ + cmd_args=("$@") fi alias $alias_str="$cmd_prefix $cmd${cmd_args:+ }${cmd_args[*]}" diff --git a/lib/git/branch_shortcuts.sh b/lib/git/branch_shortcuts.sh index 61012e2..7157339 100644 --- a/lib/git/branch_shortcuts.sh +++ b/lib/git/branch_shortcuts.sh @@ -22,7 +22,7 @@ function _scmb_git_branch_shortcuts { # Use ruby to inject numbers into ls output ruby -e "$( cat < 9 && i < 9 ? " " : " ") diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index 3d2e9c7..9fc2b44 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -217,10 +217,10 @@ _git_index_update_all_branches() { # Ignore branch if remote and merge is not configured if [[ -n "$remote" ]] && [[ -n "$merge" ]]; then - branches=(${branches[@]} "$branch") - remotes=(${remotes[@]} "$remote") + branches=("${branches[@]}" "$branch") + remotes=("${remotes[@]}" "$remote") # Get branch from merge ref (refs/heads/master => master) - merges=(${merges[@]} "$(basename $merge)") + merges=("${merges[@]}" "$(basename "$merge")") else echo "=== Skipping $branch: remote and merge refs are not configured." fi @@ -232,12 +232,12 @@ _git_index_update_all_branches() { local index=0 # Iterate over branches, and update those that can be fast-forwarded - for branch in ${branches[@]}; do + for branch in "${branches[@]}"; do branch_rev="$(git rev-parse $branch)" # Local branch can be fast-forwarded if revision is ancestor of remote revision, and not the same. # (see http://stackoverflow.com/a/2934062/304706) - if [[ "$branch_rev" != "$(git rev-parse ${remotes[$index]}/${merges[$index]})" ]] && \ - [[ "$(git merge-base $branch_rev ${remotes[$index]}/${merges[$index]})" = "$branch_rev" ]]; then + if [[ "$branch_rev" != "$(git rev-parse "${remotes[$index]}/${merges[$index]}")" ]] && \ + [[ "$(git merge-base "$branch_rev" "${remotes[$index]}/${merges[$index]}")" = "$branch_rev" ]]; then echo "=== Updating $branch branch in $base_path from ${remotes[$index]}/${merges[$index]}..." # Checkout branch if we aren't already on it. if [[ "$branch" != "$(parse_git_branch)" ]]; then git checkout $branch; fi @@ -272,7 +272,7 @@ function _git_index_batch_cmd() { local base_path for base_path in $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | \grep . | sort); do builtin cd "$base_path" - $@ + "$@" done else echo "Please give a command to run for all repos. (It may be useful to write your command as a function or script.)" diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 4c427d8..2cadc24 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -126,7 +126,7 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then # Parse path from args IFS=$'\n' - for arg in $@; do + for arg in "$@"; do if [ -d "$arg" ]; then local rel_path="${arg%/}"; fi done unset IFS @@ -142,7 +142,7 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then puts o.lines.map{|l|l.sub(re){|m|\"%s%-#{u}s %-#{g}s%#{s}s \"%[\$1,*\$3.split]}}" } - ll_output=$(echo "$ll_output" | \sed -$SED_REGEX_ARG "s/ $USER/ $(/bin/cat $HOME/.user_sym)/g" | rejustify_ls_columns) + ll_output=$(echo "$ll_output" | \sed -$SED_REGEX_ARG "s/ $USER/ $(/bin/cat "$HOME/.user_sym")/g" | rejustify_ls_columns) fi if [ "$(echo "$ll_output" | wc -l)" -gt "50" ]; then diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index d63bb0c..ee0f804 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -21,7 +21,7 @@ git_status_shortcuts() { zsh_compat # Ensure shwordsplit is on for zsh git_clear_vars # Run ruby script, store output - local cmd_output="$(/usr/bin/env ruby "$scmbDir/lib/git/status_shortcuts.rb" $@)" + local cmd_output="$(/usr/bin/env ruby "$scmbDir/lib/git/status_shortcuts.rb" "$@")" # Print debug information if $scmbDebug = "true" if [ "${scmbDebug:-}" = "true" ]; then printf "status_shortcuts.rb output => \n$cmd_output\n------------------------\n" @@ -102,8 +102,8 @@ git_show_affected_files(){ fail_if_not_git_repo || return 1 f=0 # File count # Show colored revision and commit message - echo -n "# "; git show --oneline --name-only $@ | head -n1; echo "# " - for file in $(git show --pretty="format:" --name-only $@ | \grep -v '^$'); do + echo -n "# "; git show --oneline --name-only "$@" | head -n1; echo "# " + for file in $(git show --pretty="format:" --name-only "$@" | \grep -v '^$'); do let f++ export $git_env_char$f=$file # Export numbered variable. echo -e "# \033[2;37m[\033[0m$f\033[2;37m]\033[0m $file" @@ -210,7 +210,7 @@ git_commit_prompt() { fi if [ -n "$commit_msg" ]; then - eval $@ # run any prequisite commands + eval "$@" # run any prequisite commands # Add $APPEND to commit message, if given. (Used to append things like [ci skip] for Travis CI) if [ -n "$APPEND" ]; then commit_msg="$commit_msg $APPEND"; fi echo $commit_msg | git commit -F - | tail -n +2 diff --git a/lib/git/tools.sh b/lib/git/tools.sh index 0c62e72..e5e2c5d 100644 --- a/lib/git/tools.sh +++ b/lib/git/tools.sh @@ -23,8 +23,8 @@ git_remove_history() { return fi # Remove all paths passed as arguments from the history of the repo - files=$@ - $_git_cmd filter-branch --index-filter "$_git_cmd rm -rf --cached --ignore-unmatch $files" HEAD + files=("$@") + $_git_cmd filter-branch --index-filter "$_git_cmd rm -rf --cached --ignore-unmatch ${files[*]}" HEAD # Remove the temporary history git-filter-branch otherwise leaves behind for a long time rm -rf .git/refs/original/ && $_git_cmd reflog expire --all && $_git_cmd gc --aggressive --prune } diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index f699085..3842590 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -38,7 +38,7 @@ oneTimeSetUp() { alias rvm="test" # Ensure tests run if RVM isn't loaded but $HOME/.rvm is present # Test functions - function ln() { ln $@; } + function ln() { ln "$@"; } # Before aliasing, get original locations so we can compare them in the test unalias mv rm sed cat 2>/dev/null diff --git a/test/support/shunit2 b/test/support/shunit2 index 8ca4b4b..df37583 100755 --- a/test/support/shunit2 +++ b/test/support/shunit2 @@ -25,9 +25,9 @@ SHUNIT_ERROR=2 # enable strict mode by default SHUNIT_STRICT=${SHUNIT_STRICT:-${SHUNIT_TRUE}} -_shunit_warn() { echo "shunit2:WARN $@" >&2; } -_shunit_error() { echo "shunit2:ERROR $@" >&2; } -_shunit_fatal() { echo "shunit2:FATAL $@" >&2; exit ${SHUNIT_ERROR}; } +_shunit_warn() { echo "shunit2:WARN $*" >&2; } +_shunit_error() { echo "shunit2:ERROR $*" >&2; } +_shunit_fatal() { echo "shunit2:FATAL $*" >&2; exit ${SHUNIT_ERROR}; } # specific shell checks if [ -n "${ZSH_VERSION:-}" ]; then From 3a5b7a685afc184b64ff95b2883edf2000c6bb48 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 24 Aug 2018 08:34:47 +0700 Subject: [PATCH 36/79] _print_path: remove unnecessary pipeline --- lib/git/status_shortcuts.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index ee0f804..eb81eaf 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -147,12 +147,13 @@ scmb_expand_args() { IFS="$OLDIFS" } +# Expand a variable into a (possibly relative) pathname _print_path() { - if [ "$1" = 1 ]; then - eval printf '%s' "\"\$$2\"" | sed -e "s%$(pwd)/%%" | awk '{printf("%s", $0)}' - else - eval printf '%s' "\"\$$2\"" + local pathname=$(eval printf '%s' "\"\${$2}\"") + if [ "$1" = 1 ]; then # print relative + pathname=${pathname#$PWD/} # Remove $PWD from beginning of the path fi + printf '%s' "$pathname" } # Execute a command with expanded args, e.g. Delete files 6 to 12: $ ge rm 6-12 From d5c60b2cc542b337ccae1435b5c1e99291949edf Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 24 Aug 2018 18:12:07 +0700 Subject: [PATCH 37/79] git_status_shortcuts: don't clobber $IFS --- lib/design.sh | 3 +-- lib/git/branch_shortcuts.sh | 4 +--- lib/git/fallback/status_shortcuts_shell.sh | 5 ++--- lib/git/repo_index.sh | 22 ++++++++-------------- lib/git/shell_shortcuts.sh | 6 ++---- lib/git/status_shortcuts.sh | 3 +-- test/lib/git/repo_index_test.sh | 6 ++---- 7 files changed, 17 insertions(+), 32 deletions(-) diff --git a/lib/design.sh b/lib/design.sh index 125175e..cd48cdb 100644 --- a/lib/design.sh +++ b/lib/design.sh @@ -34,7 +34,7 @@ design() { local project=`basename $(pwd)` local all_project_dirs="$design_base_dirs $design_av_dirs" # Ensure design dir contains all subdirectories - IFS=$' \t\n' + local IFS=$' \t\n' # Create root design dirs for dir in $design_ext_dirs; do mkdir -p "$root_design_dir/$dir"; done # Create project design dirs @@ -102,6 +102,5 @@ design() { printf "Invalid command.\n\n" design fi - unset IFS } diff --git a/lib/git/branch_shortcuts.sh b/lib/git/branch_shortcuts.sh index 7157339..27b0ce8 100644 --- a/lib/git/branch_shortcuts.sh +++ b/lib/git/branch_shortcuts.sh @@ -32,14 +32,12 @@ EOF )" # Set numbered file shortcut in variable - local e=1 - IFS=$'\n' + local e=1 IFS=$'\n' for branch in $($_git_cmd branch "$@" | sed "s/^[* ]\{2\}//"); do export $git_env_char$e="$branch" if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done - unset IFS } __git_alias "$git_branch_alias" "_scmb_git_branch_shortcuts" "" diff --git a/lib/git/fallback/status_shortcuts_shell.sh b/lib/git/fallback/status_shortcuts_shell.sh index 9368588..406da56 100755 --- a/lib/git/fallback/status_shortcuts_shell.sh +++ b/lib/git/fallback/status_shortcuts_shell.sh @@ -16,7 +16,7 @@ # -------------------------------------------------------------------- git_status_shortcuts() { zsh_compat # Ensure shwordsplit is on for zsh - IFS=$'\n' + local IFS=$'\n' local git_status="$(git status --porcelain 2> /dev/null)" local i @@ -95,7 +95,7 @@ git_status_shortcuts() { fi done - IFS=" " + local IFS=" " grp_num=1 for heading in 'Changes to be committed' 'Unmerged paths' 'Changes not staged for commit' 'Untracked files'; do # If no group specified as param, or specified group is current group @@ -114,7 +114,6 @@ git_status_shortcuts() { # so just use plain 'git status' git status fi - unset IFS zsh_reset # Reset zsh environment to default } # Template function for 'git_status_shortcuts'. diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index 9fc2b44..b3f4005 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -51,7 +51,7 @@ function git_index() { - IFS=$'\n' + local IFS=$'\n' if [ -z "$1" ]; then # Just change to $GIT_REPO_DIR if no params given. "cd" $GIT_REPO_DIR @@ -103,7 +103,7 @@ function git_index() { # -------------------- # Go to our base path if [ -n "$base_path" ]; then - IFS=$' \t\n' + local IFS=$' \t\n' # evaluate ~ if necessary if [[ "$base_path" == "~"* ]]; then base_path=$(eval echo ${base_path%%/*})/${base_path#*/} @@ -116,7 +116,6 @@ function git_index() { fi fi fi - unset IFS } _git_index_dirs_without_home() { @@ -126,12 +125,11 @@ _git_index_dirs_without_home() { # Recursively searches for git repos in $GIT_REPO_DIR function _find_git_repos() { # Find all unarchived projects - IFS=$'\n' + local IFS=$'\n' for repo in $(find -L "$GIT_REPO_DIR" -maxdepth 5 -name ".git" -type d \! -wholename '*/archive/*'); do echo ${repo%/.git} # Return project folder, with trailing ':' _find_git_submodules $repo # Detect any submodules done - unset IFS } # List all submodules for a git repo, if any. @@ -146,11 +144,10 @@ function _find_git_submodules() { function _rebuild_git_index() { if [ "$1" != "--silent" ]; then echo -e "== Scanning $GIT_REPO_DIR for git repos & submodules..."; fi # Get repos from src dir and custom dirs, then sort by basename - IFS=$'\n' + local IFS=$'\n' for repo in $(echo -e "$(_find_git_repos)\n$(echo $GIT_REPOS | sed "s/:/\\\\n/g")"); do echo $(basename $repo | sed "s/ /_/g"):$repo done | sort -t ":" -k1,1 | cut -d ":" -f2- >| "$GIT_REPO_DIR/.git_index" - unset IFS if [ "$1" != "--silent" ]; then echo -e "===== Indexed $_bld_col$(_git_index_count)$_txt_col repos in $GIT_REPO_DIR/.git_index" @@ -207,7 +204,7 @@ _git_index_update_all_branches() { local remotes merges branches # Get branch configuration from .git/config - IFS=$'\n' + local IFS=$'\n' for branch in $($GIT_BINARY branch 2> /dev/null | sed -e 's/.\{2\}\(.*\)/\1/'); do # Skip '(no branch)' if [[ "$branch" = "(no branch)" ]]; then continue; fi @@ -225,7 +222,6 @@ _git_index_update_all_branches() { echo "=== Skipping $branch: remote and merge refs are not configured." fi done - unset IFS # Update all remotes if there are any branches to update if [ -n "${branches[*]}" ]; then git fetch --all 2> /dev/null; fi @@ -268,7 +264,7 @@ function _git_index_batch_cmd() { cwd="$PWD" if [ -n "$1" ]; then echo -e "== Running command for $_bld_col$(_git_index_count)$_txt_col repos...\n" - unset IFS + local IFS=$'\n' local base_path for base_path in $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | \grep . | sort); do builtin cd "$base_path" @@ -285,8 +281,7 @@ if [ $shell = 'bash' ]; then # Bash tab completion function for git_index() function _git_index_tab_completion() { _check_git_index - local curw - IFS=$'\n' + local curw IFS=$'\n' COMPREPLY=() curw=${COMP_WORDS[COMP_CWORD]} @@ -313,10 +308,9 @@ if [ $shell = 'bash' ]; then else COMPREPLY=($(compgen -W '$(sed -e "s:.*/::" -e "s:$:/:" "$GIT_REPO_DIR/.git_index" | sort)' -- $curw)) fi - unset IFS return 0 } -else +else # Zsh tab completion function for git_index() function _git_index_tab_completion() { typeset -A opt_args local state state_descr context line diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 2cadc24..6c4a972 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -125,11 +125,10 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then fi # Parse path from args - IFS=$'\n' + local IFS=$'\n' for arg in "$@"; do if [ -d "$arg" ]; then local rel_path="${arg%/}"; fi done - unset IFS # Replace user/group with user symbol, if defined at ~/.user_sym # Before : -rw-rw-r-- 1 ndbroadbent ndbroadbent 1.1K Sep 19 21:39 scm_breeze.sh @@ -175,14 +174,13 @@ EOF ll_files="$(\ls "$@")" fi - IFS=$'\n' + local IFS=$'\n' for file in $ll_files; do if [ -n "$rel_path" ]; then file="$rel_path/$file"; fi export $git_env_char$e="$(eval $_abs_path_command \"${file//\"/\\\"}\")" if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done - unset IFS # Turn off shwordsplit unless it was on previously if [[ $shell == "zsh" ]] && [ -z "$SHWORDSPLIT_ON" ]; then unsetopt shwordsplit; fi diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index eb81eaf..69410a2 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -36,14 +36,13 @@ git_status_shortcuts() { files="$(echo "$cmd_output" | \grep '@@filelist@@::' | sed 's%@@filelist@@::%%g')" if [ "${scmbDebug:-}" = "true" ]; then echo "filelist => $files"; fi # Export numbered env variables for each file - IFS="|" + local IFS="|" local e=1 for file in $files; do export $git_env_char$e="$file" if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done - unset IFS if [ "${scmbDebug:-}" = "true" ]; then echo "------------------------"; fi # Print status diff --git a/test/lib/git/repo_index_test.sh b/test/lib/git/repo_index_test.sh index c866ec4..eb98334 100755 --- a/test/lib/git/repo_index_test.sh +++ b/test/lib/git/repo_index_test.sh @@ -68,13 +68,12 @@ EOF done # Setup some custom repos outside the main repo dir - IFS=":" + local IFS=":" for dir in $GIT_REPOS; do mkdir -p $dir cd $dir git init done - unset IFS verboseGitCommands @@ -83,9 +82,8 @@ EOF oneTimeTearDown() { rm -rf "${GIT_REPO_DIR}" - IFS=":" + local IFS=":" for dir in $GIT_REPOS; do rm -rf $dir; done - unset IFS } ensureIndex() { From fb7d227c30d705ddafc982fac390bcded0867bee Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 24 Aug 2018 19:30:16 +0700 Subject: [PATCH 38/79] Add _safe_eval: quote $@ elements before eval --- lib/scm_breeze.sh | 14 ++++++++++++++ test/lib/scm_breeze_test.sh | 26 ++++++++++++++++++++++++++ test/support/test_helper.sh | 16 ++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100755 test/lib/scm_breeze_test.sh diff --git a/lib/scm_breeze.sh b/lib/scm_breeze.sh index 2c692bb..b177db9 100644 --- a/lib/scm_breeze.sh +++ b/lib/scm_breeze.sh @@ -18,6 +18,20 @@ _alias() { fi } +# Quote "$@" before `eval` to prevent arbitrary code execution. +# Eg, the following will run `date`: +# evil() { eval "$@"; }; evil "echo" "foo;date" +function _safe_eval() { + if [[ $shell = bash ]]; then + # ${parameter@operator} where parameter is ${@} and operator is 'Q' + # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + eval "${@@Q}" + else # zsh + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + eval "${(q-)@}" + fi +} + find_binary(){ if [ $shell = "zsh" ]; then builtin type -p "$1" | sed "s/$1 is //" | head -1 diff --git a/test/lib/scm_breeze_test.sh b/test/lib/scm_breeze_test.sh new file mode 100755 index 0000000..f74beef --- /dev/null +++ b/test/lib/scm_breeze_test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +export scmbDir="$( cd -P "$( dirname "$0" )" && pwd )/../.." + +# Zsh compatibility +if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; SHUNIT_PARENT=$0; setopt shwordsplit; fi + +# Load test helpers +source "$scmbDir/test/support/test_helper.sh" + +# Load functions to test +source "$scmbDir/lib/scm_breeze.sh" + +#----------------------------------------------------------------------------- +# Unit tests +#----------------------------------------------------------------------------- + +test__safe_eval() { + assertEquals "runs eval with simple words" "'one' 'two' 'three'" "$(_safe_eval token_quote one two three)" + assertEquals "quotes spaces" "'a' 'b c' 'd'" "$(_safe_eval token_quote a b\ c d)" + assertEquals "quotes special chars" "'a b' '\$dollar' '\\slash' 'c d'" "$(_safe_eval token_quote a\ b '$dollar' '\slash' c\ d)" +} + + +# load and run shUnit2 +source "$scmbDir/test/support/shunit2" diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index a021b03..4649373 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -26,11 +26,27 @@ tab_completions(){ echo "${COMPREPLY[@]}"; } silentGitCommands() { git() { /usr/bin/env git "$@" > /dev/null 2>&1; } } + # Cancel silent git commands verboseGitCommands() { unset -f git } +# Quote the contents of "$@" in single quotes +# Avoid printf '%q' as 'a b' becomes a\ b in both {ba,z}sh +# See also quote_args and double_quote +function token_quote { + if [[ $shell = bash ]]; then + # Single quotes are always added + echo "${@@Q}" + else # zsh + # Single quotes only added when needed + #shellcheck disable=2154 # zsh + echo "${(qq)@}" + fi +} + + # Asserts #----------------------------------------------------------------------------- From 73ed8cb53e52e4f30f36558202c2652fa01e4e54 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 25 Aug 2018 20:30:42 +0700 Subject: [PATCH 39/79] scmb_expand_args: return an array to fix quoting issues --- lib/git/status_shortcuts.sh | 34 ++++++++++++--------------- test/lib/git/status_shortcuts_test.sh | 20 ++++++++++------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index 69410a2..f7c57cf 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -79,8 +79,8 @@ git_add_shortcuts() { git_silent_add_shortcuts() { if [ -n "$1" ]; then # Expand args and process resulting set of files. - IFS=$'\t' - for file in $(scmb_expand_args "$@"); do + eval "args=$(scmb_expand_args "$@")" # create $args array + for file in "${args[@]}"; do # Use 'git rm' if file doesn't exist and 'ga_auto_remove' is enabled. if [[ $ga_auto_remove == "yes" ]] && ! [ -e "$file" ]; then echo -n "# " @@ -90,7 +90,6 @@ git_silent_add_shortcuts() { echo -e "# Added '$file'" fi done - unset IFS echo "#" fi } @@ -121,32 +120,29 @@ scmb_expand_args() { shift fi - first=1 - OLDIFS="$IFS"; IFS=" " # We need to split on spaces to loop over expanded range + local -a args=() # initially empty array for arg in "$@"; do if [[ "$arg" =~ ^[0-9]{0,4}$ ]] ; then # Substitute $e{*} variables for any integers - if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi if [ -e "$arg" ]; then # Don't expand files or directories with numeric names - printf '%s' "$arg" + args+=("$arg") else - _print_path "$relative" "$git_env_char$arg" + args+=("$(_print_path "$relative" "$git_env_char$arg")") fi elif [[ "$arg" =~ ^[0-9]+-[0-9]+$ ]]; then # Expand ranges into $e{*} variables - for i in $(eval echo {${arg/-/..}}); do - if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi - _print_path "$relative" "$git_env_char$i" + args+=("$(_print_path "$relative" "$git_env_char$i")") done else # Otherwise, treat $arg as a normal string. - if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi - printf '%s' "$arg" + args+=("$arg") fi done - IFS="$OLDIFS" + args=$(declare -p args) # Get $args array as a string which can be `eval`-ed to recreate itself + args=${args#*=} # Remove `typeset -a args=` from beginning of string to allow caller to name variable + echo "$args" } -# Expand a variable into a (possibly relative) pathname +# Expand a variable (named by $2) into a (possibly relative) pathname _print_path() { local pathname=$(eval printf '%s' "\"\${$2}\"") if [ "$1" = 1 ]; then # print relative @@ -158,7 +154,8 @@ _print_path() { # Execute a command with expanded args, e.g. Delete files 6 to 12: $ ge rm 6-12 # Fails if command is a number or range (probably not worth fixing) exec_scmb_expand_args() { - eval "$(scmb_expand_args "$@" | sed -e "s/\([][|;()<>^ \"'&]\)/"'\\\1/g')" + eval "args=$(scmb_expand_args "$@")" # create $args array + _safe_eval "${args[@]}" } # Clear numbered env variables @@ -180,13 +177,12 @@ git_clear_vars() { _git_resolve_merge_conflict() { if [ -n "$2" ]; then # Expand args and process resulting set of files. - IFS=$'\t' - for file in $(scmb_expand_args "${@:2}"); do + eval "args=$(scmb_expand_args "$@")" # create $args array + for file in "${args[@]:2}"; do git checkout "--$1""s" "$file" # "--$1""s" is expanded to --ours or --theirs git add "$file" echo -e "# Added $1 version of '$file'" done - unset IFS echo -e "# -- If you have finished resolving conflicts, commit the resolutions with 'git commit'" fi } diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index d83d9ca..57b861b 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -49,16 +49,22 @@ setupTestRepo() { #----------------------------------------------------------------------------- test_scmb_expand_args() { - local e1="one"; local e2="two"; local e3="three"; local e4="four"; local e5="five"; local e6="six"; local e7="seven" + local e1="one" e2="two" e3="three" e4="four" e5="five" e6="six" e7='$dollar' e8='two words' local error="Args not expanded correctly" - assertEquals "$error" "$(printf 'one\tthree\tseven')" "$(scmb_expand_args 1 3 7)" - assertEquals "$error" "$(printf 'one\ttwo\tthree\tsix')" "$(scmb_expand_args 1-3 6)" - assertEquals "$error" "$(printf 'seven\ttwo\tthree\tfour\tfive\tone')" "$(scmb_expand_args seven 2-5 1)" + assertEquals "$error" "'one' 'three' 'six'" \ + "$(eval a=$(scmb_expand_args 1 3 6); token_quote "${a[@]}")" + assertEquals "$error" "'one' 'two' 'three' 'five'" \ + "$(eval a=$(scmb_expand_args 1-3 5); token_quote "${a[@]}")" + assertEquals "$error" "'\$dollar' 'two' 'three' 'four' 'one'" \ + "$(eval a=$(scmb_expand_args 7 2-4 1); token_quote "${a[@]}")" # Test that any args with spaces remain quoted - assertEquals "$error" "$(printf -- '-m\tTest Commit Message\tone')" "$(scmb_expand_args -m "Test Commit Message" 1)" - assertEquals "$error" "$(printf -- '-ma\tTest Commit Message\tUnquoted')"\ - "$(scmb_expand_args -ma "Test Commit Message" "Unquoted")" + assertEquals "$error" "'-m' 'Test Commit Message' 'one'" \ + "$(eval a=$(scmb_expand_args -m "Test Commit Message" 1); token_quote "${a[@]}")" + assertEquals "$error" "'-ma' 'Test Commit Message' 'Unquoted'"\ + "$(eval a=$(scmb_expand_args -ma "Test Commit Message" "Unquoted"); token_quote "${a[@]}")" + assertEquals "$error" "'\$dollar' 'one' 'two words'" \ + "$(eval a=$(scmb_expand_args 7 1-1 8); token_quote "${a[@]}")" } test_command_wrapping_escapes_special_characters() { From b193438ca1fbadcd0495cb43c40487b17f3602e4 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Mon, 27 Aug 2018 18:00:36 +0700 Subject: [PATCH 40/79] shell_shortcuts_test: Unset $QUOTING_STYLE for predictable ls output --- test/lib/git/shell_shortcuts_test.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 3842590..77dd21c 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -69,6 +69,15 @@ assertAliasEquals(){ } +#----------------------------------------------------------------------------- +# Setup and tear down +#----------------------------------------------------------------------------- + +setUp() { + unset QUOTING_STYLE # Use default quoting style for ls +} + + #----------------------------------------------------------------------------- # Unit tests #----------------------------------------------------------------------------- @@ -93,7 +102,7 @@ test_ls_with_file_shortcuts() { # full physical path to be absolutely certain when doing comparisons later, # because thats how the Ruby status_shortcuts.rb script is going to obtain # them. - cd $TEST_DIR + cd "$TEST_DIR" TEST_DIR=$(pwd -P) touch 'test file' 'test_file' @@ -103,8 +112,8 @@ test_ls_with_file_shortcuts() { # Run command in shell, load output from temp file into variable # (This is needed so that env variables are exported in the current shell) temp_file=$(mktemp -t scm_breeze.XXXXXXXXXX) - ls_with_file_shortcuts > $temp_file - ls_output=$(<$temp_file strip_colors) + ls_with_file_shortcuts > "$temp_file" + ls_output=$(<"$temp_file" strip_colors) # Compare as fixed strings (F), instead of normal grep behavior assertIncludes "$ls_output" '[1] a "b"' F From ed63b41182a9abe2c4339adde2fe9ae9b7430eaf Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Mon, 27 Aug 2018 18:04:20 +0700 Subject: [PATCH 41/79] test_helper: Don't clobber global name/email config Config included by gitconfig [include] is not considered global. Test git config values without --global to include local config as well. Change only local config (not global) if needed. --- test/support/test_helper.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index 4649373..d5a9854 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -4,9 +4,9 @@ orig_cwd="$PWD" source "$scmbDir/lib/git/helpers.sh" # Set up demo git user if not configured -if [ -z "$(git config --global user.email)" ]; then - git config --global user.email "testuser@example.com" - git config --global user.name "Test User" +if [ -z "$(git config user.email)" ]; then + git config user.email "testuser@example.com" + git config user.name "Test User" fi # From 5a8c2fec1264366602d8c60bc3e495251a96e8be Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Mon, 27 Aug 2018 18:21:03 +0700 Subject: [PATCH 42/79] assert(Not)?Includes: check for failure of _includes Call _includes and then check its return value. shunit2 uses the exit code to test, not a string of 0 or 1 as was done previously. --- test/support/test_helper.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index d5a9854..de8cd1b 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -50,16 +50,22 @@ function token_quote { # Asserts #----------------------------------------------------------------------------- +# Return 0 (shell's true) if "$1" contains string "$2" _includes() { if [ -n "$3" ]; then regex="$3"; else regex=''; fi - if echo "$1" | grep -q$regex "$2"; then echo 0; else echo 1; fi + echo "$1" | grep -q"$regex" "$2" # exit status of quiet grep is returned } # assert $1 contains $2 assertIncludes() { - assertTrue "'$1' should have contained '$2'" $(_includes "$@") + _includes "$@" + local grep_exit=$? + assertTrue "'$1' should have contained '$2'" '[[ $grep_exit == 0 ]]' } + # assert $1 does not contain $2 assertNotIncludes() { - assertFalse "'$1' should not have contained '$2'" $(_includes "$@") + _includes "$@" + local grep_return=$? + assertTrue "'$1' should not have contained '$2'" '[[ ! $grep_exit = 0 ]]' } From 0b5a29bbd60bc65fd5c9377fece9ad3a14e2bb56 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Thu, 30 Aug 2018 18:42:41 +0700 Subject: [PATCH 43/79] Invoke readlink -f in a way which can succeed --- lib/git/shell_shortcuts.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 6c4a972..d7fb8c4 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -100,10 +100,10 @@ if ! ls --color=auto > /dev/null 2>&1; then fi # Test if readlink supports -f option, otherwise use perl (a bit slower) -if ! readlink -f > /dev/null 2>&1; then - _abs_path_command='perl -e "use Cwd "abs_path"; print abs_path(shift)"' +if ! readlink -f / > /dev/null 2>&1; then + _abs_path_command=(perl -e 'use Cwd abs_path; print abs_path(shift)') else - _abs_path_command="readlink -f" + _abs_path_command=(readlink -f) fi # Function wrapper around 'll' From f55ce1e6a3cb04b6211430e48da9fd1a08dd928f Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 31 Aug 2018 17:03:30 +0700 Subject: [PATCH 44/79] test_ls_with_file_shortcuts: clean up temp files --- test/lib/git/shell_shortcuts_test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 77dd21c..7bab1d3 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -138,6 +138,9 @@ test_ls_with_file_shortcuts() { # Test arg with no quotes ls_output=$(ls_with_file_shortcuts a\ \"b\" | strip_colors) assertIncludes "$ls_output" '[1] c' F + + cd - + rm -r "$TEST_DIR" "$temp_file" } # load and run shUnit2 From a8092bffd9cb45c5845c3e43b62b3b333b01d69f Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 1 Sep 2018 13:27:53 +0700 Subject: [PATCH 45/79] Clean up ls_with_file_shortcuts() shwordsplit tests --- lib/git/shell_shortcuts.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index d7fb8c4..1cf72f7 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -120,7 +120,7 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then if [[ $shell == "zsh" ]]; then # Ensure sh_word_split is on - if setopt | grep -q shwordsplit; then SHWORDSPLIT_ON=true; fi + [[ -o shwordsplit ]] && SHWORDSPLIT_ON=true setopt shwordsplit fi @@ -144,6 +144,7 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then ll_output=$(echo "$ll_output" | \sed -$SED_REGEX_ARG "s/ $USER/ $(/bin/cat "$HOME/.user_sym")/g" | rejustify_ls_columns) fi + # Bail if there are two many lines to process if [ "$(echo "$ll_output" | wc -l)" -gt "50" ]; then echo -e "\033[33mToo many files to create shortcuts. Running plain ll command...\033[0m" echo "$ll_output" @@ -183,7 +184,7 @@ EOF done # Turn off shwordsplit unless it was on previously - if [[ $shell == "zsh" ]] && [ -z "$SHWORDSPLIT_ON" ]; then unsetopt shwordsplit; fi + if [[ $shell == "zsh" && -z $SHWORDSPLIT_ON ]]; then unsetopt shwordsplit; fi } # Setup aliases From 695941b596091a70c11e2ea6f29a8a6f86245055 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 1 Sep 2018 16:25:36 +0700 Subject: [PATCH 46/79] Add tests for exec_scmb_expand_args() --- test/lib/git/status_shortcuts_test.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index 57b861b..3f54c5d 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -67,6 +67,19 @@ test_scmb_expand_args() { "$(eval a=$(scmb_expand_args 7 1-1 8); token_quote "${a[@]}")" } +test_exec_scmb_expand_args() { + local e1="one" e2="a b c" e3='$dollar' e4="single'quote" e5='double"quote' e6='a(){:;};a&' + assertEquals "literals with spaces not preserved" "'foo' 'bar baz'" \ + "$(eval a="$(scmb_expand_args foo 'bar baz')"; token_quote "${a[@]}")" + assertEquals "variables with spaces not preserved" "'one' 'a b c'" \ + "$(eval a="$(scmb_expand_args 1-2)"; token_quote "${a[@]}")" + # Expecting text: '$dollar' "single'quote" 'double"quote' + # Generate quoted expected string with: token_quote "$(cat)" then copy/paste, ^D + assertEquals "special characters are preserved" \ + "'\$dollar' 'single'\\''quote' 'double\"quote' 'a(){:;};a&'" \ + "$(eval a="$(scmb_expand_args 3-6)"; token_quote "${a[@]}")" +} + test_command_wrapping_escapes_special_characters() { assertEquals 'should escape | the pipe' "$(exec_scmb_expand_args echo "should escape | the pipe")" assertEquals 'should escape ; the semicolon' "$(exec_scmb_expand_args echo "should escape ; the semicolon")" From 86d9d4b189ee8bdeae78783817d18ee1c8106409 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 1 Sep 2018 16:26:54 +0700 Subject: [PATCH 47/79] shell_shortcuts.sh: fix quoting --- lib/git/shell_shortcuts.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 1cf72f7..d852f8e 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -29,13 +29,13 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e # Special check for 'cd', to make sure SCM Breeze is loaded after RVM if [ "$cmd" = 'cd' ]; then if [ -e "$HOME/.rvm" ] && ! type rvm > /dev/null 2>&1; then - echo -e "\033[0;31mSCM Breeze must be loaded \033[1;31mafter\033[0;31m RVM, otherwise there will be a conflict when RVM wraps the 'cd' command.\033[0m" - echo -e "\033[0;31mPlease move the line that loads SCM Breeze to the bottom of your ~/.bashrc\033[0m" + echo -e "\\033[0;31mSCM Breeze must be loaded \\033[1;31mafter\\033[0;31m RVM, otherwise there will be a conflict when RVM wraps the 'cd' command.\\033[0m" + echo -e "\\033[0;31mPlease move the line that loads SCM Breeze to the bottom of your ~/.bashrc\\033[0m" continue fi fi - case "$(LC_MESSAGES="C" type $cmd 2>&1)" in + case "$(LC_MESSAGES="C" type "$cmd" 2>&1)" in # Don't do anything if command already aliased, or not found. *'exec_scmb_expand_args'*) @@ -49,10 +49,10 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e # Store original alias local original_alias="$(whence $cmd)" # Remove alias, so that we can find binary - unalias $cmd + unalias "$cmd" # Detect original $cmd type, and escape - case "$(LC_MESSAGES="C" type $cmd 2>&1)" in + case "$(LC_MESSAGES="C" type "$cmd" 2>&1)" in # Escape shell builtins with 'builtin' *'is a shell builtin'*) local escaped_cmd="builtin $cmd";; # Get full path for files with 'find_binary' function @@ -67,9 +67,9 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e *'is a'*'function'*) if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: $cmd is a function"; fi # Copy old function into new name - eval "$(declare -f $cmd | sed -$SED_REGEX_ARG "s/^$cmd \(\)/__original_$cmd ()/")" + eval "$(declare -f "$cmd" | sed -"$SED_REGEX_ARG" "s/^$cmd \\(\\)/__original_$cmd ()/")" # Remove function - unset -f $cmd + unset -f "$cmd" # Create function that wraps old function eval "${cmd}(){ exec_scmb_expand_args __original_${cmd} \"\$@\"; }";; @@ -133,7 +133,7 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then # Replace user/group with user symbol, if defined at ~/.user_sym # Before : -rw-rw-r-- 1 ndbroadbent ndbroadbent 1.1K Sep 19 21:39 scm_breeze.sh # After : -rw-rw-r-- 1 𝐍 𝐍 1.1K Sep 19 21:39 scm_breeze.sh - if [ -e $HOME/.user_sym ]; then + if [ -e "$HOME"/.user_sym ]; then # Little bit of ruby golf to rejustify the user/group/size columns after replacement function rejustify_ls_columns(){ ruby -e "o=STDIN.read;re=/^(([^ ]* +){2})(([^ ]* +){3})/;\ @@ -178,7 +178,7 @@ EOF local IFS=$'\n' for file in $ll_files; do if [ -n "$rel_path" ]; then file="$rel_path/$file"; fi - export $git_env_char$e="$(eval $_abs_path_command \"${file//\"/\\\"}\")" + export $git_env_char$e="$(_safe_eval "${_abs_path_command[@]}" "$file")" if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done From a6eeebfa91e51ec7aae21f65e0df49e461a4ca68 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 1 Sep 2018 16:27:24 +0700 Subject: [PATCH 48/79] shell_shortcuts.sh: remove unnecessary subshell --- lib/git/shell_shortcuts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index d852f8e..583b77a 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -23,7 +23,7 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e # Define 'whence' for bash, to get the value of an alias type whence > /dev/null 2>&1 || function whence() { LC_MESSAGES="C" type "$@" | sed -$SED_REGEX_ARG -e "s/.*is aliased to \`//" -e "s/'$//"; } local cmd='' - for cmd in $(echo $scmb_wrapped_shell_commands); do + for cmd in $scmb_wrapped_shell_commands; do if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: Wrapping $cmd..."; fi # Special check for 'cd', to make sure SCM Breeze is loaded after RVM From a580c5300463f3071ed02176bd4b507ee1cf551b Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sat, 1 Sep 2018 16:28:31 +0700 Subject: [PATCH 49/79] ls_with_file_shortcuts: print error to STDERR --- lib/git/shell_shortcuts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 583b77a..e3ca7f5 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -146,7 +146,7 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then # Bail if there are two many lines to process if [ "$(echo "$ll_output" | wc -l)" -gt "50" ]; then - echo -e "\033[33mToo many files to create shortcuts. Running plain ll command...\033[0m" + echo -e '\033[33mToo many files to create shortcuts. Running plain ll command...\033[0m' >&2 echo "$ll_output" return 1 fi From 1a9411dd4ae30d7b3bc4aba6e74730f6845f6efe Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Wed, 5 Sep 2018 12:24:14 -0400 Subject: [PATCH 50/79] Revert to legacy status shortcuts ruby script --- lib/git/status_shortcuts.rb | 442 ++++++++++----------------- lib/git/status_shortcuts.rb.legacy | 218 ------------- lib/git/status_shortcuts_refactor.rb | 328 ++++++++++++++++++++ 3 files changed, 494 insertions(+), 494 deletions(-) mode change 100755 => 100644 lib/git/status_shortcuts.rb delete mode 100644 lib/git/status_shortcuts.rb.legacy create mode 100644 lib/git/status_shortcuts_refactor.rb diff --git a/lib/git/status_shortcuts.rb b/lib/git/status_shortcuts.rb old mode 100755 new mode 100644 index 619d145..e121b8b --- a/lib/git/status_shortcuts.rb +++ b/lib/git/status_shortcuts.rb @@ -6,323 +6,213 @@ # 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) -# +# (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', and exports numbered +# 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 # -------------------------------------------------------------------- -# -require 'strscan' -class GitStatus - def initialize(request = nil) - @request = request.to_s # capture nils - @status = get_status - @ahead, @behind = parse_remote_stat - @grouped_changes = parse_changes - @index = 0 - end +@project_root = File.exist?(".git") ? Dir.pwd : `\git rev-parse --show-toplevel 2> /dev/null`.strip - def report - exit if all_changes.length > ENV["gs_max_changes"].to_i - print_header - print_groups - puts filelist if @grouped_changes.any? - end +@git_status = `\git status --porcelain -b 2> /dev/null` + +git_status_lines = @git_status.split("\n") +git_branch = git_status_lines[0] +@branch = git_branch[/^## (?:Initial commit on )?([^ \.]+)/, 1] +@ahead = git_branch[/\[ahead ?(\d+).*\]/, 1] +@behind = git_branch[/\[.*behind ?(\d+)\]/, 1] + +@changes = git_status_lines[1..-1] +# Exit if too many changes +exit if @changes.size > ENV["gs_max_changes"].to_i + +# 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" +} - ######### Parsing methods ######### +# Following colors must be prepended with modifiers e.g. '\033[1;', '\033[0;' +@group_c = { + :staged => "33m", + :unmerged => "31m", + :unstaged => "32m", + :untracked => "36m" +} - def get_status - `git status 2>/dev/null` - end +@stat_hash = { + :staged => [], + :unmerged => [], + :unstaged => [], + :untracked => [] +} - # 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/) - remote_line.match(/.*(\d*).*(\d*)/).captures - else - [remote_line[/is ahead of.*by (\d*).*/, 1], remote_line[/is behind.*by (\d*).*/, 1]] - end - end +@output_files = [] - # 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| - 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 +# Counter for env variables +@e = 0 - def scan_until_next_empty_line(scanner) - scanner.scan_until(/\n\n/) - end +# 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]}" : "" - # 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(.*:)?(.*)/) - 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 +# 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 - 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 +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 select_groups - req = parse_request - GROUPS.select { |k, _| k == req } - end +def has_modules? + @has_modules ||= File.exists?(File.join(@project_root, '.gitmodules')) +end - def parse_request - if @request.match(/\d/) - GROUPS.keys[@request.to_i - 1] - else - @request.to_sym +# 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 end - ######### Outputting methods ######### - - def print_header - puts delimiter(:header) + header - puts delimiter(:header) if anything_changed? + 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 - 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 + # Store data + @stat_hash[group] << {:msg => msg, :col => col, :file => file} if msg - def print_group_header(type) - puts "#{gmu('➤', type, 1)} #{GROUPS[type]}" - end - - - ######### Items of interest ######### - - def branch - @status.lines.first.strip[/^On branch (.*)$/, 1] - end - - def ahead - "+#{@ahead}" if @ahead - end - - def behind - "-#{@behind}" if @behind - end - - def difference - [behind, ahead].compact.join('/') - 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 clean_state - "No changes (working directory clean)" - end - - def hotkey - "[*] => $#{ENV['git_env_char']}" - end - - # used to delimit the left side of the screen - looks nice - def delimiter(col) - gmu("# ", col) - end - - def filelist - "@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}" - end - - - ######### 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}" + # Work tree modification states + if x == "R" && y == "M" + # Extract the second file name from the format x -> y + quoted, unquoted = /^(?:"(?:[^"\\]|\\.)*"|[^"].*) -> (?:"((?:[^"\\]|\\.)*)"|(.*[^"]))$/.match(file)[1..2] + renamed_file = quoted || unquoted + @stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => renamed_file} + elsif x != "R" && 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 -class GitChange < GitStatus - attr_reader :status +def relative_path(base, target) + back = "" + while target.sub(base,'') == target + base = base.sub(/\/[^\/]*$/, '') + back = "../#{back}" + end + "#{back}#{target.sub("#{base}/",'')}" +end - # Restructively singles out the submodules message and - # strips the remaining string to get rid of padding - def initialize(file_and_message, status) - @message = file_and_message.slice!(/\(.*\)/) - @file = file_and_message.strip - @file = (@file.include? " ") ? "\"#{@file}\"" : @file - @status = status.strip + +# 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') + else + # Else, strip file + h[:file].strip + end end - def absolute_path - File.expand_path(@file, Dir.pwd) - end + puts "#{c_group}##{@c[:rst]}" # Extra '#' +end - STATUS_COLORS = { - "copied" => :cpy, - "both deleted" => :del, - "deleted by us" => :del, - "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, - "renamed" => :ren, - "typechange" => :typ, - "untracked" => :unt, - } - # 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 +[[: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 - # 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) + # 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) + end end end -GitStatus.new(ARGV.first).report +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("|") diff --git a/lib/git/status_shortcuts.rb.legacy b/lib/git/status_shortcuts.rb.legacy deleted file mode 100644 index e121b8b..0000000 --- a/lib/git/status_shortcuts.rb.legacy +++ /dev/null @@ -1,218 +0,0 @@ -#!/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 --show-toplevel 2> /dev/null`.strip - -@git_status = `\git status --porcelain -b 2> /dev/null` - -git_status_lines = @git_status.split("\n") -git_branch = git_status_lines[0] -@branch = git_branch[/^## (?:Initial commit on )?([^ \.]+)/, 1] -@ahead = git_branch[/\[ahead ?(\d+).*\]/, 1] -@behind = git_branch[/\[.*behind ?(\d+)\]/, 1] - -@changes = git_status_lines[1..-1] -# Exit if too many changes -exit if @changes.size > ENV["gs_max_changes"].to_i - -# 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 - 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 x == "R" && y == "M" - # Extract the second file name from the format x -> y - quoted, unquoted = /^(?:"(?:[^"\\]|\\.)*"|[^"].*) -> (?:"((?:[^"\\]|\\.)*)"|(.*[^"]))$/.match(file)[1..2] - renamed_file = quoted || unquoted - @stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => renamed_file} - elsif x != "R" && 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 = "\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') - 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="\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) - 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("|") diff --git a/lib/git/status_shortcuts_refactor.rb b/lib/git/status_shortcuts_refactor.rb new file mode 100644 index 0000000..619d145 --- /dev/null +++ b/lib/git/status_shortcuts_refactor.rb @@ -0,0 +1,328 @@ +#!/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 + def initialize(request = nil) + @request = request.to_s # capture nils + @status = get_status + @ahead, @behind = parse_remote_stat + @grouped_changes = parse_changes + @index = 0 + end + + def report + exit if all_changes.length > ENV["gs_max_changes"].to_i + print_header + print_groups + puts filelist if @grouped_changes.any? + end + + + ######### 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/) + remote_line.match(/.*(\d*).*(\d*)/).captures + else + [remote_line[/is ahead of.*by (\d*).*/, 1], remote_line[/is behind.*by (\d*).*/, 1]] + 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| + 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 + + # 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(.*:)?(.*)/) + 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 + + 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 + + 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 + + + ######### Outputting methods ######### + + def print_header + puts delimiter(:header) + header + puts delimiter(:header) if anything_changed? + 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 + + + ######### Items of interest ######### + + def branch + @status.lines.first.strip[/^On branch (.*)$/, 1] + end + + def ahead + "+#{@ahead}" if @ahead + end + + def behind + "-#{@behind}" if @behind + end + + def difference + [behind, ahead].compact.join('/') + 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 clean_state + "No changes (working directory clean)" + end + + def hotkey + "[*] => $#{ENV['git_env_char']}" + end + + # used to delimit the left side of the screen - looks nice + def delimiter(col) + gmu("# ", col) + end + + def filelist + "@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}" + end + + + ######### 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) + @message = file_and_message.slice!(/\(.*\)/) + @file = file_and_message.strip + @file = (@file.include? " ") ? "\"#{@file}\"" : @file + @status = status.strip + end + + def absolute_path + File.expand_path(@file, Dir.pwd) + end + + STATUS_COLORS = { + "copied" => :cpy, + "both deleted" => :del, + "deleted by us" => :del, + "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, + "renamed" => :ren, + "typechange" => :typ, + "untracked" => :unt, + } + + # 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 +end + +GitStatus.new(ARGV.first).report From 8761a4758bc06c3d0c72c4c751736cb05c3ac35f Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Wed, 8 Oct 2014 14:53:26 -0400 Subject: [PATCH 51/79] Add alias & default shortcut for `git mergetool` Provides parity with the `gdt` alias for `git difftool` --- git.scmbrc.example | 1 + lib/git/aliases.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/git.scmbrc.example b/git.scmbrc.example index a1ccca3..e5e2aa0 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -53,6 +53,7 @@ git_diff_file_alias="gdf" git_diff_word_alias="gdw" git_diff_cached_alias="gdc" git_difftool_alias="gdt" +git_mergetool_alias="gmt" # 3. Standard commands git_clone_alias="gcl" git_fetch_alias="gf" diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index 28b0e31..91e3c9d 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -113,6 +113,7 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_add_patch_alias" 'git' 'add' '-p' __git_alias "$git_add_updated_alias" 'git' 'add' '-u' __git_alias "$git_difftool_alias" 'git' 'difftool' + __git_alias "$git_mergetool_alias" 'git' 'mergetool' # Custom default format for git log git_log_command="log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" From 21d155776ab0b368af1df67995af06ba03fbd182 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Wed, 12 Sep 2018 15:44:51 +0700 Subject: [PATCH 52/79] Allow run_tests.sh to be executed from anywhere --- run_tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run_tests.sh b/run_tests.sh index e25c5eb..8827f8b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -8,8 +8,9 @@ failed=false if [ -z "$TEST_SHELLS" ]; then TEST_SHELLS="bash zsh" fi - echo "== Will run all tests with following shells: ${TEST_SHELLS}" + +builtin cd -P -- "${0%/*}" # Change to directory this script lives in for test in $(find test/lib -name *_test.sh); do for shell in $TEST_SHELLS; do echo "== Running tests with [$shell]: $test" From 80ec1ad2adc962274f9dd0a2fd8d543dda344318 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Wed, 12 Sep 2018 17:57:46 +0700 Subject: [PATCH 53/79] Replace built-in shell array quoting with printf %q --- lib/scm_breeze.sh | 43 +++++++++++++++++---- test/lib/git/status_shortcuts_test.sh | 54 +++++++++++++++++++++------ test/lib/scm_breeze_test.sh | 6 +-- test/support/test_helper.sh | 14 ------- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/lib/scm_breeze.sh b/lib/scm_breeze.sh index b177db9..c9cb87e 100644 --- a/lib/scm_breeze.sh +++ b/lib/scm_breeze.sh @@ -18,18 +18,45 @@ _alias() { fi } +# Quote the contents of "$@" +function token_quote { + # Older versions of {ba,z}sh don't support the built-in quoting, so fall back to printf %q + local quoted=() + for token; do + quoted+=( "$(printf '%q' "$token")" ) + done + printf '%s\n' "${quoted[*]}" + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See https://github.com/scmbreeze/scm_breeze/issues/260 + # + # if [[ $shell = bash ]]; then + # # ${parameter@operator} where parameter is ${@} and operator is 'Q' + # # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # eval "${@@Q}" + # else # zsh + # # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + # eval "${(q-)@}" + # fi +} + # Quote "$@" before `eval` to prevent arbitrary code execution. # Eg, the following will run `date`: # evil() { eval "$@"; }; evil "echo" "foo;date" function _safe_eval() { - if [[ $shell = bash ]]; then - # ${parameter@operator} where parameter is ${@} and operator is 'Q' - # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html - eval "${@@Q}" - else # zsh - # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags - eval "${(q-)@}" - fi + eval $(token_quote "$@") + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See https://github.com/scmbreeze/scm_breeze/issues/260 + # + # if [[ $shell = bash ]]; then + # # ${parameter@operator} where parameter is ${@} and operator is 'Q' + # # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html + # eval "${@@Q}" + # else # zsh + # # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + # eval "${(q-)@}" + # fi } find_binary(){ diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index 3f54c5d..0cbf23b 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -51,38 +51,70 @@ setupTestRepo() { test_scmb_expand_args() { local e1="one" e2="two" e3="three" e4="four" e5="five" e6="six" e7='$dollar' e8='two words' local error="Args not expanded correctly" - assertEquals "$error" "'one' 'three' 'six'" \ + assertEquals "$error" 'one three six' \ "$(eval a=$(scmb_expand_args 1 3 6); token_quote "${a[@]}")" - assertEquals "$error" "'one' 'two' 'three' 'five'" \ + assertEquals "$error" 'one two three five' \ "$(eval a=$(scmb_expand_args 1-3 5); token_quote "${a[@]}")" - assertEquals "$error" "'\$dollar' 'two' 'three' 'four' 'one'" \ + assertEquals "$error" '\$dollar two three four one' \ "$(eval a=$(scmb_expand_args 7 2-4 1); token_quote "${a[@]}")" # Test that any args with spaces remain quoted - assertEquals "$error" "'-m' 'Test Commit Message' 'one'" \ + assertEquals "$error" '-m Test\ Commit\ Message one' \ "$(eval a=$(scmb_expand_args -m "Test Commit Message" 1); token_quote "${a[@]}")" - assertEquals "$error" "'-ma' 'Test Commit Message' 'Unquoted'"\ + assertEquals "$error" '-ma Test\ Commit\ Message Unquoted'\ "$(eval a=$(scmb_expand_args -ma "Test Commit Message" "Unquoted"); token_quote "${a[@]}")" - assertEquals "$error" "'\$dollar' 'one' 'two words'" \ + assertEquals "$error" '\$dollar one two\ words' \ "$(eval a=$(scmb_expand_args 7 1-1 8); token_quote "${a[@]}")" + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See token_quote() source and https://github.com/scmbreeze/scm_breeze/issues/260 + # local e1="one" e2="two" e3="three" e4="four" e5="five" e6="six" e7='$dollar' e8='two words' + # local error="Args not expanded correctly" + # assertEquals "$error" "'one' 'three' 'six'" \ + # "$(eval a=$(scmb_expand_args 1 3 6); token_quote "${a[@]}")" + # assertEquals "$error" "'one' 'two' 'three' 'five'" \ + # "$(eval a=$(scmb_expand_args 1-3 5); token_quote "${a[@]}")" + # assertEquals "$error" "'\$dollar' 'two' 'three' 'four' 'one'" \ + # "$(eval a=$(scmb_expand_args 7 2-4 1); token_quote "${a[@]}")" + # + # # Test that any args with spaces remain quoted + # assertEquals "$error" "'-m' 'Test Commit Message' 'one'" \ + # "$(eval a=$(scmb_expand_args -m "Test Commit Message" 1); token_quote "${a[@]}")" + # assertEquals "$error" "'-ma' 'Test Commit Message' 'Unquoted'"\ + # "$(eval a=$(scmb_expand_args -ma "Test Commit Message" "Unquoted"); token_quote "${a[@]}")" + # assertEquals "$error" "'\$dollar' 'one' 'two words'" \ + # "$(eval a=$(scmb_expand_args 7 1-1 8); token_quote "${a[@]}")" } test_exec_scmb_expand_args() { local e1="one" e2="a b c" e3='$dollar' e4="single'quote" e5='double"quote' e6='a(){:;};a&' - assertEquals "literals with spaces not preserved" "'foo' 'bar baz'" \ + assertEquals "literals with spaces not preserved" 'foo bar\ baz' \ "$(eval a="$(scmb_expand_args foo 'bar baz')"; token_quote "${a[@]}")" - assertEquals "variables with spaces not preserved" "'one' 'a b c'" \ + assertEquals "variables with spaces not preserved" 'one a\ b\ c' \ "$(eval a="$(scmb_expand_args 1-2)"; token_quote "${a[@]}")" # Expecting text: '$dollar' "single'quote" 'double"quote' # Generate quoted expected string with: token_quote "$(cat)" then copy/paste, ^D assertEquals "special characters are preserved" \ - "'\$dollar' 'single'\\''quote' 'double\"quote' 'a(){:;};a&'" \ + '\$dollar single\'\''quote double\"quote a\(\)\{:\;\}\;a\&' \ "$(eval a="$(scmb_expand_args 3-6)"; token_quote "${a[@]}")" + + # Keep this code for use when minimum versions of {ba,z}sh can be increased. + # See token_quote() source and https://github.com/scmbreeze/scm_breeze/issues/260 + # local e1="one" e2="a b c" e3='$dollar' e4="single'quote" e5='double"quote' e6='a(){:;};a&' + # assertEquals "literals with spaces not preserved" "'foo' 'bar baz'" \ + # "$(eval a="$(scmb_expand_args foo 'bar baz')"; token_quote "${a[@]}")" + # assertEquals "variables with spaces not preserved" "'one' 'a b c'" \ + # "$(eval a="$(scmb_expand_args 1-2)"; token_quote "${a[@]}")" + # # Expecting text: '$dollar' "single'quote" 'double"quote' + # # Generate quoted expected string with: token_quote "$(cat)" then copy/paste, ^D + # assertEquals "special characters are preserved" \ + # "'\$dollar' 'single'\\''quote' 'double\"quote' 'a(){:;};a&'" \ + # "$(eval a="$(scmb_expand_args 3-6)"; token_quote "${a[@]}")" } test_command_wrapping_escapes_special_characters() { - assertEquals 'should escape | the pipe' "$(exec_scmb_expand_args echo "should escape | the pipe")" - assertEquals 'should escape ; the semicolon' "$(exec_scmb_expand_args echo "should escape ; the semicolon")" + assertEquals 'should escape | the pipe' "$(exec_scmb_expand_args echo should escape '|' the pipe)" + assertEquals 'should escape ; the semicolon' "$(exec_scmb_expand_args echo should escape ';' the semicolon)" } test_git_status_shortcuts() { diff --git a/test/lib/scm_breeze_test.sh b/test/lib/scm_breeze_test.sh index f74beef..5a5a8da 100755 --- a/test/lib/scm_breeze_test.sh +++ b/test/lib/scm_breeze_test.sh @@ -16,9 +16,9 @@ source "$scmbDir/lib/scm_breeze.sh" #----------------------------------------------------------------------------- test__safe_eval() { - assertEquals "runs eval with simple words" "'one' 'two' 'three'" "$(_safe_eval token_quote one two three)" - assertEquals "quotes spaces" "'a' 'b c' 'd'" "$(_safe_eval token_quote a b\ c d)" - assertEquals "quotes special chars" "'a b' '\$dollar' '\\slash' 'c d'" "$(_safe_eval token_quote a\ b '$dollar' '\slash' c\ d)" + assertEquals "runs eval with simple words" 'one two three' "$(_safe_eval token_quote one two three)" + assertEquals "quotes spaces" 'a b\ c d' "$(_safe_eval token_quote a b\ c d)" + assertEquals "quotes special chars" 'a\ b \$dollar \\slash c\ d' "$(_safe_eval token_quote a\ b '$dollar' '\slash' c\ d)" } diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index de8cd1b..67a6155 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -32,20 +32,6 @@ verboseGitCommands() { unset -f git } -# Quote the contents of "$@" in single quotes -# Avoid printf '%q' as 'a b' becomes a\ b in both {ba,z}sh -# See also quote_args and double_quote -function token_quote { - if [[ $shell = bash ]]; then - # Single quotes are always added - echo "${@@Q}" - else # zsh - # Single quotes only added when needed - #shellcheck disable=2154 # zsh - echo "${(qq)@}" - fi -} - # Asserts #----------------------------------------------------------------------------- From e46a7c3309b98d31f0c2e93a1221f5659891a1ba Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 14 Sep 2018 15:42:43 +0700 Subject: [PATCH 54/79] ls_with_file_shortcuts: fail rather than create incorrect variables Removes the danger from issue #274. The proper fix will need to address issue #260. --- lib/git/shell_shortcuts.sh | 28 +++++++++++++++++++++++----- test/lib/git/shell_shortcuts_test.sh | 15 +++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index e3ca7f5..9167787 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -124,11 +124,29 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then setopt shwordsplit fi - # Parse path from args + # Get the directory that `ls` is being run relative to. + # Only allow one directory to avoid incorrect $e# variables when listing + # multiple directories (issue #274) local IFS=$'\n' + local rel_path for arg in "$@"; do - if [ -d "$arg" ]; then local rel_path="${arg%/}"; fi + if [[ -e $arg ]]; then # Path rather than option to ls + if [[ -z $rel_path ]]; then # We are seeing our first pathname + if [[ -d $arg ]]; then # It's a directory + rel_path=$arg + else # It's a file, expand the current directory + rel_path=. + fi + elif [[ -d $arg || ( -f $arg && $rel_path != . ) ]]; then + if [[ -f $arg ]]; then arg=$PWD; fi # Get directory for current argument + # We've already seen a different directory. Quit to avoid damage (issue #274) + printf 'scm_breeze: Cannot list relative to both directories:\n %s\n %s\n' "$arg" "$rel_path" >&2 + printf 'Currently only listing a single directory is supported. See issue #274.\n' >&2 + return 1 + fi + fi done + rel_path=$("${_abs_path_command[@]}" ${rel_path:-$PWD}) # Replace user/group with user symbol, if defined at ~/.user_sym # Before : -rw-rw-r-- 1 ndbroadbent ndbroadbent 1.1K Sep 19 21:39 scm_breeze.sh @@ -177,9 +195,9 @@ EOF local IFS=$'\n' for file in $ll_files; do - if [ -n "$rel_path" ]; then file="$rel_path/$file"; fi - export $git_env_char$e="$(_safe_eval "${_abs_path_command[@]}" "$file")" - if [ "${scmbDebug:-}" = "true" ]; then echo "Set \$$git_env_char$e => $file"; fi + file=$rel_path/$file + export $git_env_char$e=$("${_abs_path_command[@]}" "$file") + if [[ ${scmbDebug:-} = true ]]; then echo "Set \$$git_env_char$e => $file"; fi let e++ done diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 7bab1d3..7f1f6f4 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -107,7 +107,7 @@ test_ls_with_file_shortcuts() { touch 'test file' 'test_file' mkdir -p "a [b]" 'a "b"' "a 'b'" - touch "a \"b\"/c" + touch 'a "b"/c' # Run command in shell, load output from temp file into variable # (This is needed so that env variables are exported in the current shell) @@ -134,11 +134,22 @@ test_ls_with_file_shortcuts() { ls_output=$(<$temp_file strip_colors) assertIncludes "$ls_output" '[1] c' F # Test that env variable is set correctly - assertEquals "$TEST_DIR/a \"b\"/c" "$e1" + assertEquals "$TEST_DIR/"'a "b"/c' "$e1" # Test arg with no quotes ls_output=$(ls_with_file_shortcuts a\ \"b\" | strip_colors) assertIncludes "$ls_output" '[1] c' F + # Listing two directories fails (see issue #275) + mkdir 1 2 + touch 1/file + assertFalse 'Only one directory supported' 'ls_with_file_shortcuts 1 2' + assertFalse 'Fails on ' 'ls_with_file_shortcuts 1 test_file' + assertFalse 'Fails on ' 'ls_with_file_shortcuts test_file 1' + assertFalse 'Fails on /' 'ls_with_file_shortcuts 1 1/file' + + # Files under the root directory + assertTrue 'Shortcuts under /' 'ls_with_file_shortcuts / > /dev/null && [[ $e1 =~ ^/[^/]+$ ]]' + cd - rm -r "$TEST_DIR" "$temp_file" } From 4adf7714924709a3a17c960b3af2513bfe48c738 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 14 Sep 2018 19:20:39 +0700 Subject: [PATCH 55/79] Ensure file ordering matches in both invocations of ls --- lib/git/shell_shortcuts.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 9167787..c41d6d0 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -112,10 +112,13 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then unalias ll > /dev/null 2>&1; unset -f ll > /dev/null 2>&1 function ls_with_file_shortcuts { local ll_output + local ll_command # Ensure sort ordering of the two invocations is the same if [ "$_ls_bsd" != "BSD" ]; then - ll_output="$(\ls -lhv --group-directories-first --color "$@")" + ll_command=(\ls -hv --group-directories-first) + ll_output="$("${ll_command[@]}" -l --color "$@")" else - ll_output="$(CLICOLOR_FORCE=1 \ls -l -G "$@")" + ll_command=(\ls) + ll_output="$(CLICOLOR_FORCE=1 "${ll_command[@]}" -lG "$@")" fi if [[ $shell == "zsh" ]]; then @@ -187,10 +190,14 @@ EOF local ll_files='' local file='' + # XXX FIXME XXX + # There is a race condition here: If a file is removed between the above + # and this second call of `ls` then the $e# variables can refer to the + # wrong files. if [ -z $_ls_bsd ]; then - ll_files="$(\ls -v --group-directories-first --color=never "$@")" + ll_files="$(QUOTING_STYLE=literal "${ll_command[@]}" --color=never "$@")" else - ll_files="$(\ls "$@")" + ll_files="$("${ll_command[@]}" "$@")" fi local IFS=$'\n' From f85b9a4727835c4a0fa3fdad369976a3316b02d8 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 14 Sep 2018 19:21:47 +0700 Subject: [PATCH 56/79] git_show_affected_files: make $f local --- lib/git/status_shortcuts.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index f7c57cf..05f8a80 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -82,7 +82,7 @@ git_silent_add_shortcuts() { eval "args=$(scmb_expand_args "$@")" # create $args array for file in "${args[@]}"; do # Use 'git rm' if file doesn't exist and 'ga_auto_remove' is enabled. - if [[ $ga_auto_remove == "yes" ]] && ! [ -e "$file" ]; then + if [[ $ga_auto_remove = yes && ! -e $file ]]; then echo -n "# " git rm "$file" else @@ -98,7 +98,7 @@ git_silent_add_shortcuts() { # and exports numbered environment variables for each file. git_show_affected_files(){ fail_if_not_git_repo || return 1 - f=0 # File count + local f=0 # File count # Show colored revision and commit message echo -n "# "; git show --oneline --name-only "$@" | head -n1; echo "# " for file in $(git show --pretty="format:" --name-only "$@" | \grep -v '^$'); do From e5da83665a79ab9b2b253f7fba8d005f4917d969 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Fri, 14 Sep 2018 19:29:06 +0700 Subject: [PATCH 57/79] bin_path(): use builtin commands and avoid `which` --- lib/git/aliases.sh | 2 +- lib/git/helpers.sh | 9 ++++++++- lib/git/shell_shortcuts.sh | 2 +- test/lib/git/shell_shortcuts_test.sh | 7 ------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index e9c8f59..b7b3f84 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -17,7 +17,7 @@ unalias git > /dev/null 2>&1 unset -f git > /dev/null 2>&1 # Use the full path to git to avoid infinite loop with git function -export _git_cmd="$(\which git)" +export _git_cmd="$(bin_path git)" # Wrap git with the 'hub' github wrapper, if installed (https://github.com/defunkt/hub) if type hub > /dev/null 2>&1; then export _git_cmd="hub"; fi diff --git a/lib/git/helpers.sh b/lib/git/helpers.sh index f2d4107..785bd60 100644 --- a/lib/git/helpers.sh +++ b/lib/git/helpers.sh @@ -13,4 +13,11 @@ function fail_if_not_git_repo() { return 1 fi return 0 -} \ No newline at end of file +} + +bin_path() { + if [[ -n ${ZSH_VERSION:-} ]]; + then builtin whence -cp "$1" 2> /dev/null + else builtin type -P "$1" + fi +} diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index c41d6d0..03348a1 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -108,7 +108,7 @@ fi # Function wrapper around 'll' # Adds numbered shortcuts to output of ls -l, just like 'git status' -if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then +if [ "$shell_ls_aliases_enabled" = "true" ] && builtin command -v ruby > /dev/null 2>&1; then unalias ll > /dev/null 2>&1; unset -f ll > /dev/null 2>&1 function ls_with_file_shortcuts { local ll_output diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index 7f1f6f4..6e470a7 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -21,13 +21,6 @@ fi source "$scmbDir/test/support/test_helper.sh" source "$scmbDir/lib/scm_breeze.sh" -bin_path() { - if [ -n "${ZSH_VERSION:-}" ]; - then where "$@" | tail -1 - else which "$@" - fi -} - # Setup #----------------------------------------------------------------------------- oneTimeSetUp() { From 48302bcd8c70115598d1d7323e235b591ff8b884 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Sun, 16 Sep 2018 20:31:15 +0700 Subject: [PATCH 58/79] Be compatible with shells of Ubuntu 14.04 --- lib/git/repo_index.sh | 7 ++++--- lib/git/status_shortcuts.sh | 30 +++++++++++++++++++-------- lib/git/tools.sh | 3 ++- lib/scm_breeze.sh | 3 ++- run_tests.sh | 2 +- test/lib/git/status_shortcuts_test.sh | 18 ++++++++-------- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index b3f4005..9edcd24 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -202,15 +202,16 @@ _git_index_update_all_branches() { return fi - local remotes merges branches + # zsh 5.0.2 requires local separate to assignment for arrays + local remote merge remotes merges branches # Get branch configuration from .git/config local IFS=$'\n' for branch in $($GIT_BINARY branch 2> /dev/null | sed -e 's/.\{2\}\(.*\)/\1/'); do # Skip '(no branch)' if [[ "$branch" = "(no branch)" ]]; then continue; fi - local remote=$(git config --get branch.$branch.remote) - local merge=$(git config --get branch.$branch.merge) + remote=$(git config --get "branch.$branch.remote") + merge=$(git config --get "branch.$branch.merge") # Ignore branch if remote and merge is not configured if [[ -n "$remote" ]] && [[ -n "$merge" ]]; then diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index 05f8a80..04f0c96 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -79,7 +79,8 @@ git_add_shortcuts() { git_silent_add_shortcuts() { if [ -n "$1" ]; then # Expand args and process resulting set of files. - eval "args=$(scmb_expand_args "$@")" # create $args array + local args + eval args="$(scmb_expand_args "$@")" # populate $args array for file in "${args[@]}"; do # Use 'git rm' if file doesn't exist and 'ga_auto_remove' is enabled. if [[ $ga_auto_remove = yes && ! -e $file ]]; then @@ -108,8 +109,8 @@ git_show_affected_files(){ done; echo "# " } - # Allows expansion of numbered shortcuts, ranges of shortcuts, or standard paths. +# Return a string which can be `eval`ed like: eval args="$(scmb_expand_args "$@")" # Numbered shortcut variables are produced by various commands, such as: # * git_status_shortcuts() - git status implementation # * git_show_affected_files() - shows files affected by a given SHA1, etc. @@ -120,7 +121,8 @@ scmb_expand_args() { shift fi - local -a args=() # initially empty array + local args + args=() # initially empty array. zsh 5.0.2 from Ubuntu 14.04 requires this to be separated for arg in "$@"; do if [[ "$arg" =~ ^[0-9]{0,4}$ ]] ; then # Substitute $e{*} variables for any integers if [ -e "$arg" ]; then @@ -137,14 +139,22 @@ scmb_expand_args() { args+=("$arg") fi done - args=$(declare -p args) # Get $args array as a string which can be `eval`-ed to recreate itself - args=${args#*=} # Remove `typeset -a args=` from beginning of string to allow caller to name variable - echo "$args" + + # "declare -p" with zsh 5.0.2 on Ubuntu 14.04 creates a string that it cannot process: + # typeset -a args args=(one three six) + # There should be a ; between the two "args" tokens + # "declare -p" with bash 4.3.11(1) on Ubuntu 14.04 creates a string like: + # declare -a a='([0]="a" [1]="b c" [2]="d")' + # The RHS of this string is incompatible with zsh 5.0.2 and "eval args=" + + # Generate a quoted array string to assign to "eval args=" + echo "( $(token_quote "${args[@]}") )" } # Expand a variable (named by $2) into a (possibly relative) pathname _print_path() { - local pathname=$(eval printf '%s' "\"\${$2}\"") + local pathname + pathname=$(eval printf '%s' "\"\${$2}\"") if [ "$1" = 1 ]; then # print relative pathname=${pathname#$PWD/} # Remove $PWD from beginning of the path fi @@ -154,7 +164,8 @@ _print_path() { # Execute a command with expanded args, e.g. Delete files 6 to 12: $ ge rm 6-12 # Fails if command is a number or range (probably not worth fixing) exec_scmb_expand_args() { - eval "args=$(scmb_expand_args "$@")" # create $args array + local args + eval "args=$(scmb_expand_args "$@")" # populate $args array _safe_eval "${args[@]}" } @@ -177,7 +188,8 @@ git_clear_vars() { _git_resolve_merge_conflict() { if [ -n "$2" ]; then # Expand args and process resulting set of files. - eval "args=$(scmb_expand_args "$@")" # create $args array + local args + eval "args=$(scmb_expand_args "$@")" # populate $args array for file in "${args[@]:2}"; do git checkout "--$1""s" "$file" # "--$1""s" is expanded to --ours or --theirs git add "$file" diff --git a/lib/git/tools.sh b/lib/git/tools.sh index e5e2c5d..c158a77 100644 --- a/lib/git/tools.sh +++ b/lib/git/tools.sh @@ -23,6 +23,7 @@ git_remove_history() { return fi # Remove all paths passed as arguments from the history of the repo + local files files=("$@") $_git_cmd filter-branch --index-filter "$_git_cmd rm -rf --cached --ignore-unmatch ${files[*]}" HEAD # Remove the temporary history git-filter-branch otherwise leaves behind for a long time @@ -142,4 +143,4 @@ git_branch_delete_all() { commit_docs() { git commit -m "Update README / Documentation [ci skip]" -} \ No newline at end of file +} diff --git a/lib/scm_breeze.sh b/lib/scm_breeze.sh index c9cb87e..2744b00 100644 --- a/lib/scm_breeze.sh +++ b/lib/scm_breeze.sh @@ -21,7 +21,8 @@ _alias() { # Quote the contents of "$@" function token_quote { # Older versions of {ba,z}sh don't support the built-in quoting, so fall back to printf %q - local quoted=() + local quoted + quoted=() # Assign separately for zsh 5.0.2 of Ubuntu 14.04 for token; do quoted+=( "$(printf '%q' "$token")" ) done diff --git a/run_tests.sh b/run_tests.sh index 8827f8b..c0a3149 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -10,7 +10,7 @@ if [ -z "$TEST_SHELLS" ]; then fi echo "== Will run all tests with following shells: ${TEST_SHELLS}" -builtin cd -P -- "${0%/*}" # Change to directory this script lives in +cd -P -- "${0%/*}" # Change to directory this script lives in for test in $(find test/lib -name *_test.sh); do for shell in $TEST_SHELLS; do echo "== Running tests with [$shell]: $test" diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index 0cbf23b..d8b9e47 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -52,19 +52,19 @@ test_scmb_expand_args() { local e1="one" e2="two" e3="three" e4="four" e5="five" e6="six" e7='$dollar' e8='two words' local error="Args not expanded correctly" assertEquals "$error" 'one three six' \ - "$(eval a=$(scmb_expand_args 1 3 6); token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args 1 3 6)"; token_quote "${args[@]}")" assertEquals "$error" 'one two three five' \ - "$(eval a=$(scmb_expand_args 1-3 5); token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args 1-3 5)"; token_quote "${args[@]}")" assertEquals "$error" '\$dollar two three four one' \ - "$(eval a=$(scmb_expand_args 7 2-4 1); token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args 7 2-4 1)"; token_quote "${args[@]}")" # Test that any args with spaces remain quoted assertEquals "$error" '-m Test\ Commit\ Message one' \ - "$(eval a=$(scmb_expand_args -m "Test Commit Message" 1); token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args -m "Test Commit Message" 1)"; token_quote "${args[@]}")" assertEquals "$error" '-ma Test\ Commit\ Message Unquoted'\ - "$(eval a=$(scmb_expand_args -ma "Test Commit Message" "Unquoted"); token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args -ma "Test Commit Message" "Unquoted")"; token_quote "${args[@]}")" assertEquals "$error" '\$dollar one two\ words' \ - "$(eval a=$(scmb_expand_args 7 1-1 8); token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args 7 1-1 8)"; token_quote "${args[@]}")" # Keep this code for use when minimum versions of {ba,z}sh can be increased. # See token_quote() source and https://github.com/scmbreeze/scm_breeze/issues/260 @@ -89,14 +89,14 @@ test_scmb_expand_args() { test_exec_scmb_expand_args() { local e1="one" e2="a b c" e3='$dollar' e4="single'quote" e5='double"quote' e6='a(){:;};a&' assertEquals "literals with spaces not preserved" 'foo bar\ baz' \ - "$(eval a="$(scmb_expand_args foo 'bar baz')"; token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args foo 'bar baz')"; token_quote "${args[@]}")" assertEquals "variables with spaces not preserved" 'one a\ b\ c' \ - "$(eval a="$(scmb_expand_args 1-2)"; token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args 1-2)"; token_quote "${args[@]}")" # Expecting text: '$dollar' "single'quote" 'double"quote' # Generate quoted expected string with: token_quote "$(cat)" then copy/paste, ^D assertEquals "special characters are preserved" \ '\$dollar single\'\''quote double\"quote a\(\)\{:\;\}\;a\&' \ - "$(eval a="$(scmb_expand_args 3-6)"; token_quote "${a[@]}")" + "$(eval args="$(scmb_expand_args 3-6)"; token_quote "${args[@]}")" # Keep this code for use when minimum versions of {ba,z}sh can be increased. # See token_quote() source and https://github.com/scmbreeze/scm_breeze/issues/260 From 29db6322893b8b5c6e0f518121e6a5bd9e62432c Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Tue, 18 Sep 2018 14:28:45 +0700 Subject: [PATCH 59/79] .travis.yml: add sudo: required Avoid Travis CI error for zsh on linux: $ ./test/support/travisci_deps.sh This job is running on container-based infrastructure, which does not allow use of 'sudo', setuid, and setgid executables. If you require sudo, add 'sudo: required' to your .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index d038f60..a16628d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ env: - TEST_SHELLS=bash - TEST_SHELLS=zsh +sudo: required + install: - ./test/support/travisci_deps.sh From 5530e693b37586fbda8fce2a8e8d4279c5482e50 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Mon, 24 Sep 2018 18:06:23 +0700 Subject: [PATCH 60/79] Added ~/.staff_sym emoji, and some docs to the README --- README.md | 16 ++++++++++++++++ docs/images/custom_user_and_staff_symbols.jpg | Bin 0 -> 135540 bytes lib/git/shell_shortcuts.sh | 13 ++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/images/custom_user_and_staff_symbols.jpg diff --git a/README.md b/README.md index 338cc22..5f355f8 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,22 @@ as `gs` for the extended `git status`, and `ga` for the `git add` function. If you already have an alias like `alias gco="git checkout"`, you can now type `gco 1` to checkout the first file in the output of SCM Breeze's `git status`. +## Custom emojis for username and "staff" group + +The `ll` command adds numbered shortcuts to files, but another fun feature is replacing your +username and the "staff" group with custom emojis: + +![Custom user and staff emojis](/docs/images/custom_user_and_staff_symbols.jpg) + +Set your own emojis by running: + +```bash +echo 🍀 > ~/.user_sym +echo 🖥 > ~/.staff_sym +``` + +I also like using `~/.user_sym` [in my Bash prompt](https://github.com/ndbroadbent/dotfiles/blob/master/bashrc/prompt.sh#L71). + ## Notes about Tab Completion for Aliases diff --git a/docs/images/custom_user_and_staff_symbols.jpg b/docs/images/custom_user_and_staff_symbols.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b65e0558566872f42ffaed9e3f5c0a0461141eb4 GIT binary patch literal 135540 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 T|zq`Y1kKQnjq*3Ho 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 literal 0 HcmV?d00001 diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 4c427d8..9b5423e 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -142,7 +142,18 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && which ruby > /dev/null 2>&1; then puts o.lines.map{|l|l.sub(re){|m|\"%s%-#{u}s %-#{g}s%#{s}s \"%[\$1,*\$3.split]}}" } - ll_output=$(echo "$ll_output" | \sed -$SED_REGEX_ARG "s/ $USER/ $(/bin/cat $HOME/.user_sym)/g" | rejustify_ls_columns) + if [ -f "$HOME/.user_sym" ]; then + local USER_SYM=$(/bin/cat $HOME/.user_sym) + if [ -f "$HOME/.staff_sym" ]; then + local STAFF_SYM=$(/bin/cat $HOME/.staff_sym) + ll_output=$(echo "$ll_output" | \ + \sed -$SED_REGEX_ARG "s/ $USER staff/ $USER_SYM $STAFF_SYM /g") + fi + ll_output=$(echo "$ll_output" | \ + \sed -$SED_REGEX_ARG "s/ $USER/ $USER_SYM /g") + fi + + ll_output=$(echo "$ll_output" | rejustify_ls_columns) fi if [ "$(echo "$ll_output" | wc -l)" -gt "50" ]; then From 7e32c12f6c3c61b62598ceb47ac13709db5caad3 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Mon, 24 Sep 2018 18:09:48 +0700 Subject: [PATCH 61/79] Fix max-width of image in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f355f8..76df131 100644 --- a/README.md +++ b/README.md @@ -353,9 +353,9 @@ If you already have an alias like `alias gco="git checkout"`, you can now type ## Custom emojis for username and "staff" group The `ll` command adds numbered shortcuts to files, but another fun feature is replacing your -username and the "staff" group with custom emojis: +username and the "staff" group with custom emojis. You can set these in `~/.user_sym` and `~/.staff_sym`. -![Custom user and staff emojis](/docs/images/custom_user_and_staff_symbols.jpg) +Custom user and staff emojis Set your own emojis by running: From c76a580b975cf5cd51bdc79c485ae0cd73ec4524 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Mon, 24 Sep 2018 18:12:26 +0700 Subject: [PATCH 62/79] Fix image path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76df131..727297d 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,7 @@ If you already have an alias like `alias gco="git checkout"`, you can now type The `ll` command adds numbered shortcuts to files, but another fun feature is replacing your username and the "staff" group with custom emojis. You can set these in `~/.user_sym` and `~/.staff_sym`. -Custom user and staff emojis +Custom user and staff emojis Set your own emojis by running: From 1f60a87a77c9181a24cb05d6049b2c7a6cd2e512 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Mon, 24 Sep 2018 18:16:24 +0700 Subject: [PATCH 63/79] Figured out how to set image width in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 727297d..60ed7e8 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,7 @@ If you already have an alias like `alias gco="git checkout"`, you can now type The `ll` command adds numbered shortcuts to files, but another fun feature is replacing your username and the "staff" group with custom emojis. You can set these in `~/.user_sym` and `~/.staff_sym`. -Custom user and staff emojis +Custom user and staff emojis Set your own emojis by running: From 8233abd23fd3edec3651e969fde49b706d44c5d3 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 26 Sep 2018 23:04:51 +0700 Subject: [PATCH 64/79] Save/restore the git commit message in case a git pre-commit hook fails. Also rename the APPEND var to GIT_COMMIT_MSG_SUFFIX. --- lib/git/keybindings.sh | 4 +- lib/git/status_shortcuts.sh | 63 ++++++++++++++++++++------- test/lib/git/status_shortcuts_test.sh | 5 ++- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/lib/git/keybindings.sh b/lib/git/keybindings.sh index b286075..dc9f186 100644 --- a/lib/git/keybindings.sh +++ b/lib/git/keybindings.sh @@ -35,11 +35,11 @@ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then if [[ $shell == "zsh" ]]; then _bind "$git_commit_all_keys" " git_commit_all""$RETURN_CHAR" _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""$RETURN_CHAR" - _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""$RETURN_CHAR" + _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ GIT_COMMIT_MSG_SUFFIX='[ci skip]' git_commit_all ""$RETURN_CHAR" else _bind "$git_commit_all_keys" "\" git_commit_all$RETURN_CHAR\"" _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit $RETURN_CHAR\"" - _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all $RETURN_CHAR\"" + _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A GIT_COMMIT_MSG_SUFFIX='[ci skip]' git_commit_all $RETURN_CHAR\"" fi fi diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index d63bb0c..bd585c0 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -203,29 +203,62 @@ theirs(){ _git_resolve_merge_conflict "their" "$@"; } # * Add escaped commit command and unescaped message to bash history. git_commit_prompt() { local commit_msg + local saved_commit_msg + if [ -f "/tmp/.git_commit_message~" ]; then + saved_commit_msg="$(cat /tmp/.git_commit_message~)" + echo -e "\033[0;36mLeave blank to use saved commit message: \033[0m$saved_commit_msg" + fi if [[ $shell == "zsh" ]]; then vared -h -p "Commit Message: " commit_msg else read -r -e -p "Commit Message: " commit_msg fi - if [ -n "$commit_msg" ]; then - eval $@ # run any prequisite commands - # Add $APPEND to commit message, if given. (Used to append things like [ci skip] for Travis CI) - if [ -n "$APPEND" ]; then commit_msg="$commit_msg $APPEND"; fi - echo $commit_msg | git commit -F - | tail -n +2 - else - echo -e "\033[0;31mAborting commit due to empty commit message.\033[0m" + if [ -z "$commit_msg" ]; then + if [ -n "$saved_commit_msg" ]; then + commit_msg="$saved_commit_msg" + else + echo -e "\033[0;31mAborting commit due to empty commit message.\033[0m" + return + fi fi - escaped=$(echo "$commit_msg" | sed -e 's/"/\\"/g' -e 's/!/"'"'"'!'"'"'"/g') + # Add $GIT_COMMIT_MSG_SUFFIX to commit message, if given. + # (Used to append things like [ci skip] for Travis CI) + if [ -n "$GIT_COMMIT_MSG_SUFFIX" ]; then + commit_msg="$commit_msg $GIT_COMMIT_MSG_SUFFIX" + fi + + # Exclamation marks are really difficult to escape properly in a bash prompt. + # They must always be enclosed with single quotes. + escaped_msg=$(echo "$commit_msg" | sed -e 's/"/\\"/g' -e "s/!/\"'!'\"/g") + # Add command to bash history, so that if a git pre-commit hook fails, + # you can just press "up" and "return" to retry the commit. if [[ $shell == "zsh" ]]; then - print -s "git commit -m \"${escaped//\\/\\\\}\"" # zsh's print needs double escaping - print -s "$commit_msg" + # zsh's print needs double escaping + print -s "git commit -m \"${escaped_msg//\\/\\\\}\"" else - echo "git commit -m \"$escaped\"" >> $HISTFILE - # Also add unescaped commit message, for git prompt - echo "$commit_msg" >> $HISTFILE + history -s "git commit -m \"$escaped_msg\"" + # Need to write history to a file for tests + if [ -n "$SHUNIT_VERSION" ]; then history -w $HISTFILE; fi + fi + + # Also save the commit message to a temp file in case git commit fails + echo "$commit_msg" > "/tmp/.git_commit_message~" + eval $@ # run any prequisite commands + + echo "$commit_msg" | git commit -F - | tail -n +2 + + # Fetch the pipe status (for both bash and zsh): + GIT_PIPE_STATUS=("${PIPESTATUS[@]}${pipestatus[@]}") + if [[ $shell == "zsh" ]]; then + git_exit_status="${GIT_PIPE_STATUS[2]}" # zsh array indexes start at 1 + else + git_exit_status="${GIT_PIPE_STATUS[1]}" + fi + if [[ "$git_exit_status" == 0 ]]; then + # Delete saved commit message if commit was successful + rm -f "/tmp/.git_commit_message~" fi } @@ -234,8 +267,8 @@ git_commit_all() { fail_if_not_git_repo || return 1 changes=$(git status --porcelain | wc -l | tr -d ' ') if [ "$changes" -gt 0 ]; then - if [ -n "$APPEND" ]; then - local appending=" | \033[0;36mappending '\033[1;36m$APPEND\033[0;36m' to commit message.\033[0m" + if [ -n "$GIT_COMMIT_MSG_SUFFIX" ]; then + local appending=" | \033[0;36mappending '\033[1;36m$GIT_COMMIT_MSG_SUFFIX\033[0;36m' to commit message.\033[0m" fi echo -e "\033[0;33mCommitting all files (\033[0;31m$changes\033[0;33m)\033[0m$appending" git_commit_prompt "git add --all ." diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index d83d9ca..638858a 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -260,9 +260,10 @@ test_git_commit_prompt() { if [[ $shell == "zsh" ]]; then test_history="$(history)" else + # Need to load history from $HISTFILE + # (Couldn't get the 'history' builtin to work during tests.) test_history="$(cat $HISTFILE)" fi - assertIncludes "$test_history" "$commit_msg" assertIncludes "$test_history" "git commit -m \"$dbl_escaped_msg\"" } @@ -284,7 +285,7 @@ test_git_commit_prompt_with_append() { # Test the git commit prompt, by piping a commit message # instead of user input. - echo "$commit_msg" | APPEND="[ci skip]" git_commit_prompt > /dev/null + echo "$commit_msg" | GIT_COMMIT_MSG_SUFFIX="[ci skip]" git_commit_prompt > /dev/null git_show_output=$(git show --oneline --name-only) assertIncludes "$git_show_output" "$commit_msg \[ci skip\]" From e5971cfdd52da795792197f70342f11cb19d7a54 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 26 Sep 2018 23:13:31 +0700 Subject: [PATCH 65/79] Added keyboard shortcut to add all changes to the previous commit (Ctrl+x, z) --- git.scmbrc.example | 7 ++++--- lib/git/keybindings.sh | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/git.scmbrc.example b/git.scmbrc.example index e5e2aa0..287bfaa 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -113,9 +113,10 @@ git_pull_request_alias="gpr" # ---------------------------------------------- # Keyboard shortcuts are on by default. Set this to 'false' to disable them. git_keyboard_shortcuts_enabled="true" -git_commit_all_keys="\C-x " # CTRL+x, SPACE -git_add_and_commit_keys="\C-xc" # CTRL+x, c -git_commit_all_with_ci_skip_keys="\C-xv" # CTRL+x, v (Appends [ci skip] to commit message) +git_commit_all_keys="\C-x " # CTRL+x, SPACE +git_add_and_commit_keys="\C-xc" # CTRL+x, c +git_commit_all_with_ci_skip_keys="\C-xv" # CTRL+x, v (Appends [ci skip] to message) +git_add_and_amend_commit_keys="\C-xz" # CTRL+x, z # Shell Command Wrapping diff --git a/lib/git/keybindings.sh b/lib/git/keybindings.sh index dc9f186..4bbca42 100644 --- a/lib/git/keybindings.sh +++ b/lib/git/keybindings.sh @@ -36,10 +36,12 @@ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then _bind "$git_commit_all_keys" " git_commit_all""$RETURN_CHAR" _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""$RETURN_CHAR" _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ GIT_COMMIT_MSG_SUFFIX='[ci skip]' git_commit_all ""$RETURN_CHAR" + _bind "$git_add_and_amend_commit_keys" " git add --all . && git commit --amend -C HEAD""$RETURN_CHAR" else _bind "$git_commit_all_keys" "\" git_commit_all$RETURN_CHAR\"" _bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit $RETURN_CHAR\"" _bind "$git_commit_all_with_ci_skip_keys" "\"\C-A GIT_COMMIT_MSG_SUFFIX='[ci skip]' git_commit_all $RETURN_CHAR\"" + _bind "$git_add_and_amend_commit_keys" "\" git add --all . && git commit --amend -C HEAD$RETURN_CHAR\"" fi fi From d15ece1d998f850ffc16a2aa698bacd35397a913 Mon Sep 17 00:00:00 2001 From: "Tom \"Ravi\" Hale" Date: Tue, 9 Oct 2018 12:12:08 +0700 Subject: [PATCH 66/79] _scmb_git_branch_shortcuts: quote $@ inside ruby heredoc --- lib/git/branch_shortcuts.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git/branch_shortcuts.sh b/lib/git/branch_shortcuts.sh index 27b0ce8..dc21a57 100644 --- a/lib/git/branch_shortcuts.sh +++ b/lib/git/branch_shortcuts.sh @@ -20,9 +20,9 @@ function _scmb_git_branch_shortcuts { return 1 fi - # Use ruby to inject numbers into ls output + # Use ruby to inject numbers into git branch output ruby -e "$( cat < 9 && i < 9 ? " " : " ") From 888a53f1bae2abc7325d9b81229bdaa504dbdd8d Mon Sep 17 00:00:00 2001 From: Willa Drengwitz Date: Tue, 16 Oct 2018 18:56:05 -0400 Subject: [PATCH 67/79] Add apkg repository update before install --- test/support/travisci_deps.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/support/travisci_deps.sh b/test/support/travisci_deps.sh index 83cc32e..f636bcf 100755 --- a/test/support/travisci_deps.sh +++ b/test/support/travisci_deps.sh @@ -23,10 +23,10 @@ if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then if [[ -z "$TEST_SHELLS" ]]; then needs_zsh=true; fi # finally, we install zsh if needed! - if $needs_zsh ; then + if $needs_zsh; then + sudo apt-get update sudo apt-get install zsh else echo "No deps required." fi - fi From 5bc863284bb1396ba974fa04760271292e6f3663 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Tue, 18 Dec 2018 21:27:04 +0700 Subject: [PATCH 68/79] Change maxdepth to 3 in _find_git_repos (c --rebuild was taking too long) --- lib/git/repo_index.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index 9edcd24..4ec8dd1 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -126,7 +126,7 @@ _git_index_dirs_without_home() { function _find_git_repos() { # Find all unarchived projects local IFS=$'\n' - for repo in $(find -L "$GIT_REPO_DIR" -maxdepth 5 -name ".git" -type d \! -wholename '*/archive/*'); do + for repo in $(find -L "$GIT_REPO_DIR" -maxdepth 3 -name ".git" -type d \! -wholename '*/archive/*'); do echo ${repo%/.git} # Return project folder, with trailing ':' _find_git_submodules $repo # Detect any submodules done From dec2fff0bf4a5160837e6c1b92470336a60c2a98 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 19 Dec 2018 20:00:16 +0700 Subject: [PATCH 69/79] Support coreutils on Mac, which provides a greadlink command that supports the -f option (brew install coreutils) --- lib/git/shell_shortcuts.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 49d9b81..54f79ce 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -99,11 +99,13 @@ if ! ls --color=auto > /dev/null 2>&1; then _ls_bsd="BSD" fi -# Test if readlink supports -f option, otherwise use perl (a bit slower) -if ! readlink -f / > /dev/null 2>&1; then - _abs_path_command=(perl -e 'use Cwd abs_path; print abs_path(shift)') -else +# Test if readlink supports -f option, test for greadlink on Mac, then fallback to perl +if \readlink -f / > /dev/null 2>&1; then _abs_path_command=(readlink -f) +elif greadlink -f / > /dev/null 2>&1; then + _abs_path_command=(greadlink -f) +else + _abs_path_command=(perl -e 'use Cwd abs_path; print abs_path(shift)') fi # Function wrapper around 'll' From 24f96762b15dcac11a550fa356acaa2d0016b808 Mon Sep 17 00:00:00 2001 From: Vishal Kotcherlakota Date: Tue, 26 Feb 2019 11:05:47 -0800 Subject: [PATCH 70/79] Update README.md It took me a minute to figure out how to use the git_index feature. Hopefully this documentation update helps the next person! --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60ed7e8..87eccdb 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,9 @@ doesn't need to 'learn' anything, and it can do SCM-specific stuff like: The default alias for `git_index` is 'c', which might stand for 'code' -You will first need to configure your repository directory, and then build the -index: +You will first need to configure your repository directory by setting `GIT_REPO_DIR` in `~/.git.sbmrc`. + +Then, build the index: ```bash $ c --rebuild From cebddb8abada63804b44d77dd62fe1cbfa633168 Mon Sep 17 00:00:00 2001 From: Cory Thomas Date: Wed, 15 May 2019 16:25:14 -0500 Subject: [PATCH 71/79] Fix infinite loop when parent directory does not exist resolves #241 --- lib/git/fallback/status_shortcuts_shell.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/git/fallback/status_shortcuts_shell.sh b/lib/git/fallback/status_shortcuts_shell.sh index 406da56..afc0e67 100755 --- a/lib/git/fallback/status_shortcuts_shell.sh +++ b/lib/git/fallback/status_shortcuts_shell.sh @@ -126,9 +126,10 @@ _gs_output_file_group() { if [ -z "$project_root" ]; then relative="${stat_file[$i]}" else - dest=$(readlink -f "$project_root/${stat_file[$i]}") + absolute="$project_root/${stat_file[$i]}" + dest=$(readlink -f "$absolute") local pwd=$(readlink -f "$PWD") - relative="$(_gs_relative_path "$pwd" "$dest" )" + relative="$(_gs_relative_path "$pwd" "${dest:-$absolute}" )" fi if [[ $f -gt 10 && $e -lt 10 ]]; then local pad=" "; else local pad=""; fi # (padding) @@ -149,7 +150,7 @@ _gs_relative_path(){ # Credit to 'pini' for the following script. # (http://stackoverflow.com/questions/2564634/bash-convert-absolute-path-into-relative-path-given-a-current-directory) target=$2; common_part=$1; back="" - while [[ "${target#$common_part}" == "${target}" ]]; do + while [[ -n "${common_part}" && "${target#$common_part}" == "${target}" ]]; do common_part="${common_part%/*}" back="../${back}" done From 3c97507b3ed5101c51ac43eab19d3fee9550a1b0 Mon Sep 17 00:00:00 2001 From: Cory Thomas Date: Wed, 15 May 2019 16:33:10 -0500 Subject: [PATCH 72/79] Make variables local instead of global --- lib/git/fallback/status_shortcuts_shell.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/git/fallback/status_shortcuts_shell.sh b/lib/git/fallback/status_shortcuts_shell.sh index afc0e67..0a10387 100755 --- a/lib/git/fallback/status_shortcuts_shell.sh +++ b/lib/git/fallback/status_shortcuts_shell.sh @@ -118,6 +118,8 @@ git_status_shortcuts() { } # Template function for 'git_status_shortcuts'. _gs_output_file_group() { + local relative + for i in ${stat_grp[$1]}; do # Print colored hashes & files based on modification groups local c_group="\033[0;$(eval echo -e \$c_grp_$1)" @@ -126,8 +128,8 @@ _gs_output_file_group() { if [ -z "$project_root" ]; then relative="${stat_file[$i]}" else - absolute="$project_root/${stat_file[$i]}" - dest=$(readlink -f "$absolute") + local absolute="$project_root/${stat_file[$i]}" + local dest=$(readlink -f "$absolute") local pwd=$(readlink -f "$PWD") relative="$(_gs_relative_path "$pwd" "${dest:-$absolute}" )" fi From 2c3b15e13c2cd8a07c2c7e1f14121fa9a0607769 Mon Sep 17 00:00:00 2001 From: Josh Hagins Date: Wed, 28 Aug 2019 15:01:01 -0400 Subject: [PATCH 73/79] Wrap more common shell commands by default --- git.scmbrc.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git.scmbrc.example b/git.scmbrc.example index 287bfaa..6e8e00d 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -124,6 +124,6 @@ git_add_and_amend_commit_keys="\C-xz" # CTRL+x, z # Expand numbered args for common shell commands shell_command_wrapping_enabled="true" # Here you can tweak the list of wrapped commands. -scmb_wrapped_shell_commands="vim emacs gedit cat rm cp mv ln cd" +scmb_wrapped_shell_commands="vim emacs gedit cat rm cp mv ln cd ls less subl code" # Add numbered shortcuts to output of ls -l, just like 'git status' shell_ls_aliases_enabled="true" From 689c8e29e4a83d0a6c399f5a6c7552aa44d76663 Mon Sep 17 00:00:00 2001 From: Lotte Steenbrink Date: Tue, 15 Oct 2019 21:26:33 +0200 Subject: [PATCH 74/79] README.md: add remark that some commands don't work w/o ruby --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87eccdb..27281f0 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ to your `.bashrc` or `.zshrc`: `[ -s "$HOME/.scm_breeze/scm_breeze.sh" ] && source "$HOME/.scm_breeze/scm_breeze.sh"` -**Note:** SCM Breeze performs much faster if you have ruby installed. +**Note:** You need to install ruby for some SCM Breeze commands to work. This also improves performance. See [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/) for installation information. ### File Shortcuts From 1a7d86fdbc6848811c6f4e77e594cbe58d64060a Mon Sep 17 00:00:00 2001 From: Martin Rey Date: Mon, 9 Mar 2020 22:25:17 +0100 Subject: [PATCH 75/79] Use ZDOTDIR env variable for path to .zshrc .zshrc can be found at "~/.zshrc", when ZDOTDIR is not set. If ZDOTDIR is set, .zshrc is in "${ZDOTDIR}/.zshrc". Using parameter expansion, the hardcoded path can be replaced with "${ZDOTDIR:-$HOME}/.zshrc". That way, it'll use the value in ZDOTDIR when set and the value in HOME when not. This thus mirrors the usage of ZDOTDIR in zsh and makes the code more fexible. --- README.md | 6 +++--- install.sh | 20 +++++++++++++++++--- uninstall.sh | 15 +++++++++------ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 87eccdb..4c5fd30 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ features. ```bash git clone git://github.com/scmbreeze/scm_breeze.git ~/.scm_breeze ~/.scm_breeze/install.sh -source ~/.bashrc # or source ~/.zshrc +source ~/.bashrc # or source "${ZDOTDIR:-$HOME}/.zshrc" ``` The install script creates required default configs and adds the following line @@ -253,7 +253,7 @@ default base subdirectories are: Images, Backgrounds, Logos, Icons, Mockups, and Screenshots. After you have changed these settings, remember to run `source ~/.bashrc` or -`source ~/.zshrc`. +`source "${ZDOTDIR:-$HOME}/.zshrc"`. #### 2) Initialize design directories for your projects @@ -324,7 +324,7 @@ Each feature is modular, so you are free to ignore the parts you don't want to use. Just comment out the relevant line in `~/.scm_breeze/scm_breeze.sh`. **Note:** After changing any settings, you will need to run `source ~/.bashrc` -(or `source ~/.zshrc`) +(or `source "${ZDOTDIR:-$HOME}/.zshrc"`) I know we grow attached to the aliases we use every day, so I've made the alias system completely customizable. You have two options when it comes to aliases: diff --git a/install.sh b/install.sh index dd025ef..538db73 100755 --- a/install.sh +++ b/install.sh @@ -10,10 +10,10 @@ fi # This loads SCM Breeze into the shell session. exec_string="[ -s \"$HOME/.scm_breeze/scm_breeze.sh\" ] && source \"$HOME/.scm_breeze/scm_breeze.sh\"" -# Add line to bashrc, zshrc, and bash_profile if not already present. +# Add line to bashrc and bash_profile if not already present. added_to_profile=false already_present=false -for rc in bashrc zshrc bash_profile; do +for rc in bashrc bash_profile; do if [ -s "$HOME/.$rc" ]; then if grep -q "$exec_string" "$HOME/.$rc"; then printf "== Already installed in '~/.$rc'\n" @@ -26,13 +26,27 @@ for rc in bashrc zshrc bash_profile; do fi done +# Add line to .zshrc if not aleady present. +# When set, the ZDOTDIR environment variable states the directory zshrc is in. +# If not set, HOME environment variable is used as fallback. +if [ -s "${ZDOTDIR:-$HOME}/.zshrc" ]; then + if grep -q "$exec_string" "${ZDOTDIR:-$HOME}/.zshrc"; then + printf "== Already installed in '${ZDOTDIR:-$HOME}/.zshrc'\n" + already_present=true + else + printf "\n$exec_string\n" >> "${ZDOTDIR:-$HOME}/.zshrc" + printf "== Added SCM Breeze to '${ZDOTDIR:-$HOME}/.zshrc'\n" + already_present=true + fi +fi + # Load SCM Breeze update scripts source "$scmbDir/lib/scm_breeze.sh" # Create '~/.*.scmbrc' files from example files _create_or_patch_scmbrc if [ "$added_to_profile" = true ] || [ "$already_present" = true ]; then - echo "== SCM Breeze Installed! Run 'source ~/.bashrc || source ~/.bash_profile' or 'source ~/.zshrc'" + echo "== SCM Breeze Installed! Run 'source ~/.bashrc || source ~/.bash_profile' or 'source \"${ZDOTDIR:-$HOME}/.zshrc\"'" echo " to load SCM Breeze into your current shell." else echo "== Error:" diff --git a/uninstall.sh b/uninstall.sh index f91235a..a0b6ba8 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -7,9 +7,12 @@ if [[ $OSTYPE == "Darwin" ]]; then sed="sed -i ''" fi -for rc in bashrc zshrc; do - if [ -f "$HOME/.$rc" ]; then - $sed '/scm_breeze/d' "$HOME/.$rc" && - printf "Removed SCM Breeze from %s\n" "$HOME/.$rc" - fi -done +if [ -f "$HOME/.bashrc" ]; then + $sed '/scm_breeze/d' "$HOME/.bashrc" && + printf "Removed SCM Breeze from '%s'\n" "$HOME/.bashrc" +fi + +if [ -f "${ZDOTDIR:-$HOME}/.zshrc" ]; then + $sed '/scm_breeze/d' "${ZDOTDIR:-$HOME}/.zshrc" && + printf "Removed SCM Breeze from '%s'\n" "${ZDOTDIR:-$HOME}/.zshrc" +fi From 800b1b1e0aa375505b02eb0e71f6e7709f898df1 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Wed, 29 Jul 2020 17:48:53 -0400 Subject: [PATCH 76/79] Avoid running ll code if its not used Prior to this, if $shell_ls_aliases_enabled="false", this code would still run, but unnecessarily. --- lib/git/shell_shortcuts.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 54f79ce..43b7215 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -92,25 +92,25 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e fi -# BSD ls is different to Linux (GNU) ls -# Test for BSD ls -if ! ls --color=auto > /dev/null 2>&1; then - # ls is BSD - _ls_bsd="BSD" -fi - -# Test if readlink supports -f option, test for greadlink on Mac, then fallback to perl -if \readlink -f / > /dev/null 2>&1; then - _abs_path_command=(readlink -f) -elif greadlink -f / > /dev/null 2>&1; then - _abs_path_command=(greadlink -f) -else - _abs_path_command=(perl -e 'use Cwd abs_path; print abs_path(shift)') -fi - # Function wrapper around 'll' # Adds numbered shortcuts to output of ls -l, just like 'git status' if [ "$shell_ls_aliases_enabled" = "true" ] && builtin command -v ruby > /dev/null 2>&1; then + # BSD ls is different to Linux (GNU) ls + # Test for BSD ls + if ! ls --color=auto > /dev/null 2>&1; then + # ls is BSD + _ls_bsd="BSD" + fi + + # Test if readlink supports -f option, test for greadlink on Mac, then fallback to perl + if \readlink -f / > /dev/null 2>&1; then + _abs_path_command=(readlink -f) + elif greadlink -f / > /dev/null 2>&1; then + _abs_path_command=(greadlink -f) + else + _abs_path_command=(perl -e 'use Cwd abs_path; print abs_path(shift)') + fi + unalias ll > /dev/null 2>&1; unset -f ll > /dev/null 2>&1 function ls_with_file_shortcuts { local ll_output From 9187f1b04c86d128b72eb28033eee40939bd8427 Mon Sep 17 00:00:00 2001 From: Arthas Park Date: Sun, 6 Sep 2020 01:49:39 +0900 Subject: [PATCH 77/79] Add aliases for git switch (gsw) / git restore (grt) --- git.scmbrc.example | 2 ++ lib/git/aliases.sh | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/git.scmbrc.example b/git.scmbrc.example index 6e8e00d..bd32ef2 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -105,6 +105,8 @@ git_submodule_update_rec_alias="gsur" git_top_level_alias="gtop" git_whatchanged_alias="gwc" git_apply_alias="gapp" +git_switch_alias="gsw" +git_restore_alias="grt" # Hub aliases (https://github.com/github/hub) git_pull_request_alias="gpr" diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index b8184c3..4308337 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -28,9 +28,9 @@ if type hub > /dev/null 2>&1; then export _git_cmd="hub"; fi function git(){ # Only expand args for git commands that deal with paths or branches case $1 in - commit|blame|add|log|rebase|merge|difftool) + commit|blame|add|log|rebase|merge|difftool|switch) exec_scmb_expand_args "$_git_cmd" "$@";; - checkout|diff|rm|reset) + checkout|diff|rm|reset|restore) exec_scmb_expand_args --relative "$_git_cmd" "$@";; branch) _scmb_git_branch_shortcuts "${@:2}";; @@ -114,6 +114,7 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_add_updated_alias" 'git' 'add' '-u' __git_alias "$git_difftool_alias" 'git' 'difftool' __git_alias "$git_mergetool_alias" 'git' 'mergetool' + __git_alias "$git_restore_alias" 'git' 'restore' # Custom default format for git log git_log_command="log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" @@ -156,6 +157,7 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_submodule_update_rec_alias" 'git' 'submodule' 'update' '--init' '--recursive' __git_alias "$git_whatchanged_alias" 'git' 'whatchanged' __git_alias "$git_apply_alias" 'git' 'apply' + __git_alias "$git_switch_alias" 'git' 'switch' # Compound/complex commands _alias "$git_fetch_all_alias" 'git fetch --all' From ad6bfdda3d82ad5068f0cb192229e3eb1af20186 Mon Sep 17 00:00:00 2001 From: telzhov Date: Sat, 2 Jan 2021 23:33:39 +0300 Subject: [PATCH 78/79] Replaced calls to '_git' wrapper with its contents, '__git_wrap__git_main' Since '_git' has been removed in git 2.30 --- lib/git/aliases.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index 4308337..56c95be 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -57,7 +57,7 @@ let COMP_CWORD+=1 local cur words cword prev _get_comp_words_by_ref -n =: cur words cword prev -_git +__git_wrap__git_main } " } @@ -184,7 +184,7 @@ if [ $shell = "bash" ]; then [[ -s "/usr/share/git/completion/git-completion.bash" ]] && source "/usr/share/git/completion/git-completion.bash" # new path in Ubuntu 13.04 [[ -s "/usr/share/bash-completion/completions/git" ]] && source "/usr/share/bash-completion/completions/git" - complete -o default -o nospace -F _git $git_alias + complete -o default -o nospace -F __git_wrap__git_main $git_alias # Git repo management & aliases. # If you know how to rewrite _git_index_tab_completion() for zsh, please send me a pull request! From 27e702bb7ae37841f57b057d463352ba3ceb92d0 Mon Sep 17 00:00:00 2001 From: Yeonghoon Park Date: Wed, 14 Apr 2021 17:53:54 +0900 Subject: [PATCH 79/79] gs fixes --- lib/git/status_shortcuts.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/git/status_shortcuts.rb b/lib/git/status_shortcuts.rb index e121b8b..51611db 100644 --- a/lib/git/status_shortcuts.rb +++ b/lib/git/status_shortcuts.rb @@ -26,7 +26,7 @@ git_status_lines = @git_status.split("\n") git_branch = git_status_lines[0] -@branch = git_branch[/^## (?:Initial commit on )?([^ \.]+)/, 1] +@branch = git_branch[/^## (?:Initial commit on )?([^ ]+)/, 1] @ahead = git_branch[/\[ahead ?(\d+).*\]/, 1] @behind = git_branch[/\[.*behind ?(\d+)\]/, 1] @@ -76,16 +76,15 @@ difference = difference.length > 0 ? " #{@c[:dark]}| #{@c[:new]}#{difference}# # 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] +if @changes.size == 0 + puts "%s#%s On branch: %s#{@branch}#{difference}%s %s| \033[0;32mNo changes (working directory clean)%s" % [ + @c[:dark], @c[:rst], @c[:branch], @c[:rst], @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] +puts "%s#%s On branch: %s#{@branch}#{difference}%s %s| [%s*%s]%s => $#{ENV["git_env_char"]}*\n%s#%s" % [ + @c[:dark], @c[:rst], @c[:branch], @c[:rst], @c[:dark], @c[:rst], @c[:dark], @c[:rst], @c[:dark], @c[:rst] ] def has_modules?