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. -![SCM Breeze Example Gif](http://i.imgur.com/3fD8cpo.gif) - - - [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`. + +Custom user and staff emojis + +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