diff --git a/.travis.yml b/.travis.yml
index 3d40388..a16628d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,18 @@
-script: ./run_tests.sh
+os:
+ - linux
+ - osx
+
+env:
+ - TEST_SHELLS=bash
+ - TEST_SHELLS=zsh
+
+sudo: required
+
+install:
+ - ./test/support/travisci_deps.sh
+
before_script:
- - sudo apt-get install zsh
+ - echo -e "test_repo_11\ntest_repo_1" | sort
+
+script:
+ - ./run_tests.sh
diff --git a/README.markdown b/README.md
similarity index 90%
rename from README.markdown
rename to README.md
index 936281e..e1512d5 100644
--- a/README.markdown
+++ b/README.md
@@ -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.
-
-
-
- [Installation](#installation)
- [Usage](#usage)
- [File Shortcuts](#file-shortcuts)
@@ -32,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
@@ -40,13 +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.
-
-
-## Usage
-
-
+**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
@@ -114,7 +105,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
@@ -190,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
@@ -261,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
@@ -332,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:
@@ -359,6 +351,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. You can set these in `~/.user_sym` and `~/.staff_sym`.
+
+
+
+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
@@ -406,3 +414,7 @@ SCMs.
***Enjoy!***
+## Alternative Projects
+
+1. https://github.com/shinriyo/breeze `fish` support
+1. https://github.com/mroth/scmpuff static go binary
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 0000000..b65e055
Binary files /dev/null and b/docs/images/custom_user_and_staff_symbols.jpg differ
diff --git a/git.scmbrc.example b/git.scmbrc.example
index 2728b5a..bd32ef2 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"
@@ -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"
@@ -68,7 +69,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"
@@ -89,7 +91,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)
@@ -103,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"
@@ -111,9 +115,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
@@ -121,6 +126,6 @@ git_commit_all_with_ci_skip_keys="\C-xv" # CTRL+x, v (Appends [ci skip] to c
# 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"
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/lib/design.sh b/lib/design.sh
index e0e2091..cd48cdb 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
@@ -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/aliases.sh b/lib/git/aliases.sh
index bef8f5e..4298051 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
@@ -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}";;
@@ -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
}
"
}
@@ -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[*]}"
@@ -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'
@@ -100,21 +101,23 @@ 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'
__git_alias "$git_difftool_meld_alias" 'git' 'difftool -y -t meld'
__git_alias "$git_difftool_vimdiff_alias" 'git' 'difftool -y -t vimdiff'
__git_alias "$git_difftool_gvimdiff_alias" 'git' 'difftool -y -t gvimdiff'
+ __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"
@@ -157,6 +160,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'
@@ -184,7 +188,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!
diff --git a/lib/git/branch_shortcuts.sh b/lib/git/branch_shortcuts.sh
index 61012e2..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 ? " " : " ")
@@ -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..0a10387 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,11 +114,12 @@ 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'.
_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)"
@@ -127,9 +128,10 @@ _gs_output_file_group() {
if [ -z "$project_root" ]; then
relative="${stat_file[$i]}"
else
- dest=$(readlink -f "$project_root/${stat_file[$i]}")
+ local absolute="$project_root/${stat_file[$i]}"
+ local 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)
@@ -150,7 +152,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
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/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/keybindings.sh b/lib/git/keybindings.sh
index 8ec4066..4bbca42 100644
--- a/lib/git/keybindings.sh
+++ b/lib/git/keybindings.sh
@@ -24,16 +24,24 @@ _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~ 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\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 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
diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh
index b197d49..4ec8dd1 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
@@ -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)" |
@@ -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'
- for repo in $(find -L "$GIT_REPO_DIR" -maxdepth 5 -name ".git" -type d \! -wholename '*/archive/*'); do
+ local IFS=$'\n'
+ 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
- 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 | cut -d " " -f2- >| "$GIT_REPO_DIR/.git_index"
- unset IFS
+ echo $(basename $repo | sed "s/ /_/g"):$repo
+ done | sort -t ":" -k1,1 | cut -d ":" -f2- >| "$GIT_REPO_DIR/.git_index"
if [ "$1" != "--silent" ]; then
echo -e "===== Indexed $_bld_col$(_git_index_count)$_txt_col repos in $GIT_REPO_DIR/.git_index"
@@ -205,39 +202,39 @@ _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
- 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
- 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
- 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
done
- unset IFS
# Update all remotes if there are any branches to update
if [ -n "${branches[*]}" ]; then git fetch --all 2> /dev/null; fi
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
@@ -268,11 +265,11 @@ 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"
- $@
+ "$@"
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.)"
@@ -285,8 +282,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 +309,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 6299537..43b7215 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)"
@@ -23,19 +23,19 @@ 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
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} \"\$@\"; }";;
@@ -92,61 +92,95 @@ 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, 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
- _abs_path_command="readlink -f"
-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
+ # 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
+ 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
# Ensure sh_word_split is on
- if setopt | grep -q shwordsplit; then SHWORDSPLIT_ON=true; fi
+ [[ -o shwordsplit ]] && SHWORDSPLIT_ON=true
setopt shwordsplit
fi
- # Parse path from args
- IFS=$'\n'
- for arg in $@; do
- if [ -d "$arg" ]; then local rel_path="${arg%/}"; fi
+ # 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 [[ -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
- unset IFS
+ 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
# 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
+ # TODO(ghthor): Convert this to a cat <&2
echo "$ll_output"
return 1
fi
@@ -169,23 +203,26 @@ 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
- 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
+ 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
- unset IFS
# 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
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?
diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh
index d63bb0c..54b3191 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"
@@ -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
@@ -80,10 +79,11 @@ 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
+ 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
+ if [[ $ga_auto_remove = yes && ! -e $file ]]; then
echo -n "# "
git rm "$file"
else
@@ -91,7 +91,6 @@ git_silent_add_shortcuts() {
echo -e "# Added '$file'"
fi
done
- unset IFS
echo "#"
fi
}
@@ -100,18 +99,18 @@ 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
+ 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"
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.
@@ -122,43 +121,52 @@ scmb_expand_args() {
shift
fi
- first=1
- OLDIFS="$IFS"; IFS=" " # We need to split on spaces to loop over expanded range
+ 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 [ "$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"
+
+ # "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() {
- if [ "$1" = 1 ]; then
- eval printf '%s' "\"\$$2\"" | sed -e "s%$(pwd)/%%" | awk '{printf("%s", $0)}'
- else
- 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
+ printf '%s' "$pathname"
}
# 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')"
+ local args
+ eval "args=$(scmb_expand_args "$@")" # populate $args array
+ _safe_eval "${args[@]}"
}
# Clear numbered env variables
@@ -180,13 +188,13 @@ 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
+ 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"
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
}
@@ -203,29 +211,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 +275,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/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
diff --git a/lib/git/tools.sh b/lib/git/tools.sh
index 0c62e72..c158a77 100644
--- a/lib/git/tools.sh
+++ b/lib/git/tools.sh
@@ -23,8 +23,9 @@ 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
+ 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
rm -rf .git/refs/original/ && $_git_cmd reflog expire --all && $_git_cmd gc --aggressive --prune
}
@@ -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 2c692bb..2744b00 100644
--- a/lib/scm_breeze.sh
+++ b/lib/scm_breeze.sh
@@ -18,6 +18,48 @@ _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
+ quoted=() # Assign separately for zsh 5.0.2 of Ubuntu 14.04
+ 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() {
+ 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(){
if [ $shell = "zsh" ]; then
builtin type -p "$1" | sed "s/$1 is //" | head -1
diff --git a/run_tests.sh b/run_tests.sh
index 9aebd4c..c0a3149 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -3,8 +3,16 @@
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}"
+
+cd -P -- "${0%/*}" # Change to directory this script lives in
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
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"
diff --git a/test/lib/git/repo_index_test.sh b/test/lib/git/repo_index_test.sh
index f7006ed..eb98334 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=":"
+ local IFS=":"
for dir in $GIT_REPOS; do
- mkdir -p $dir; cd $dir; git init;
+ mkdir -p $dir
+ cd $dir
+ git init
done
- unset IFS
verboseGitCommands
@@ -72,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() {
@@ -85,30 +94,32 @@ 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)" "bitbucket.*\
-blue_submodule.*\
-github.*\
-green_submodule.*\
-red_submodule.*\
-source_forge.*\
-submodules_everywhere.*\
-test_repo_11.*\
-test_repo_1"
-
+ assertIncludes "$(index_no_newlines)" $(
+ cat <> $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"
@@ -125,7 +136,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"
}
@@ -133,16 +144,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_11" "$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() {
@@ -163,16 +184,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
@@ -180,7 +200,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"
@@ -188,8 +207,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"
-
diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh
index b4664f3..6e470a7 100755
--- a/test/lib/git/shell_shortcuts_test.sh
+++ b/test/lib/git/shell_shortcuts_test.sh
@@ -31,12 +31,16 @@ 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
+ 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
- export mv_path="$(which mv)"
- export rm_path="$(which rm)"
- export sed_path="$(which sed)"
- export cat_pathj="$(which cat)"
alias mv="nocorrect $mv_path"
alias rm="$rm_path --option"
alias sed="$sed_path"
@@ -58,16 +62,25 @@ assertAliasEquals(){
}
+#-----------------------------------------------------------------------------
+# Setup and tear down
+#-----------------------------------------------------------------------------
+
+setUp() {
+ unset QUOTING_STYLE # Use default quoting style for ls
+}
+
+
#-----------------------------------------------------------------------------
# Unit tests
#-----------------------------------------------------------------------------
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"
+ assertAliasEquals "exec_scmb_expand_args builtin cd" "cd"
assertIncludes "$(declare -f ln)" "ln ()"
assertIncludes "$(declare -f ln)" "exec_scmb_expand_args __original_ln"
}
@@ -76,18 +89,26 @@ test_ls_with_file_shortcuts() {
export git_env_char="e"
TEST_DIR=$(mktemp -d -t scm_breeze.XXXXXXXXXX)
- cd $TEST_DIR
+
+ # 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"
+ 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)
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 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
@@ -106,10 +127,24 @@ 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"
}
# load and run shUnit2
diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh
index 71eb5d5..eb3acae 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() {
@@ -48,21 +49,72 @@ 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 args="$(scmb_expand_args 1 3 6)"; token_quote "${args[@]}")"
+ assertEquals "$error" 'one two three five' \
+ "$(eval args="$(scmb_expand_args 1-3 5)"; token_quote "${args[@]}")"
+ assertEquals "$error" '\$dollar two three four one' \
+ "$(eval args="$(scmb_expand_args 7 2-4 1)"; token_quote "${args[@]}")"
# 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 args="$(scmb_expand_args -m "Test Commit Message" 1)"; token_quote "${args[@]}")"
+ assertEquals "$error" '-ma Test\ Commit\ Message Unquoted'\
+ "$(eval args="$(scmb_expand_args -ma "Test Commit Message" "Unquoted")"; token_quote "${args[@]}")"
+ assertEquals "$error" '\$dollar one two\ words' \
+ "$(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
+ # 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' \
+ "$(eval args="$(scmb_expand_args foo 'bar baz')"; token_quote "${args[@]}")"
+ assertEquals "variables with spaces not preserved" 'one a\ b\ c' \
+ "$(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 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
+ # 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() {
@@ -259,9 +311,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\""
}
@@ -283,7 +336,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\]"
diff --git a/test/lib/scm_breeze_test.sh b/test/lib/scm_breeze_test.sh
new file mode 100755
index 0000000..5a5a8da
--- /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/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
diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh
index 7407b59..67a6155 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
#
@@ -15,7 +15,8 @@ fi
# Strip color codes from a string
strip_colors() {
- sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g"
+ # Updated with info from: https://superuser.com/a/380778
+ perl -pe 's/\x1b\[[0-9;]*[mG]//g'
}
# Print space separated tab completion options
@@ -25,25 +26,32 @@ tab_completions(){ echo "${COMPREPLY[@]}"; }
silentGitCommands() {
git() { /usr/bin/env git "$@" > /dev/null 2>&1; }
}
+
# Cancel silent git commands
verboseGitCommands() {
unset -f git
}
+
# Asserts
#-----------------------------------------------------------------------------
+# Return 0 (shell's true) if "$1" contains string "$2"
_includes() {
- if [ -n "$3" ]; then regex="$3"; else regex=P; fi
- if echo "$1" | grep -q$regex "$2"; then echo 0; else echo 1; fi
+ if [ -n "$3" ]; then regex="$3"; else regex=''; 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 "$@")
-}
-# assert $1 does not contain $2
-assertNotIncludes() {
- assertFalse "'$1' should not have contained '$2'" $(_includes "$@")
+ _includes "$@"
+ local grep_exit=$?
+ assertTrue "'$1' should have contained '$2'" '[[ $grep_exit == 0 ]]'
}
+# assert $1 does not contain $2
+assertNotIncludes() {
+ _includes "$@"
+ local grep_return=$?
+ assertTrue "'$1' should not have contained '$2'" '[[ ! $grep_exit = 0 ]]'
+}
diff --git a/test/support/travisci_deps.sh b/test/support/travisci_deps.sh
new file mode 100755
index 0000000..f636bcf
--- /dev/null
+++ b/test/support/travisci_deps.sh
@@ -0,0 +1,32 @@
+#!/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.
+if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then
+
+ # 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 update
+ sudo apt-get install zsh
+ else
+ echo "No deps required."
+ fi
+fi
diff --git a/uninstall.sh b/uninstall.sh
index 0805a6e..a0b6ba8 100755
--- a/uninstall.sh
+++ b/uninstall.sh
@@ -1,7 +1,18 @@
#!/bin/sh
# uninstall by (github: bernardofire)
# Remove line from bashrc and zshrc if present.
-for rc in bashrc zshrc; do
- sed -i '/scm_breeze/d' "$HOME/.$rc"
- printf "Removed SCM Breeze from %s\n" "$HOME/.$rc"
-done
+
+sed="sed -i"
+if [[ $OSTYPE == "Darwin" ]]; then
+ sed="sed -i ''"
+fi
+
+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