diff --git a/README.markdown b/README.markdown index ff430f4..88b9695 100644 --- a/README.markdown +++ b/README.markdown @@ -1,12 +1,9 @@ # SCM Breeze [![TravisCI](https://secure.travis-ci.org/ndbroadbent/scm_breeze.png?branch=master)](http://travis-ci.org/ndbroadbent/scm_breeze) ### Streamline your SCM workflow. -**SCM Breeze** is a set of shell scripts (for `bash` and `zsh`) that enhance your interaction with tools -such as git. It integrates with your shell to give you numbered file shortcuts, +**SCM Breeze** is a set of shell scripts (for `bash` and `zsh`) that enhance 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. -Note: **git** is currently the only supported SCM. I've kept the project's name generic, but other SCMs aren't supported yet. -
## Demo video @@ -29,7 +26,7 @@ You can configure the variable prefix, which is 'e' by default.
-### UPDATE: Now with 'ls' shortcuts: +### 'ls' shortcuts:
Ls With Shortcuts @@ -286,6 +283,12 @@ The install script just adds the following line to your `.bashrc` or `.zshrc`: `[ -s "$HOME/.scm_breeze/scm_breeze.sh" ] && source "$HOME/.scm_breeze/scm_breeze.sh"` + +# Updating + +Please run `update_scm_breeze` to fetch the latest code. This will update SCM Breeze from Github, +and will create or patch your `~/.*.scmbrc` config files if any new settings are added. + # Uninstall ```bash @@ -338,13 +341,6 @@ You will need to set that up yourself. You just need to set the option: `setopt no_complete_aliases` (oh-my-zsh sets this by default). Zsh will then expand aliases like `gb` to `git branch`, and use the completion for that. - -# Updating - -Run `update_scm_breeze`. This will update SCM Breeze from Github, -and will create or patch your `~/.*.scmbrc` files if any new settings are added. - - # Contributing SCM Breeze lives on Github at [https://github.com/ndbroadbent/scm_breeze](https://github.com/ndbroadbent/scm_breeze) diff --git a/git.scmbrc.example b/git.scmbrc.example index 08f685b..2191309 100644 --- a/git.scmbrc.example +++ b/git.scmbrc.example @@ -47,6 +47,7 @@ git_reset_hard_alias="grsh" git_rm_alias="grm" git_blame_alias="gbl" git_diff_alias="gd" +git_diff_word_alias="gdw" git_diff_cached_alias="gdc" # 3. Standard commands git_clone_alias="gcl" @@ -55,6 +56,7 @@ git_fetch_all_alias="gfa" git_fetch_and_rebase_alias="gfr" git_pull_alias="gpl" git_push_alias="gps" +git_push_force_alias="gpsf" git_pull_then_push_alias="gpls" git_status_original_alias="gst" git_status_short_alias="gss" @@ -70,6 +72,7 @@ git_branch_alias="gb" git_branch_all_alias="gba" git_branch_move_alias="gbm" git_branch_delete_alias="gbd" +git_branch_delete_force_alias="gbD" git_rebase_alias="grb" git_rebase_interactive_alias="grbi" git_rebase_alias_continue="grbc" @@ -81,14 +84,16 @@ git_log_stat_alias="gls" git_log_graph_alias="glg" git_show_alias="gsh" git_show_summary="gsm" # (gss taken by git status short) +git_tag_alias="gt" # Git Keyboard Shortcuts # ---------------------------------------------- # 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_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) # Shell Command Wrapping diff --git a/lib/git/aliases.sh b/lib/git/aliases.sh index f27eadd..65bfba0 100644 --- a/lib/git/aliases.sh +++ b/lib/git/aliases.sh @@ -24,9 +24,9 @@ if type hub > /dev/null 2>&1; then export _git_cmd="hub"; fi # Create 'git' function that calls hub if defined, and expands all numeric arguments function git(){ - # Only expand args for a subset of git commands + # Only expand args for git commands that deal with paths or branches case $1 in - checkout|commit|reset|rm|blame|diff|add|log) + checkout|commit|rm|blame|diff|add|log|rebase) exec_scmb_expand_args "$_git_cmd" "$@";; branch) _scmb_git_branch_shortcuts "${@:2}";; @@ -94,6 +94,7 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_rm_alias" "git" "rm" __git_alias "$git_blame_alias" "git" "blame" __git_alias "$git_diff_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_add_patch_alias" "git" "add" "-p" # Custom default format for git log @@ -106,6 +107,7 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_checkout_branch_alias" "git" 'checkout' "-b" __git_alias "$git_pull_alias" "git" 'pull' __git_alias "$git_push_alias" "git" 'push' + __git_alias "$git_push_force_alias" "git" 'push' '-f' __git_alias "$git_status_original_alias" "git" 'status' # (Standard git status) __git_alias "$git_status_short_alias" "git" 'status' '-s' __git_alias "$git_clean_alias" "git" "clean" @@ -119,6 +121,7 @@ if [ "$git_setup_aliases" = "yes" ]; then __git_alias "$git_cherry_pick_alias" "git" 'cherry-pick' __git_alias "$git_show_alias" "git" 'show' __git_alias "$git_show_summary" "git" 'show' '--summary' + __git_alias "$git_tag_alias" "git" 'tag' # Compound/complex commands diff --git a/lib/git/branch_shortcuts.sh b/lib/git/branch_shortcuts.sh index c57e646..e7b9064 100644 --- a/lib/git/branch_shortcuts.sh +++ b/lib/git/branch_shortcuts.sh @@ -39,10 +39,11 @@ EOF done } -alias "$git_branch_alias"="exec_scmb_expand_args _scmb_git_branch_shortcuts" -alias "$git_branch_all_alias"="exec_scmb_expand_args _scmb_git_branch_shortcuts -a" -alias "$git_branch_move_alias"="exec_scmb_expand_args _scmb_git_branch_shortcuts -m" -alias "$git_branch_delete_alias"="exec_scmb_expand_args _scmb_git_branch_shortcuts -D" +__git_alias "$git_branch_alias" "_scmb_git_branch_shortcuts" +__git_alias "$git_branch_all_alias" "_scmb_git_branch_shortcuts" "-a" +__git_alias "$git_branch_move_alias" "_scmb_git_branch_shortcuts" "-m" +__git_alias "$git_branch_delete_alias" "_scmb_git_branch_shortcuts" "-d" +__git_alias "$git_branch_delete_force_alias" "_scmb_git_branch_shortcuts" "-D" # Define completions for git branch shortcuts if [ "$shell" = "bash" ]; then @@ -50,4 +51,4 @@ if [ "$shell" = "bash" ]; then __define_git_completion $alias_str branch complete -o default -o nospace -F _git_"$alias_str"_shortcut $alias_str done -fi \ No newline at end of file +fi diff --git a/lib/git/keybindings.sh b/lib/git/keybindings.sh index d077353..4ea46c3 100644 --- a/lib/git/keybindings.sh +++ b/lib/git/keybindings.sh @@ -27,11 +27,13 @@ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then # Uses emacs style keybindings, so vi mode is not supported for now if ! set -o | grep -q '^vi .*on$'; then if [[ $shell == "zsh" ]]; then - _bind "$git_commit_all_keys" " git_commit_all""\n" - _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""\n" + _bind "$git_commit_all_keys" " git_commit_all""\n" + _bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""\n" + _bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""\n" else - _bind "$git_commit_all_keys" "\" git_commit_all\n\"" - _bind "$git_add_and_commit_keys" "\"\033[1~ git_add_and_commit \n\"" + _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\"" fi fi diff --git a/lib/git/repo_index.sh b/lib/git/repo_index.sh index d4d10ab..e7920a1 100644 --- a/lib/git/repo_index.sh +++ b/lib/git/repo_index.sh @@ -217,10 +217,10 @@ _git_index_update_all_branches() { # Ignore branch if remote and merge is not configured if [[ -n "$remote" ]] && [[ -n "$merge" ]]; then - branches=("${branches[@]}" "$branch") - remotes=("${remotes[@]}" "$remote") + branches=(${branches[@]} "$branch") + remotes=(${remotes[@]} "$remote") # Get branch from merge ref (refs/heads/master => master) - merges=("${merges[@]}" "$(basename $merge)") + merges=(${merges[@]} "$(basename $merge)") else echo "=== Skipping $branch: remote and merge refs are not configured." fi diff --git a/lib/git/shell_shortcuts.sh b/lib/git/shell_shortcuts.sh index 532eb61..690bf93 100644 --- a/lib/git/shell_shortcuts.sh +++ b/lib/git/shell_shortcuts.sh @@ -11,11 +11,20 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e # Do it in a function so we don't bleed variables function _git_wrap_commands() { # Define 'whence' for bash, to get the value of an alias - type whence > /dev/null 2>&1 || function whence() { type "$@" | sed -e "s/.*is aliased to \`//" -e "s/'$//"; } + type whence > /dev/null 2>&1 || function whence() { type "$@" | sed -E -e "s/.*is aliased to \`//" -e "s/'$//"; } local cmd='' for cmd in $(echo $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" + continue + fi + fi + case "$(type $cmd 2>&1)" in # Don't do anything if command already aliased, or not found. @@ -25,34 +34,34 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e *'not found'*) if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: $cmd not found!"; fi;; - *'is aliased to'*|*'is an alias for'*) + *'aliased to'*|*'is an alias for'*) if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: $cmd is an alias"; fi # Store original alias local original_alias="$(whence $cmd)" - # Remove alias, so that which can return binary + # Remove alias, so that we can find binary unalias $cmd # Detect original $cmd type, and escape case "$(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 'which' - *) local escaped_cmd="$(\which $cmd)";; + # Get full path for files with 'find_binary' function + *) local escaped_cmd="$(find_binary $cmd)";; esac # Expand original command into full path, to avoid infinite loops - local expanded_alias="$(echo $original_alias | sed "s%\(^\| \)$cmd\($\| \)%\\1$escaped_cmd\\2%")" + local expanded_alias="$(echo $original_alias | sed -E "s%(^| )$cmd($| )%\\1$escaped_cmd\\2%")" # Wrap previous alias with escaped command alias $cmd="exec_scmb_expand_args $expanded_alias";; *'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 "s/^$cmd ()/__original_$cmd ()/")" + eval "$(declare -f $cmd | sed -E "s/^$cmd \(\)/__original_$cmd ()/")" # Remove function unset -f $cmd - # Create wrapped alias for old function - alias "$cmd"="exec_scmb_expand_args __original_$cmd";; + # Create function that wraps old function + eval "${cmd}(){ exec_scmb_expand_args __original_${cmd} \"\$@\"; }";; *'is a shell builtin'*) if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: $cmd is a shell builtin"; fi @@ -62,8 +71,8 @@ if [ "$shell_command_wrapping_enabled" = "true" ] || [ "$bash_command_wrapping_e *) if [ "${scmbDebug:-}" = "true" ]; then echo "SCMB: $cmd is an executable file"; fi # Otherwise, command is a regular script or binary, - # and the full path can be found from 'which' - alias $cmd="exec_scmb_expand_args $(\which $cmd)";; + # and the full path can be found with 'find_binary' function + alias $cmd="exec_scmb_expand_args $(find_binary $cmd)";; esac done # Clean up @@ -122,7 +131,7 @@ function ls_with_file_shortcuts { puts o.lines.map{|l|l.sub(re){|m|\"%s%-#{u}s %-#{g}s%#{s}s \"%[\$1,*\$3.split]}}" } - ll_output=$(echo "$ll_output" | \sed "s/ $USER/ $(/bin/cat $HOME/.user_sym)/g" | rejustify_ls_columns) + ll_output=$(echo "$ll_output" | \sed -E "s/ $USER/ $(/bin/cat $HOME/.user_sym)/g" | rejustify_ls_columns) fi if [ "$(echo "$ll_output" | wc -l)" -gt "50" ]; then diff --git a/lib/git/status_shortcuts.sh b/lib/git/status_shortcuts.sh index 2b6bde6..c03485d 100644 --- a/lib/git/status_shortcuts.sh +++ b/lib/git/status_shortcuts.sh @@ -121,7 +121,11 @@ scmb_expand_args() { for arg in "$@"; do if [[ "$arg" =~ ^[0-9]+$ ]] ; then # Substitute $e{*} variables for any integers if [ "$first" -eq 1 ]; then first=0; else printf '\t'; fi - eval printf '%s' "\"\$$git_env_char$arg\"" + if [ -e "$arg" ]; then + printf '%s' "$arg" + else + eval printf '%s' "\"\$$git_env_char$arg\"" + fi elif [[ "$arg" =~ ^[0-9]+-[0-9]+$ ]]; then # Expand ranges into $e{*} variables for i in $(eval echo {${arg/-/..}}); do @@ -185,6 +189,8 @@ git_commit_prompt() { 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" @@ -206,7 +212,10 @@ git_commit_all() { fail_if_not_git_repo || return 1 changes=$(git status --porcelain | wc -l) if [ "$changes" -gt 0 ]; then - echo -e "\033[0;33mCommitting all files (\033[0;31m$changes\033[0;33m)\033[0m" + if [ -n "$APPEND" ]; then + local appending=" | \033[0;36mappending '\033[1;36m$APPEND\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 -A" else echo "# No changed files to commit." @@ -225,4 +234,3 @@ git_add_and_commit() { echo "# No staged changes to commit." fi } - diff --git a/lib/git/tools.sh b/lib/git/tools.sh index 8493517..0c62e72 100644 --- a/lib/git/tools.sh +++ b/lib/git/tools.sh @@ -50,7 +50,7 @@ __git_ignore() { } # Always expand args git_ignore() { - exec_scmb_expand_args __git_ignore $@ + exec_scmb_expand_args __git_ignore "$@" } # Add one git ignore rule, just for your machine @@ -61,10 +61,10 @@ git_exclude() { # Exclude basename of file __git_exclude_basename() { - __git_ignore $(basename "$1") ".git/info/exclude" + __git_ignore "$(basename "$1")" ".git/info/exclude" } git_exclude_basename() { - exec_scmb_expand_args __git_exclude_basename $@ + exec_scmb_expand_args __git_exclude_basename "$@" } @@ -129,11 +129,14 @@ fi # Delete a git branch from local, cached remote and remote server git_branch_delete_all() { if [ -z "$1" ]; then - echo "Usage: git_branch_delete_all branch" + echo "Usage: git_branch_delete_all branch (-f forces deletion of unmerged branches.)" return fi - $_git_cmd branch -D $1 - $_git_cmd branch -D -r origin/$1 + local opt="-d" + if [ "$2" = '-f' ] || [ "$2" = '--force' ]; then opt="-D"; fi + + $_git_cmd branch $opt $1 + $_git_cmd branch $opt -r origin/$1 $_git_cmd push origin :$1 } diff --git a/lib/scm_breeze.sh b/lib/scm_breeze.sh index 0dc30ce..7d84148 100644 --- a/lib/scm_breeze.sh +++ b/lib/scm_breeze.sh @@ -12,11 +12,15 @@ disable_nullglob() { if [ $shell = "zsh" ]; then unsetopt NULL_GLOB; else shopt # Alias wrapper that ignores errors if alias is not defined. _alias(){ alias "$@" 2> /dev/null; } -if [ $shell = "zsh" ]; then - export GIT_BINARY=$(type -p git | sed 's/git is //' | head -1) -else - export GIT_BINARY=$(type -P git) -fi +find_binary(){ + if [ $shell = "zsh" ]; then + builtin type -p "$1" | sed "s/$1 is //" | head -1 + else + builtin type -P "$1" + fi +} + +export GIT_BINARY=$(find_binary git) # Updates SCM Breeze from GitHub. update_scm_breeze() { diff --git a/test/lib/git/shell_shortcuts_test.sh b/test/lib/git/shell_shortcuts_test.sh index e5c860e..3a6843a 100755 --- a/test/lib/git/shell_shortcuts_test.sh +++ b/test/lib/git/shell_shortcuts_test.sh @@ -17,8 +17,9 @@ else shopt -s expand_aliases fi -# Load test helpers +# Load test helpers and core functions source "$scmbDir/test/support/test_helper.sh" +source "$scmbDir/lib/scm_breeze.sh" # Setup #----------------------------------------------------------------------------- @@ -26,6 +27,8 @@ oneTimeSetUp() { export shell_command_wrapping_enabled="true" export scmb_wrapped_shell_commands="not_found cat rm cp mv ln cd sed" + alias rvm="test" # Ensure tests run if RVM isn't loaded but $HOME/.rvm is present + # Test functions function ln() { ln $@; } # Test aliases @@ -60,7 +63,8 @@ test_shell_command_wrapping() { assertAliasEquals "exec_scmb_expand_args /bin/sed" "sed" assertAliasEquals "exec_scmb_expand_args /bin/cat" "cat" assertAliasEquals "exec_scmb_expand_args builtin cd" "cd" - assertAliasEquals "exec_scmb_expand_args __original_ln" "ln" + assertIncludes "$(declare -f ln)" "ln ()" + assertIncludes "$(declare -f ln)" "exec_scmb_expand_args __original_ln" } test_ls_with_file_shortcuts() { diff --git a/test/lib/git/status_shortcuts_test.sh b/test/lib/git/status_shortcuts_test.sh index 626873d..aa86d2e 100755 --- a/test/lib/git/status_shortcuts_test.sh +++ b/test/lib/git/status_shortcuts_test.sh @@ -234,9 +234,9 @@ test_git_commit_prompt() { commit_msg="\"Nathan's git commit prompt function!\"" dbl_escaped_msg="\\\\\"Nathan's git commit prompt function\"'"'!'"'\"\\\\\"" # Create temporary history file - HISTFILE=$(mktemp -t scm_breeze.XXXXXXXXXX) - HISTFILESIZE=1000 - HISTSIZE=1000 + export HISTFILE=$(mktemp -t scm_breeze.XXXXXXXXXX) + export HISTFILESIZE=1000 + export HISTSIZE=1000 touch a b c d git add . > /dev/null @@ -252,12 +252,48 @@ test_git_commit_prompt() { assertIncludes "$git_show_output" "$commit_msg" # Test that history was appended correctly. - if [[ $shell != "zsh" ]]; then history -n; fi # Reload bash history - test_history="$(history)" + if [[ $shell == "zsh" ]]; then + test_history="$(history)" + else + test_history="$(cat $HISTFILE)" + fi assertIncludes "$test_history" "$commit_msg" assertIncludes "$test_history" "git commit -m \"$dbl_escaped_msg\"" } +test_git_commit_prompt_with_append() { + setupTestRepo + + commit_msg="Updating README, no build please" + + # Create temporary history file + HISTFILE=$(mktemp -t scm_breeze.XXXXXXXXXX) + HISTFILESIZE=1000 + HISTSIZE=1000 + + touch a b c + git add . > /dev/null + + # Zsh 'vared' doesn't handle input via pipe, so replace with function that reads into commit_msg variable. + function vared(){ read commit_msg; } + + # 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 + + git_show_output=$(git show --oneline --name-only) + assertIncludes "$git_show_output" "$commit_msg \[ci skip\]" + + # Test that history was appended correctly. + if [[ $shell == "zsh" ]]; then + test_history="$(history)" + else + test_history="$(cat $HISTFILE)" + fi + assertIncludes "$test_history" "$commit_msg \[ci skip\]" + assertIncludes "$test_history" "git commit -m \"$commit_msg \[ci skip\]\"" +} + test_adding_files_with_spaces() { setupTestRepo diff --git a/test/support/test_helper.sh b/test/support/test_helper.sh index bf371d0..7407b59 100644 --- a/test/support/test_helper.sh +++ b/test/support/test_helper.sh @@ -3,6 +3,12 @@ orig_cwd="$PWD" # Load SCM Breeze helpers 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" +fi + # # Test helpers #-----------------------------------------------------------------------------