Skip to content
Browse files

Initial Commit - moved out of ubuntu_config.

  • Loading branch information...
0 parents commit e634d0c4b5a71a874ad360f1ad19426bcd74d087 @ndbroadbent committed Oct 18, 2011
1 .gitignore
@@ -0,0 +1 @@
+git.scmbrc
2 .travis.yml
@@ -0,0 +1,2 @@
+before_script:
+ - sudo apt-get install zsh
3 Gemfile
@@ -0,0 +1,3 @@
+source 'http://rubygems.org'
+
+gem 'rake'
10 Gemfile.lock
@@ -0,0 +1,10 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ rake (0.9.2)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rake
28 README.markdown
@@ -0,0 +1,28 @@
+# 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)**
+
+# -------------------------------------------------------
+
+This is a collection of shell scripts (for `bash` and `zsh`) that enhance your interaction with git.
+
+* Numbered file shortcuts for git commands
+* Repository management scripts for Git projects
+* Symlink design assets
+
+
+Pull requests always welcome!
+
+
+### About
+
+These scripts have been refined and incubated in my bashrc for over a year,
+and I decided that they deserved their own project.
+
+I've tried to make each section modular, so you are free to choose what parts you want to use.
+
+I also know that we grow attached to the aliases we use every day, so I won't force you to use mine.
+I've given some example aliases at `aliases.example.sh`, but please customize them however you like.
+
15 Rakefile
@@ -0,0 +1,15 @@
+require 'rake'
+
+desc "Run shUnit2 tests"
+task :test do
+ Dir.glob("test/**/*_test.sh").each do |test|
+ ["bash", "zsh"].each do |shell|
+ puts "== Running tests with [#{shell}]: #{test}"
+ @failed = !system("#{shell} #{test}") || @failed
+ end
+ end
+ exit @failed ? 1 : 0
+end
+
+task :default => ['test']
+
72 git.scmbrc.example
@@ -0,0 +1,72 @@
+#
+# Git File Shortcuts Config
+# ------------------------------------------------------------------------------
+# - Set your preferred prefix for env variable file shortcuts.
+# (I chose 'e' because it is the easiest key to press after '$'.)
+export git_env_char="e"
+# - Max changes before reverting to 'git status'. git_status_shortcuts() may slow down for lots of changes.
+export gs_max_changes="110"
+# - Automatically use 'git rm' to remove deleted files when using the git_add_shorcuts() command?
+export ga_auto_remove="yes"
+
+
+# Git Repo Management Config
+# --------------------------
+# Repos will be automatically added from this directory.
+export GIT_REPO_DIR="$HOME/src"
+# Add the full paths of any extra repos to GIT_REPOS, separated with ':'
+# e.g. "/opt/rails/project:/opt/rails/another project:$HOME/other/repo"
+export GIT_REPOS=""
+export git_status_command="git_status_shortcuts"
+
+
+# Alias configuration
+# ------------------------------------------------------------------------------------
+git_alias="g"
+
+# 1. 'Git Breeze' functions
+git_status_shortcuts_alias="gs"
+git_add_shortcuts_alias="ga"
+git_show_files_alias="gsf"
+exec_git_expand_args_alias="ge"
+# 2. Commands that handle paths (with shortcut args expanded)
+git_checkout_alias="gco"
+git_commit_alias="gc"
+git_reset_alias="grs"
+git_rm_alias="grm"
+git_blame_alias="gbl"
+git_diff_alias="gd"
+git_diff_cached_alias="gdc"
+# 3. Standard commands
+git_clone_alias="gcl"
+git_fetch_alias="gf"
+git_fetch_and_rebase_alias="gfr"
+git_pull_alias="gpl"
+git_push_alias="gps"
+git_status_original_alias="gst"
+git_status_short_alias="gss"
+git_add_all_alias="gaa"
+git_commit_all_alias="gca"
+git_commit_amend_alias="gcm"
+git_commit_amend_no_msg_alias="gcmh"
+git_remote_alias="gr"
+git_branch_alias="gb"
+git_branch_all_alias="gba"
+git_rebase_alias="grb"
+git_merge_alias="gm"
+git_cherry_pick_alias="gcp"
+git_log_alias="gl"
+git_log_stat_alias="gls"
+git_log_graph_alias="glg"
+git_show_alias="gsh"
+
+# Git repo management
+git_repo_alias="s"
+
+
+# Keyboard shortcuts configuration
+# ---------------------------------------------
+git_status_shortcuts_keys="\C- " # CTRL+SPACE
+git_commit_all_keys="\C-x " # CTRL+x SPACE
+git_add_and_commit_keys="\C-xc" # CTRL+x c
+
10 install.sh
@@ -0,0 +1,10 @@
+# This loads Git Breeze into the shell session.
+exec_string='[[ -s "$HOME/.scm_breeze/scm_breeze.sh" ]] && . "$HOME/.scm_breeze/scm_breeze.sh"'
+
+# Add line to bashrc and zshrc if not already present.
+for rc in bashrc zshrc; do
+ if [[ -s "$HOME/.$rc" ]] && ! grep -q "$exec_string" "$HOME/.$rc"; then
+ echo -e "\n$exec_string" >> "$HOME/.$rc"
+ fi
+done
+
8 lib/_shared.sh
@@ -0,0 +1,8 @@
+# Detect shell
+if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; else shell="bash"; fi
+# Detect whether zsh 'shwordsplit' option is on by default.
+if [[ $shell == "zsh" ]]; then zsh_shwordsplit=$((setopt | grep -q shwordsplit) && echo "true"); fi
+# Switch on/off shwordsplit for functions that require it.
+zsh_compat(){ if [[ $shell == "zsh" && -z $zsh_shwordsplit ]]; then setopt shwordsplit; fi; }
+zsh_reset(){ if [[ $shell == "zsh" && -z $zsh_shwordsplit ]]; then unsetopt shwordsplit; fi; }
+
0 lib/bzr/BUILDME
No changes.
8 lib/git/_shared.sh
@@ -0,0 +1,8 @@
+# Detect shell
+if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; else shell="bash"; fi
+# Detect whether zsh 'shwordsplit' option is on by default.
+if [[ $shell == "zsh" ]]; then zsh_shwordsplit=$((setopt | grep -q shwordsplit) && echo "true"); fi
+# Switch on/off shwordsplit for functions that require it.
+zsh_compat(){ if [[ $shell == "zsh" && -z $zsh_shwordsplit ]]; then setopt shwordsplit; fi; }
+zsh_reset(){ if [[ $shell == "zsh" && -z $zsh_shwordsplit ]]; then unsetopt shwordsplit; fi; }
+
123 lib/git/aliases_and_bindings.sh
@@ -0,0 +1,123 @@
+#
+# Set up configured aliases & keyboard shortcuts
+# --------------------------------------------------------------------
+
+# Git Breeze functions
+alias $git_status_shortcuts_alias="git_status_shortcuts"
+alias $git_add_shortcuts_alias="git_add_shorcuts"
+alias $exec_git_expand_args_alias="exec_git_expand_args"
+alias $git_show_files_alias="git_show_affected_files"
+alias $git_commit_all_alias='git_commit_all'
+
+# Expand numbers and ranges for commands that deal with paths
+_exp="exec_git_expand_args"
+alias $git_checkout_alias="$_exp git checkout"
+alias $git_commit_alias="$_exp git commit"
+alias $git_reset_alias="$_exp git reset"
+alias $git_rm_alias="$_exp git rm"
+alias $git_blame_alias="$_exp git blame"
+alias $git_diff_alias="$_exp git diff"
+alias $git_diff_cached_alias="$_exp git diff --cached"
+
+# Standard commands
+alias $git_clone_alias='git clone'
+alias $git_fetch_alias='git fetch'
+alias $git_fetch_and_rebase_alias='git fetch && git rebase'
+alias $git_pull_alias='git pull'
+alias $git_push_alias='git push'
+alias $git_status_original_alias='git status' # (Standard git status)
+alias $git_status_short_alias='git status -s'
+alias $git_remote_alias='git remote -v'
+alias $git_branch_alias='git branch'
+alias $git_branch_all_alias='git branch -a'
+alias $git_rebase_alias='git rebase'
+alias $git_merge_alias='git merge'
+alias $git_cherry_pick_alias='git cherry-pick'
+alias $git_log_alias='git log'
+alias $git_log_stat_alias='git log --stat --max-count=5'
+alias $git_log_graph_alias='git log --graph --max-count=5'
+alias $git_show_alias='git show'
+alias $git_add_all_alias='git add -A'
+alias $git_commit_amend_alias='git commit --amend'
+# Add staged changes to latest commit without prompting for message
+alias $git_commit_amend_no_msg_alias='git commit --amend -C HEAD'
+
+# Git repo management alias
+alias $git_repo_alias="git_repo" # The 's' stands for 'switch' or 'sourcecode'
+
+
+# Tab completion for aliases
+if [[ $shell == "zsh" ]]; then
+ # Turn on support for bash completion
+ autoload bashcompinit
+ bashcompinit
+
+ # -- zsh
+ compdef $git_alias=git
+ compdef _git $git_pull_alias=git-pull
+ compdef _git $git_push_alias=git-push
+ compdef _git $git_fetch_alias=git-fetch
+ compdef _git $git_fetch_and_rebase_alias=git-fetch
+ compdef _git $git_diff_alias=git-diff
+ compdef _git $git_commit_alias=git-commit
+ compdef _git $git_commit_all_alias=git-commit
+ compdef _git $git_checkout_alias=git-checkout
+ compdef _git $git_branch_alias=git-branch
+ compdef _git $git_branch_all_alias=git-branch
+ compdef _git $git_log_alias=git-log
+ compdef _git $git_log_stat_alias=git-log
+ compdef _git $git_log_graph_alias=git-log
+ compdef _git $git_add_shortcuts_alias=git-add
+ compdef _git $git_merge_alias=git-merge
+else
+ # -- bash
+ complete -o default -o nospace -F _git $git_alias
+ complete -o default -o nospace -F _git_pull $git_pull_alias
+ complete -o default -o nospace -F _git_push $git_push_alias
+ complete -o default -o nospace -F _git_fetch $git_fetch_alias
+ complete -o default -o nospace -F _git_branch $git_branch_alias
+ complete -o default -o nospace -F _git_rebase $git_rebase_alias
+ complete -o default -o nospace -F _git_merge $git_merge_alias
+ complete -o default -o nospace -F _git_log $git_log_alias
+ complete -o default -o nospace -F _git_diff $git_diff_alias
+ complete -o default -o nospace -F _git_checkout $git_checkout_alias
+ complete -o default -o nospace -F _git_remote $git_remote_alias
+ complete -o default -o nospace -F _git_show $git_show_alias
+fi
+
+# Git repo management & aliases.
+# If you know how to rewrite _git_repo_tab_completion() for zsh, please send me a pull request!
+complete -o nospace -o filenames -F _git_repo_tab_completion git_repo
+complete -o nospace -o filenames -F _git_repo_tab_completion $git_repo_alias
+
+
+# Keyboard Bindings
+# -----------------------------------------------------------
+# 'git_commit_all' and 'git_add_and_commit' give commit message prompts.
+# See [here](http://qntm.org/bash#sec1) for info about why I wanted a prompt.
+
+# Cross-shell key bindings
+_bind(){
+ if [[ $shell == "zsh" ]]; then
+ bindkey -s "$1" "$2" # zsh
+ else
+ bind "\"$1\": \"$2\"" # bash
+ fi
+}
+
+case "$TERM" in
+xterm*|rxvt*)
+ # CTRL-SPACE => $ git_status_shortcuts {ENTER}
+ _bind "$git_status_shortcuts_keys" " git_status_shortcuts\n"
+ # CTRL-x-SPACE => $ git_commit_all {ENTER}
+ _bind "$git_commit_all_keys" " git_commit_all\n"
+ # CTRL-x-c => $ git_add_and_commit {ENTER}
+ # 1 3 CTRL-x-c => $ git_add_and_commit 1 3 {ENTER}
+ _bind "$git_add_and_commit_keys" "\e[1~ git_add_and_commit \n"
+
+ # Commands are prepended with a space so that they won't be added to history.
+ # Make sure this is turned on with:
+ # zsh: setopt histignorespace histignoredups
+ # bash: HISTCONTROL=ignorespace:ignoredups
+esac
+
154 lib/git/fallback/status_shortcuts_shell.sh
@@ -0,0 +1,154 @@
+# ------------------------------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# ------------------------------------------------------------------------------
+#
+# bash/zsh 'git_status_shortcuts' implementation, in case Ruby is not installed.
+# Of course, I wrote this function first, and then rewrote it in Ruby.
+#
+# Processes 'git status --porcelain', and exports numbered
+# env variables that contain the path of each affected file.
+# Output is also more concise than standard 'git status'.
+#
+# Call with optional <group> parameter to just show one modification state
+# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
+# --------------------------------------------------------------------
+git_status_shortcuts() {
+ zsh_compat # Ensure shwordsplit is on for zsh
+ local IFS=$'\n'
+ local git_status="$(git status --porcelain 2> /dev/null)"
+
+ if [ -n "$git_status" ] && [[ $(echo "$git_status" | wc -l) -le $gs_max_changes ]]; then
+ unset stat_file; unset stat_col; unset stat_msg; unset stat_grp; unset stat_x; unset stat_y
+ # Clear numbered env variables.
+ for (( i=1; i<=$gs_max_changes; i++ )); do unset $git_env_char$i; done
+
+ # Get branch
+ local branch=`git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'`
+ # Get project root
+ if [ -d .git ]; then
+ local project_root="$PWD"
+ else
+ local project_root=$(git rev-parse --git-dir 2> /dev/null | sed "s%/\.git$%%g")
+ fi
+
+ # Colors
+ local c_rst="\e[0m"
+ local c_branch="\e[1m"
+ local c_header="\e[0m"
+ local c_dark="\e[2;37m"
+ local c_del="\e[0;31m"
+ local c_mod="\e[0;32m"
+ local c_new="\e[0;33m"
+ local c_ren="\e[0;34m"
+ local c_cpy="\e[0;33m"
+ local c_ign="\e[0;36m"
+ # Following colors must be prepended with modifiers e.g. '\e[1;', '\e[0;'
+ local c_grp_1="33m"; local c_grp_2="31m"; local c_grp_3="32m"; local c_grp_4="36m"
+
+ local f=1; local e=1 # Counters for number of files, and ENV variables
+
+ echo -e "$c_dark#$c_rst On branch: $c_branch$branch$c_rst $c_dark| [$c_rst*$c_dark]$c_rst => \$$git_env_char*\n$c_dark#$c_rst"
+
+ for line in $git_status; do
+ if [[ $shell == *bash ]]; then
+ x=${line:0:1}; y=${line:1:1}; file=${line:3}
+ else
+ x=$line[1]; y=$line[2]; file=$line[4,-1]
+ fi
+
+ # Index modification states
+ msg=""
+ case "$x$y" in
+ "DD") msg=" both deleted"; col="$c_del"; grp="2";;
+ "AU") msg=" added by us"; col="$c_new"; grp="2";;
+ "UD") msg="deleted by them"; col="$c_del"; grp="2";;
+ "UA") msg=" added by them"; col="$c_new"; grp="2";;
+ "DU") msg=" deleted by us"; col="$c_del"; grp="2";;
+ "AA") msg=" both added"; col="$c_new"; grp="2";;
+ "UU") msg=" both modified"; col="$c_mod"; grp="2";;
+ "M"?) msg=" modified"; col="$c_mod"; grp="1";;
+ "A"?) msg=" new file"; col="$c_new"; grp="1";;
+ "D"?) msg=" deleted"; col="$c_del"; grp="1";;
+ "R"?) msg=" renamed"; col="$c_ren"; grp="1";;
+ "C"?) msg=" copied"; col="$c_cpy"; grp="1";;
+ "??") msg="untracked"; col="$c_ign"; grp="4";;
+ esac
+ if [ -n "$msg" ]; then
+ # Store data at array index and add to group
+ stat_file[$f]=$file; stat_msg[$f]=$msg; stat_col[$f]=$col
+ stat_grp[$grp]="${stat_grp[$grp]} $f"
+ let f++
+ fi
+
+ # Work tree modification states
+ msg=""
+ if [[ "$y" == "M" ]]; then msg=" modified"; col="$c_mod"; grp="3"; fi
+ # Don't show {Y} as deleted during a merge conflict.
+ if [[ "$y" == "D" && "$x" != "D" && "$x" != "U" ]]; then msg=" deleted"; col="$c_del"; grp="3"; fi
+ if [ -n "$msg" ]; then
+ stat_file[$f]=$file; stat_msg[$f]=$msg; stat_col[$f]=$col
+ stat_grp[$grp]="${stat_grp[$grp]} $f"
+ let f++
+ fi
+ done
+
+ 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
+ if [ -z "$1" ] || [[ "$1" == "$grp_num" ]]; then
+ local c_arrow="\e[1;$(eval echo \$c_grp_$grp_num)"
+ local c_hash="\e[0;$(eval echo \$c_grp_$grp_num)"
+ if [ -n "${stat_grp[$grp_num]}" ]; then
+ echo -e "$c_arrow$c_header $heading\n$c_hash#$c_rst"
+ _gs_output_file_group $grp_num
+ fi
+ fi
+ let grp_num++
+ done
+ else
+ # This function will slow down if there are too many changed files,
+ # so just use plain 'git status'
+ git status
+ fi
+ zsh_reset # Reset zsh environment to default
+}
+# Template function for 'git_status_shortcuts'.
+_gs_output_file_group() {
+ for i in ${stat_grp[$1]}; do
+ # Print colored hashes & files based on modification groups
+ local c_group="\e[0;$(eval echo -e \$c_grp_$1)"
+
+ # Deduce relative path based on current working directory
+ if [ -z "$project_root" ]; then
+ relative="${stat_file[$i]}"
+ else
+ dest="$project_root/${stat_file[$i]}"
+ relative="$(_gs_relative_path "$PWD" "$dest" )"
+ fi
+
+ if [[ $f -gt 10 && $e -lt 10 ]]; then local pad=" "; else local pad=""; fi # (padding)
+ echo -e "$c_hash#$c_rst ${stat_col[$i]}${stat_msg[$i]}:\
+$pad$c_dark [$c_rst$e$c_dark] $c_group$relative$c_rst"
+ # Export numbered variables in the order they are displayed.
+ # (Exports full path, but displays relative path)
+ export $git_env_char$e="$project_root/${stat_file[$i]}"
+ let e++
+ done
+ echo -e "$c_hash#$c_rst"
+}
+
+# Show relative path if current directory is not project root
+_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
+ common_part="${common_part%/*}"
+ back="../${back}"
+ done
+ echo "${back}${target#$common_part/}"
+}
+
229 lib/git/repo_management.sh
@@ -0,0 +1,229 @@
+# -------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# -------------------------------------------------------
+
+# -------------------------------------------------------
+# Repository management scripts for Git projects
+# -------------------------------------------------------
+
+
+# * The `git_repo` function makes it easy to list & switch between
+# git projects in $GIT_REPO_DIR (default = ~/src)
+#
+# * Change directory to any of your git repos or submodules, with recursive tab completion.
+#
+# * A repository index will be created at $GIT_REPO_DIR/.git_index
+# (Scanning for git projects and submodules can take a few seconds.)
+#
+# * Cache can be rebuilt by running:
+# $ git_repo --rebuild-index
+# ('--' commands have tab completion too.)
+#
+# * Ignores projects within an 'archive' folder.
+#
+# * Allows you to run batch commands across all your repositories:
+#
+# - Update every repo from their remote: 'git_repo --update-all'
+# - Produce a count of repos for each host: 'git_repo --count-by-host'
+# - Run a custom command for each repo: 'git_repo --batch-cmd <command>'
+#
+# Examples:
+#
+# $ git_repo --list
+# # => Lists all git projects
+#
+# $ git_repo ub[TAB]
+# # => Provides tab completion for all project folders that begin with 'ub'
+#
+# $ git_repo ubuntu_config
+# # => Changes directory to ubuntu_config, and auto-updates code from git remote.
+#
+# $ git_repo buntu_conf
+# # => Same result as `git_repo ubuntu_config`
+#
+# $ git_repo
+# # => cd $GIT_REPO_DIR
+
+
+function git_repo() {
+ local IFS=$'\n'
+ if [ -z "$1" ]; then
+ # Just change to $GIT_REPO_DIR if no params given.
+ cd $GIT_REPO_DIR
+ else
+ if [ "$1" = "--rebuild-index" ]; then
+ _rebuild_git_repo_index
+ elif [ "$1" = "--update-all" ]; then
+ _git_repo_git_update_all
+ elif [ "$1" = "--batch-cmd" ]; then
+ _git_repo_git_batch_cmd "${@:2:$(($#-1))}" # Pass all args except $1
+ elif [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
+ echo -e "$_bld_col$(_git_repo_count)$_txt_col Git repositories in $_bld_col$GIT_REPO_DIR$_txt_col:\n"
+ for repo in $(_git_repo_dirs_without_home); do
+ echo $(basename $repo) : $repo
+ done | sort | column -t -s ':'
+ elif [ "$1" = "--count-by-host" ]; then
+ echo -e "=== Producing a report of the number of repos per host...\n"
+ _git_repo_batch_cmd git remote -v | grep "origin.*(fetch)" |
+ sed -e "s/origin\s*//" -e "s/(fetch)//" |
+ sed -e "s/\(\([^/]*\/\/\)\?\([^@]*@\)\?\([^:/]*\)\).*/\1/" |
+ sort | uniq -c
+ echo
+ else
+ _check_git_repo_index
+ # Figure out which directory we need to change to.
+ local project=$(echo $1 | cut -d "/" -f1)
+ # Find base path of project
+ local base_path="$(grep "/$project$" "$GIT_REPO_DIR/.git_index")"
+ if [ -n "$base_path" ]; then
+ sub_path=$(echo $1 | sed "s:^$project::")
+ # Append subdirectories to base path
+ base_path="$base_path$sub_path"
+ fi
+ # Try partial matches
+ # - string at beginning of project
+ if [ -z "$base_path" ]; then base_path=$(_git_repo_dirs_without_home | grep -m1 "/$project"); fi
+ # - string anywhere in project
+ if [ -z "$base_path" ]; then base_path=$(_git_repo_dirs_without_home | grep -m1 "$project"); fi
+ # --------------------
+ # Go to our base path
+ if [ -n "$base_path" ]; then
+ unset IFS
+ eval cd "$base_path" # eval turns ~ into $HOME
+ # Run git callback (either update or show changes), if we are in the root directory
+ if [ -z "${sub_path%/}" ]; then _git_repo_pull_or_status; fi
+ else
+ echo -e "$_wrn_col'$1' did not match any git repos in $GIT_REPO_DIR$_txt_col"
+ fi
+ fi
+ fi
+}
+
+_git_repo_dirs_without_home() {
+ sed -e "s/--.*//" -e "s%$HOME%~%" $GIT_REPO_DIR/.git_index
+}
+
+# Recursively searches for git repos in $GIT_REPO_DIR
+function _find_git_repos() {
+ # Find all unarchived projects
+ local IFS=$'\n'
+ for repo in $(find "$GIT_REPO_DIR" -maxdepth 4 -name ".git" -type d \! -wholename '*/archive/*'); do
+ echo ${repo%/.git} # Return project folder, with trailing ':'
+ _find_git_submodules $repo # Detect any submodules
+ done
+}
+
+# List all submodules for a git repo, if any.
+function _find_git_submodules() {
+ if [ -e "$1/../.gitmodules" ]; then
+ grep "\[submodule" "$1/../.gitmodules" | sed "s%\[submodule \"%${1%/.git}/%g" | sed "s/\"]//g"
+ fi
+}
+
+
+# Rebuilds index of git repos in $GIT_REPO_DIR.
+function _rebuild_git_repo_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
+ 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"
+
+ if [ "$1" != "--silent" ]; then
+ echo -e "===== Cached $_bld_col$(_git_repo_count)$_txt_col repos in $GIT_REPO_DIR/.git_index"
+ fi
+}
+
+# Build index if empty
+function _check_git_repo_index() {
+ if [ ! -f "$GIT_REPO_DIR/.git_index" ]; then
+ _rebuild_git_repo_index --silent
+ fi
+}
+
+# Produces a count of repos in the tab completion index (excluding commands)
+function _git_repo_count() {
+ echo $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | grep . | wc -l)
+}
+
+# If the working directory is clean, update the git repository. Otherwise, show changes.
+function _git_repo_pull_or_status() {
+ if ! [ `git status --porcelain | wc -l` -eq 0 ]; then
+ # Fall back to 'git status' if git status alias isn't configured
+ if type $git_status_command 2>&1 | grep -qv "not found"; then
+ eval $git_status_command
+ else
+ git status
+ fi
+ else
+ # Check that a local 'origin' remote exists.
+ if (git remote -v | grep -q origin); then
+ branch=`parse_git_branch`
+ # Only update the git repo if it hasn't been touched for at least 6 hours.
+ if $(find ".git" -maxdepth 0 -type d -mmin +360 | grep -q "\.git"); then
+ # If we aren't on any branch, checkout master.
+ if [ "$branch" = "(no branch)" ]; then
+ echo -e "=== Checking out$_git_col master$_txt_col branch."
+ git checkout master
+ branch="master"
+ fi
+ echo -e "=== Updating '$branch' branch in $_bld_col$base_path$_txt_col from$_git_col origin$_txt_col... (Press Ctrl+C to cancel)"
+ # Pull the latest code from the server
+ git pull origin $branch
+ fi
+ fi
+ fi
+}
+
+# Updates all git repositories with clean working directories.
+function _git_repo_update_all() {
+ echo -e "== Updating code in $_bld_col$(_git_repo_count)$_txt_col repos...\n"
+ for base_path in $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | grep . | sort); do
+ echo -e "===== Updating code in \e[1;32m$base_path\e[0m...\n"
+ cd "$base_path"
+ _git_repo_pull_or_status
+ done
+}
+
+# Runs a command for all git repos
+function _git_repo_batch_cmd() {
+ if [ -n "$1" ]; then
+ echo -e "== Running command for $_bld_col$(_git_repo_count)$_txt_col repos...\n"
+ for base_path in $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | grep . | sort); do
+ 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.)"
+ fi
+}
+
+
+# Bash tab completion function for git_repo()
+function _git_repo_tab_completion() {
+ _check_git_repo_index
+ local curw
+ local IFS=$'\n'
+ COMPREPLY=()
+ curw=${COMP_WORDS[COMP_CWORD]}
+
+ # If the first part of $curw matches a high-level directory,
+ # then match on sub-directories for that project
+ local project=$(echo "$curw" | cut -d "/" -f1)
+ local base_path=$(grep "/$project$" "$GIT_REPO_DIR/.git_index" | sed 's/ /\\ /g')
+
+ # If matching path was found and curr string contains a /, then complete project sub-directories
+ if [[ -n "$base_path" && $curw == */* ]]; then
+ local search_path=$(echo "$curw" | sed "s:^${project/\\/\\\\\\}::")
+ COMPREPLY=($(compgen -d "$base_path$search_path" | grep -v "/.git" | sed -e "s:$base_path:$project:" -e "s:$:/:" ))
+ # Else, tab complete all the entries in .git_index, plus '--' commands
+ else
+ local commands="--list\n--rebuild-index\n--update-all\n--batch-cmd\n--count-by-host"
+ COMPREPLY=($(compgen -W '$(sed -e "s:.*/::" -e "s:$:/:" "$GIT_REPO_DIR/.git_index" | sort)$(echo -e "\n"$commands)' -- $curw))
+ fi
+ return 0
+}
+
160 lib/git/status_shortcuts.rb
@@ -0,0 +1,160 @@
+#!/usr/bin/env ruby
+# encoding: UTF-8
+# ------------------------------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# ------------------------------------------------------------------------------
+#
+# A much faster implementation of git_status_shortcuts() in ruby
+# (bash: 0m0.549s, ruby: 0m0.045s)
+#
+# Last line of output contains the ordered absolute file paths,
+# which need to be extracted by the shell and exported as numbered env variables.
+#
+# Processes 'git status --porcelain', and exports numbered
+# env variables that contain the path of each affected file.
+# Output is also more concise than standard 'git status'.
+#
+# Call with optional <group> parameter to just show one modification state
+# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
+# --------------------------------------------------------------------
+
+@project_root = File.exist?(".git") ? Dir.pwd : `git rev-parse --git-dir 2> /dev/null`.sub(/\/\.git$/, '').strip
+
+@git_status = `git status --porcelain 2> /dev/null`
+# Exit if no changes
+exit if @git_status == ""
+git_branch = `git branch -v 2> /dev/null`
+@branch = git_branch[/^\* ([^ ]*)/, 1]
+@ahead = git_branch[/^\* [^ ]* *[^ ]* *\[ahead ?(\d+)\]/, 1]
+
+@changes = @git_status.split("\n")
+# Exit if too many changes
+exit if @changes.size > ENV["gs_max_changes"].to_i
+
+# Colors
+@c = {
+ :rst => "\e[0m",
+ :del => "\e[0;31m",
+ :mod => "\e[0;32m",
+ :new => "\e[0;33m",
+ :ren => "\e[0;34m",
+ :cpy => "\e[0;33m",
+ :unt => "\e[0;36m",
+ :dark => "\e[2;37m",
+ :branch => "\e[1m",
+ :header => "\e[0m"
+}
+
+
+# Following colors must be prepended with modifiers e.g. '\e[1;', '\e[0;'
+@group_c = {
+ :staged => "33m",
+ :unmerged => "31m",
+ :unstaged => "32m",
+ :untracked => "36m"
+}
+
+@stat_hash = {
+ :staged => [],
+ :unmerged => [],
+ :unstaged => [],
+ :untracked => []
+}
+
+@output_files = []
+
+# Counter for env variables
+@e = 0
+
+# Heading
+ahead = @ahead ? " #{@c[:dark]}| #{@c[:new]}+#{@ahead}#{@c[:rst]}" : ""
+puts "%s#%s On branch: %s#{@branch}#{ahead} %s| [%s*%s]%s => $#{ENV["git_env_char"]}*\n%s#%s" % [
+ @c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst], @c[:dark], @c[:rst], @c[:dark], @c[:rst]
+]
+
+
+# Index modification states
+@changes.each do |change|
+ x, y, file = change[0, 1], change[1, 1], change[3..-1]
+
+ msg, col, group = case change[0..1]
+ when "DD"; [" both deleted", :del, :unmerged]
+ when "AU"; [" added by us", :new, :unmerged]
+ when "UD"; ["deleted by them", :del, :unmerged]
+ when "UA"; [" added by them", :new, :unmerged]
+ when "DU"; [" deleted by us", :del, :unmerged]
+ when "AA"; [" both added", :new, :unmerged]
+ when "UU"; [" both modified", :mod, :unmerged]
+ when /M./; [" modified", :mod, :staged]
+ when /A./; [" new file", :new, :staged]
+ when /D./; [" deleted", :del, :staged]
+ when /R./; [" renamed", :ren, :staged]
+ when /C./; [" copied", :cpy, :staged]
+ when "??"; ["untracked", :unt, :untracked]
+ end
+
+ # Store data
+ @stat_hash[group] << {:msg => msg, :col => col, :file => file} if msg
+
+ # Work tree modification states
+ if y == "M"
+ @stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => file}
+ elsif y == "D" && x != "D" && x != "U"
+ # Don't show deleted 'y' during a merge conflict.
+ @stat_hash[:unstaged] << {:msg => " deleted", :col => :del, :file => file}
+ end
+end
+
+def relative_path(base, target)
+ back = ""
+ while target.sub(base,'') == target
+ base = base.sub(/\/[^\/]*$/, '')
+ back = "../#{back}"
+ end
+ "#{back}#{target.sub("#{base}/",'')}"
+end
+
+
+# Output files
+def output_file_group(group)
+ # Print colored hashes & files based on modification groups
+ c_group = "\e[0;#{@group_c[group]}"
+
+ @stat_hash[group].each do |h|
+ @e += 1
+ padding = (@e < 10 && @changes.size >= 10) ? " " : ""
+
+ rel_file = relative_path(Dir.pwd, File.join(@project_root, h[:file]))
+
+ puts "#{c_group}##{@c[:rst]} #{@c[h[:col]]}#{h[:msg]}:\
+#{padding}#{@c[:dark]} [#{@c[:rst]}#{@e}#{@c[:dark]}] #{c_group}#{rel_file}#{@c[:rst]}"
+ # Save the ordered list of output files
+ @output_files << h[:file]
+ end
+
+ puts "#{c_group}##{@c[:rst]}" # Extra '#'
+end
+
+
+[[:staged, 'Changes to be committed'],
+[:unmerged, 'Unmerged paths'],
+[:unstaged, 'Changes not staged for commit'],
+[:untracked, 'Untracked files']
+].each_with_index do |data, i|
+ group, heading = *data
+
+ # Allow filtering by specific group (by string or integer)
+ if !ARGV[0] || ARGV[0] == group.to_s || ARGV[0] == (i+1).to_s; then
+ if !@stat_hash[group].empty?
+ c_arrow="\e[1;#{@group_c[group]}"
+ c_hash="\e[0;#{@group_c[group]}"
+ puts "#{c_arrow}#{@c[:header]} #{heading}\n#{c_hash}##{@c[:rst]}"
+ output_file_group(group)
+ end
+ end
+end
+
+puts @output_files.map{|f| File.join(@project_root, f) }.join("|")
+
188 lib/git/status_shortcuts.sh
@@ -0,0 +1,188 @@
+# ------------------------------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# ------------------------------------------------------------------------------
+
+# ------------------------------------------------------------------------------
+# Numbered file shortcuts for git commands
+# ------------------------------------------------------------------------------
+
+
+# Processes 'git status --porcelain', and exports numbered
+# env variables that contain the path of each affected file.
+# Output is also more concise than standard 'git status'.
+#
+# Call with optional <group> parameter to filter by modification state:
+# 1 || staged, 2 || unmerged, 3 || unstaged, 4 || untracked
+# --------------------------------------------------------------------
+git_status_shortcuts() {
+ # Run ruby script, store output
+ cmd_output=$(/usr/bin/env ruby "$gitbreezeDir/lib/git/status_shortcuts.rb" $@)
+ if [[ -z "$cmd_output" ]]; then
+ # Just show regular git status if ruby script returns nothing.
+ git status; return 1
+ fi
+ git_clear_vars
+ # Fetch list of files from last line of script output
+ files="$(echo "$cmd_output" | tail -n 1)"
+ # Export numbered env variables for each file
+ local IFS="|"
+ e=1; for file in $files; do export $git_env_char$e="$file"; let e++; done
+
+ # Print status
+ echo "$cmd_output" | head -n -1
+}
+
+
+
+# 'git add' & 'git rm' wrapper
+# This shortcut means 'stage the change to the file'
+# i.e. It will add new and changed files, and remove deleted files.
+# Should be used in conjunction with the git_status_shortcuts() function for 'git status'.
+# - 'auto git rm' behaviour can be turned off
+# -------------------------------------------------------------------------------
+git_add_shorcuts() {
+ if [ -z "$1" ]; then
+ echo "Usage: ga <file> => git add <file>"
+ echo " ga 1 => git add \$e1"
+ echo " ga 2..4 => git add \$e2 \$e3 \$e4"
+ echo " ga 2 5..7 => git add \$e2 \$e5 \$e6 \$e7"
+ if [[ $ga_auto_remove == "yes" ]]; then
+ echo -e "\nNote: Deleted files will also be staged using this shortcut."
+ echo " To turn off this behaviour, change the 'auto_remove' option."
+ fi
+ else
+ git_silent_add_shorcuts "$@"
+ # Makes sense to run 'git status' after this command.
+ git_status_shortcuts
+ fi
+}
+# Does nothing if no args are given.
+git_silent_add_shorcuts() {
+ if [ -n "$1" ]; then
+ # Expand args and process resulting set of files.
+ for file in $(git_expand_args "$@"); do
+ # Use 'git rm' if file doesn't exist and 'ga_auto_remove' is enabled.
+ if [[ $ga_auto_remove == "yes" ]] && ! [ -e $file ]; then
+ echo -n "# "
+ git rm $file
+ else
+ git add $file
+ echo -e "# add '$file'"
+ fi
+ done
+ echo "#"
+ fi
+}
+
+
+# Prints a list of all files affected by a given SHA1,
+# and exports numbered environment variables for each file.
+git_show_affected_files(){
+ f=0 # File count
+ # Show colored revision and commit message
+ echo -n "# "; script -q -c "git show --oneline --name-only $@" /dev/null | sed "s/\r//g" | head -n 1; 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 "# \e[2;37m[\e[0m$f\e[2;37m]\e[0m $file"
+ done; echo "# "
+}
+
+
+# Allows expansion of numbered shortcuts, ranges of shortcuts, or standard paths.
+# 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.
+git_expand_args() {
+ files=""
+ for arg in "$@"; do
+ if [[ "$arg" =~ ^[0-9]+$ ]] ; then # Substitute $e{*} variables for any integers
+ files="$files $(eval echo \$$git_env_char$arg)"
+ elif [[ $arg =~ ^[0-9]+\.\.[0-9]+$ ]]; then # Expand ranges into $e{*} variables
+ for i in $(seq $(echo $arg | tr ".." " ")); do
+ files="$files $(eval echo \$$git_env_char$i)"
+ done
+ else # Otherwise, treat $arg as a normal string.
+ files="$files $arg"
+ fi
+ done
+ echo $files
+}
+# Execute a command with expanded args, e.g. Delete files 6 to 12: $ ge rm 6..12
+exec_git_expand_args() { $(git_expand_args "$@"); }
+
+# Clear numbered env variables
+git_clear_vars() {
+ for (( i=1; i<=$gs_max_changes; i++ )); do
+ # Stop clearing after first empty var
+ if [[ -z "$(eval echo "\$$git_env_char$i")" ]]; then break; fi
+ unset $git_env_char$i
+ done
+}
+
+
+# Shortcuts for resolving merge conflicts.
+ours(){ local files=$(git_expand_args "$@"); git checkout --ours $files; git add $files; }
+theirs(){ local files=$(git_expand_args "$@"); git checkout --theirs $files; git add $files; }
+
+
+# Git commit prompts
+# ------------------------------------------------------------------------------
+
+# * Prompt for commit message
+# * Execute prerequisite commands if message given, abort if not
+# * Pipe commit message to 'git commit'
+# * Add escaped commit command and unescaped message to bash history.
+git_commit_prompt() {
+ local commit_msg
+ if [[ $shell == "zsh" ]]; then
+ # zsh 'read' is weak. If you know how to make this better, please send a pull request.
+ # (Bash 'read' supports prompt, arrow keys, home/end, up through bash history, etc.)
+ echo -n "Commit Message: "; read commit_msg
+ else
+ read -r -e -p "Commit Message: " commit_msg
+ fi
+
+ if [ -n "$commit_msg" ]; then
+ eval $@ # run any prequisite commands
+ echo $commit_msg | git commit -F - | tail -n +2
+ else
+ echo -e "\e[0;31mAborting commit due to empty commit message.\e[0m"
+ fi
+ escaped=$(echo "$commit_msg" | sed -e 's/"/\\"/g' -e 's/!/"'"'"'!'"'"'"/g')
+
+ if [[ $shell == "zsh" ]]; then
+ print -s "git commit -m \"${escaped//\\/\\\\}\"" # zsh's print needs double escaping
+ print -s "$commit_msg"
+ else
+ echo "git commit -m \"$escaped\"" >> $HISTFILE
+ # Also add unescaped commit message, for git prompt
+ echo "$commit_msg" >> $HISTFILE
+ fi
+}
+
+# Prompt for commit message, then commit all modified and untracked files.
+git_commit_all() {
+ changes=$(git status --porcelain | wc -l)
+ if [ "$changes" -gt 0 ]; then
+ echo -e "\e[0;33mCommitting all files (\e[0;31m$changes\e[0;33m)\e[0m"
+ git_commit_prompt "git add -A"
+ else
+ echo "# No changed files to commit."
+ fi
+}
+
+# Add paths or expanded args if any given, then commit all staged changes.
+git_add_and_commit() {
+ git_silent_add_shorcuts "$@"
+ changes=$(git diff --cached --numstat | wc -l)
+ if [ "$changes" -gt 0 ]; then
+ git_status_shortcuts 1 # only show staged changes
+ git_commit_prompt
+ else
+ echo "# No staged changes to commit."
+ fi
+}
+
31 lib/git/tools.sh
@@ -0,0 +1,31 @@
+# -------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# -------------------------------------------------------
+
+# -----------------------------------------------------------------
+# Misc Git Tools
+# - Please feel free to add your own git scripts, and send me a pull request
+# at https://github.com/ndbroadbent/scm_breeze
+# -----------------------------------------------------------------
+
+
+# Remove files/folders from git history
+# -------------------------------------------------------------------
+# To use it, cd to your repository's root and then run the function
+# with a list of paths you want to delete. e.g. git_remove_history path1 path2
+# Original Author: David Underhill
+git_remove_history() {
+ # Make sure we're at the root of a git repo
+ if [ ! -d .git ]; then
+ echo "Error: must run this script from the root of a git repository"
+ return
+ fi
+ # Remove all paths passed as arguments from the history of the repo
+ files=$@
+ git filter-branch --index-filter "git 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 reflog expire --all && git gc --aggressive --prune
+}
+
0 lib/hg/BUILDME
No changes.
0 lib/svn/BUILDME
No changes.
24 scm_breeze.sh
@@ -0,0 +1,24 @@
+#
+# Get directory of this file (for bash and zsh).
+# git_breeze.sh must not be run directly.
+# It must be sourced, e.g "source ~/.git_breeze/git_breeze.sh"
+# ------------------------------------------------------------
+
+export gitbreezeDir="$(dirname ${BASH_SOURCE:-$0})"
+
+# Load config
+. "$HOME/.git.scmbrc"
+
+. "$gitbreezeDir/lib/_shared.sh"
+. "$gitbreezeDir/lib/git/aliases_and_bindings.sh"
+. "$gitbreezeDir/lib/git/status_shortcuts.sh"
+. "$gitbreezeDir/lib/git/repo_management.sh"
+. "$gitbreezeDir/lib/git/tools.sh"
+
+
+if ! type ruby > /dev/null 2>&1; then
+ # If Ruby is not installed, fall back to the
+ # slower bash/zsh implementation of 'git_status_shortcuts'
+ . "$gitbreezeDir/lib/git/fallback/status_shortcuts_shell.sh"
+fi
+
185 test/lib/git/repo_management_test.sh
@@ -0,0 +1,185 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# ------------------------------------------------------------------------------
+#
+# Unit tests for git shell scripts
+
+thisDir="$( cd -P "$( dirname "$0" )" && pwd )"
+
+# Zsh compatibility
+if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; SHUNIT_PARENT=$0; setopt shwordsplit; fi
+
+# Load test helpers
+. "$thisDir/../../support/test_helper"
+
+# Load functions to test
+. "$thisDir/../../../lib/_shared.sh"
+. "$thisDir/../../../lib/git/repo_management.sh"
+
+
+# Setup and tear down
+#-----------------------------------------------------------------------------
+oneTimeSetUp() {
+ GIT_REPO_DIR=$(mktemp -d)
+ GIT_REPOS="/tmp/test_repo_1:/tmp/test_repo_11"
+ git_status_command="git status"
+
+ git_index_file="$GIT_REPO_DIR/.git_index"
+
+ silentGitCommands
+
+ cd $GIT_REPO_DIR
+ # Setup test repos in temp repo dir
+ for repo in github bitbucket source_forge; do
+ mkdir $repo; cd $repo; git init; cd - > /dev/null
+ done
+
+ # Add some nested dirs for testing resursive tab completion
+ mkdir -p github/videos/octocat/live_action
+ # Add hidden dir to test that '.git' is filtered, but other hidden dirs are available.
+ mkdir -p github/.im_hidden
+
+ # Setup a test repo with some submodules
+ # (just a dummy '.gitmodules' file and some nested .git directories)
+ mkdir submodules_everywhere
+ cd submodules_everywhere
+ git init
+ cat > .gitmodules <<EOF
+[submodule "very/nested/directory/red_submodule"]
+[submodule "very/nested/directory/green_submodule"]
+[submodule "very/nested/directory/blue_submodule"]
+EOF
+ mkdir -p "very/nested/directory"
+ cd "very/nested/directory"
+ for repo in red_submodule green_submodule blue_submodule; do
+ mkdir $repo; cd $repo; git init; cd - > /dev/null
+ done
+
+ # Setup some custom repos outside the main repo dir
+ local IFS=":"
+ for dir in $GIT_REPOS; do
+ mkdir -p $dir; cd $dir; git init;
+ done
+ unset IFS
+
+ verboseGitCommands
+
+ cd "$orig_cwd"
+}
+
+oneTimeTearDown() {
+ rm -rf "${GIT_REPO_DIR}"
+ local IFS=":"
+ for dir in $GIT_REPOS; do rm -rf $dir; done
+}
+
+ensureIndex() {
+ _check_git_repo_index
+}
+
+index_no_newlines() {
+ cat $git_index_file | tr "\\n" " "
+}
+
+
+#-----------------------------------------------------------------------------
+# Unit tests
+#-----------------------------------------------------------------------------
+
+test_repo_index_command() {
+ git_repo --rebuild-index > /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"
+
+}
+
+test_check_git_repo_index() {
+ ensureIndex
+ echo "should not be regenerated" >> $git_index_file
+ _check_git_repo_index
+ # Test that index is not rebuilt unless empty
+ assertIncludes "$(index_no_newlines)" "should not be regenerated"
+ rm $git_index_file
+ # Test the index is rebuilt
+ _check_git_repo_index
+ assertTrue "[ -f $git_index_file ]"
+}
+
+test_git_repo_count() {
+ assertEquals "9" "$(_git_repo_count)"
+}
+
+test_repo_list() {
+ ensureIndex
+ list=$(git_repo --list)
+ assertIncludes "$list" "bitbucket" || return
+ assertIncludes "$list" "blue_submodule" || return
+ assertIncludes "$list" "test_repo_11"
+}
+
+# Test matching rules for changing directory
+test_git_repo_changing_directory() {
+ ensureIndex
+ git_repo "github"; assertEquals "$GIT_REPO_DIR/github" "$PWD"
+ git_repo "github/"; assertEquals "$GIT_REPO_DIR/github" "$PWD"
+ git_repo "bucket"; assertEquals "$GIT_REPO_DIR/bitbucket" "$PWD"
+ git_repo "green_sub"; assertEquals "$GIT_REPO_DIR/submodules_everywhere/very/nested/directory/green_submodule" "$PWD"
+ git_repo "_submod"; assertEquals "$GIT_REPO_DIR/submodules_everywhere/very/nested/directory/blue_submodule" "$PWD"
+ git_repo "test_repo_1"; assertEquals "/tmp/test_repo_1" "$PWD"
+ git_repo "test_repo_11"; assertEquals "/tmp/test_repo_11" "$PWD"
+ git_repo "test_repo_"; assertEquals "/tmp/test_repo_11" "$PWD"
+ git_repo "github/videos/octocat/live_action"; assertEquals "$GIT_REPO_DIR/github/videos/octocat/live_action" "$PWD"
+}
+
+test_git_repo_tab_completion() {
+ # Only run tab completion test for bash
+ if [[ "$0" == *bash ]]; then
+ ensureIndex
+ COMP_CWORD=0
+
+ # Test that '--' commands have tab completion
+ COMP_WORDS="--"
+ _git_repo_tab_completion
+ assertEquals "Incorrect number of tab-completed '--' commands" "5" "$(tab_completions | wc -w)"
+
+ COMP_WORDS="gith"
+ _git_repo_tab_completion
+ assertIncludes "$(tab_completions)" "github/"
+
+ # Test completion for project sub-directories when project ends with '/'
+ COMP_WORDS="github/"
+ _git_repo_tab_completion
+ 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/"
+
+ COMP_WORDS="github/videos/"
+ _git_repo_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_repo_tab_completion
+ assertIncludes "$(tab_completions)" "test_repo_1/ test_repo_11/"
+ fi
+}
+
+
+# load and run shUnit2
+# Call this function to run tests
+. "$thisDir/../../support/shunit2"
+
255 test/lib/git/status_shortcuts_test.sh
@@ -0,0 +1,255 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# Git Breeze - Streamline your git workflow.
+# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+# ------------------------------------------------------------------------------
+#
+# Unit tests for git shell scripts
+
+thisDir="$( cd -P "$( dirname "$0" )" && pwd )"
+
+# Zsh compatibility
+if [ -n "${ZSH_VERSION:-}" ]; then shell="zsh"; SHUNIT_PARENT=$0; setopt shwordsplit; fi
+
+# Load test helpers
+. "$thisDir/../../support/test_helper"
+
+# Load functions to test
+. "$thisDir/../../../lib/_shared.sh"
+. "$thisDir/../../../lib/git/status_shortcuts.sh"
+
+
+# Setup and tear down
+#-----------------------------------------------------------------------------
+oneTimeSetUp() {
+ # Test Config
+ git_env_char="e"
+ gs_max_changes="20"
+ ga_auto_remove="yes"
+
+ testRepo=$(mktemp -d)
+}
+
+oneTimeTearDown() {
+ rm -rf "${testRepo}"
+}
+
+setupTestRepo() {
+ rm -rf "${testRepo}"
+ mkdir -p "$testRepo"
+ cd "$testRepo"
+ git init > /dev/null
+}
+
+
+#-----------------------------------------------------------------------------
+# Unit tests
+#-----------------------------------------------------------------------------
+
+test_git_expand_args() {
+ local e1="one"; local e2="two"; local e3="three"; local e4="four"; local e5="five"; local e6="six"; local e7="seven"
+ local error="Args not expanded correctly"
+ assertEquals "$error" "one three seven" "$(git_expand_args 1 3 7)"
+ assertEquals "$error" "one two three six" "$(git_expand_args 1..3 6)"
+ assertEquals "$error" "seven two three four five one" "$(git_expand_args seven 2..5 1)"
+}
+
+
+test_git_status_shortcuts() {
+ setupTestRepo
+
+ silentGitCommands
+
+ # Set up some modifications
+ touch deleted_file
+ git add deleted_file
+ git commit -m "Test commit"
+ touch new_file
+ touch untracked_file
+ git add new_file
+ echo "changed" > new_file
+ rm deleted_file
+
+ verboseGitCommands
+
+ # Test that groups can be filtered by passing a parameter
+ git_status1=$(git_status_shortcuts 1)
+ git_status3=$(git_status_shortcuts 3)
+ git_status4=$(git_status_shortcuts 4)
+
+ # Test for presence of expected groups
+ assertIncludes "$git_status1" "Changes to be committed"
+ assertIncludes "$git_status3" "Changes not staged for commit"
+ assertIncludes "$git_status4" "Untracked files"
+ assertNotIncludes "$git_status3" "Changes to be committed"
+ assertNotIncludes "$git_status4" "Changes not staged for commit"
+ assertNotIncludes "$git_status1" "Untracked files"
+ assertNotIncludes "$git_status4" "Changes to be committed"
+ assertNotIncludes "$git_status1" "Changes not staged for commit"
+ assertNotIncludes "$git_status3" "Untracked files"
+
+ # Run command in shell, load output from temp file into variable
+ temp_file=$(mktemp)
+ git_status_shortcuts > $temp_file
+ git_status=$(cat $temp_file | strip_colors)
+
+ assertIncludes "$git_status" "new file: *\[1\] *new_file" || return
+ assertIncludes "$git_status" "deleted: *\[2\] *deleted_file" || return
+ assertIncludes "$git_status" "modified: *\[3\] *new_file" || return
+ assertIncludes "$git_status" "untracked: *\[4\] *untracked_file" || return
+
+ # Test that shortcut env variables are set with full path
+ local error="Env variable was not set"
+ assertEquals "$error" "$testRepo/new_file" "$e1" || return
+ assertEquals "$error" "$testRepo/deleted_file" "$e2" || return
+ assertEquals "$error" "$testRepo/new_file" "$e3" || return
+ assertEquals "$error" "$testRepo/untracked_file" "$e4" || return
+}
+
+test_git_status_produces_relative_paths() {
+ setupTestRepo
+
+ mkdir -p dir1/sub1/subsub1
+ mkdir -p dir1/sub2
+ mkdir -p dir2
+ touch dir1/sub1/subsub1/testfile
+ touch dir1/sub2/testfile
+ touch dir2/testfile
+ git add .
+
+ git_status=$(git_status_shortcuts | strip_colors)
+ assertIncludes "$git_status" "dir1/sub1/subsub1/testfile" || return
+
+ cd $testRepo/dir1
+ git_status=$(git_status_shortcuts | strip_colors)
+ assertIncludes "$git_status" " sub1/subsub1/testfile" || return
+ assertIncludes "$git_status" " sub2/testfile" || return
+ assertIncludes "$git_status" "../dir2/testfile" || return
+
+ cd $testRepo/dir1/sub1
+ git_status=$(git_status_shortcuts | strip_colors)
+ assertIncludes "$git_status" " subsub1/testfile" || return
+ assertIncludes "$git_status" " ../sub2/testfile" || return
+ assertIncludes "$git_status" "../../dir2/testfile" || return
+
+ cd $testRepo/dir1/sub1/subsub1
+ git_status=$(git_status_shortcuts | strip_colors)
+ assertIncludes "$git_status" " testfile" || return
+ assertIncludes "$git_status" " ../../sub2/testfile" || return
+ assertIncludes "$git_status" "../../../dir2/testfile" || return
+}
+
+
+test_git_status_shortcuts_merge_conflicts() {
+ setupTestRepo
+
+ silentGitCommands
+
+ # Set up every possible merge conflict
+ touch both_modified both_deleted deleted_by_them deleted_by_us
+ echo "renamed file needs some content" > renamed_file
+ git add both_modified both_deleted renamed_file deleted_by_them deleted_by_us
+ git commit -m "First commit"
+
+ git checkout -b conflict_branch
+ echo "added by branch" > both_added
+ echo "branch line" > both_modified
+ echo "deleted by us" > deleted_by_us
+ git rm deleted_by_them both_deleted
+ git mv renamed_file renamed_file_on_branch
+ git add both_added both_modified deleted_by_us
+ git commit -m "Branch commit"
+
+ git checkout master
+ echo "added by master" > both_added
+ echo "master line" > both_modified
+ echo "deleted by them" > deleted_by_them
+ git rm deleted_by_us both_deleted
+ git mv renamed_file renamed_file_on_master
+ git add both_added both_modified deleted_by_them
+ git commit -m "Master commit"
+
+ git merge conflict_branch
+
+ verboseGitCommands
+
+ # Test output without stripped color codes
+ git_status=$(git_status_shortcuts | strip_colors)
+ assertIncludes "$git_status" "both added: *\[[0-9]*\] *both_added" || return
+ assertIncludes "$git_status" "both modified: *\[[0-9]*\] *both_modified" || return
+ assertIncludes "$git_status" "deleted by them: *\[[0-9]*\] *deleted_by_them" || return
+ assertIncludes "$git_status" "deleted by us: *\[[0-9]*\] *deleted_by_us" || return
+ assertIncludes "$git_status" "both deleted: *\[[0-9]*\] *renamed_file" || return
+ assertIncludes "$git_status" "added by them: *\[[0-9]*\] *renamed_file_on_branch" || return
+ assertIncludes "$git_status" "added by us: *\[[0-9]*\] *renamed_file_on_master" || return
+}
+
+
+test_git_status_shortcuts_max_changes() {
+ setupTestRepo
+
+ export gs_max_changes="5"
+
+ # Add 5 untracked files
+ touch a b c d e
+ git_status=$(git_status_shortcuts | strip_colors)
+ for i in $(seq 1 5); do
+ assertIncludes "$git_status" "\[$i\]" || return
+ done
+
+ # 6 untracked files is more than $gs_max_changes
+ touch f
+ git_status=$(git_status_shortcuts | strip_colors)
+ assertNotIncludes "$git_status" "\[[0-9]*\]" || return
+
+ export gs_max_changes="20"
+}
+
+
+test_git_add_shorcuts() {
+ setupTestRepo
+
+ touch a b c d e f g h i j
+ # Show git status, which sets up env variables
+ git_status_shortcuts > /dev/null
+ git_add_shorcuts 2..4 7 8 > /dev/null
+ git_status=$(git_status_shortcuts 1 | strip_colors)
+
+ for c in b c d g h; do
+ assertIncludes "$git_status" "\[[0-9]*\] $c" || return
+ done
+}
+
+test_git_commit_prompt() {
+ setupTestRepo
+
+ commit_msg="\"Nathan's git commit prompt function!\""
+ dbl_escaped_msg="\\\\\"Nathan's git commit prompt function\"'"'!'"'\"\\\\\""
+ # Create temporary history file
+ HISTFILE=$(mktemp)
+ HISTFILESIZE=1000
+ HISTSIZE=1000
+
+ touch a b c d
+ git add . > /dev/null
+
+ # Lightly test the git commit prompt, by piping a commit message
+ # instead of user input.
+ echo "$commit_msg" | git_commit_prompt > /dev/null
+
+ git_show_output=$(git show --oneline --name-only)
+ 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)"
+ assertIncludes "$test_history" "$commit_msg"
+ assertIncludes "$test_history" "git commit -m \"$dbl_escaped_msg\""
+}
+
+
+
+# load and run shUnit2
+. "$thisDir/../../support/shunit2"
+
1,048 test/support/shunit2
@@ -0,0 +1,1048 @@
+#! /bin/sh
+# $Id: shunit2 335 2011-05-01 20:10:33Z kate.ward@forestent.com $
+# vim:et:ft=sh:sts=2:sw=2
+#
+# Copyright 2008 Kate Ward. All Rights Reserved.
+# Released under the LGPL (GNU Lesser General Public License)
+#
+# shUnit2 -- Unit testing framework for Unix shell scripts.
+# http://code.google.com/p/shunit2/
+#
+# Author: kate.ward@forestent.com (Kate Ward)
+#
+# shUnit2 is a xUnit based unit test framework for Bourne shell scripts. It is
+# based on the popular JUnit unit testing framework for Java.
+
+# return if shunit already loaded
+[ -n "${SHUNIT_VERSION:-}" ] && exit 0
+
+SHUNIT_VERSION='2.1.6'
+
+SHUNIT_TRUE=0
+SHUNIT_FALSE=1
+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}; }
+
+# specific shell checks
+if [ -n "${ZSH_VERSION:-}" ]; then
+ setopt |grep "^shwordsplit$" >/dev/null
+ if [ $? -ne ${SHUNIT_TRUE} ]; then
+ _shunit_fatal 'zsh shwordsplit option is required for proper operation'
+ fi
+ if [ -z "${SHUNIT_PARENT:-}" ]; then
+ _shunit_fatal "zsh does not pass \$0 through properly. please declare \
+\"SHUNIT_PARENT=\$0\" before calling shUnit2"
+ fi
+fi
+
+#
+# constants
+#
+
+__SHUNIT_ASSERT_MSG_PREFIX='ASSERT:'
+__SHUNIT_MODE_SOURCED='sourced'
+__SHUNIT_MODE_STANDALONE='standalone'
+__SHUNIT_PARENT=${SHUNIT_PARENT:-$0}
+
+# set the constants readonly
+shunit_constants_=`set |grep '^__SHUNIT_' |cut -d= -f1`
+echo "${shunit_constants_}" |grep '^Binary file' >/dev/null && \
+ shunit_constants_=`set |grep -a '^__SHUNIT_' |cut -d= -f1`
+for shunit_constant_ in ${shunit_constants_}; do
+ shunit_ro_opts_=''
+ case ${ZSH_VERSION:-} in
+ '') ;; # this isn't zsh
+ [123].*) ;; # early versions (1.x, 2.x, 3.x)
+ *) shunit_ro_opts_='-g' ;; # all later versions. declare readonly globally
+ esac
+ readonly ${shunit_ro_opts_} ${shunit_constant_}
+done
+unset shunit_constant_ shunit_constants_ shunit_ro_opts_
+
+# variables
+__shunit_lineno='' # line number of executed test
+__shunit_mode=${__SHUNIT_MODE_SOURCED} # operating mode
+__shunit_reportGenerated=${SHUNIT_FALSE} # is report generated
+__shunit_script='' # filename of unittest script (standalone mode)
+__shunit_skip=${SHUNIT_FALSE} # is skipping enabled
+__shunit_suite='' # suite of tests to execute
+
+# counts of tests
+__shunit_testSuccess=${SHUNIT_TRUE}
+__shunit_testsTotal=0
+__shunit_testsPassed=0
+__shunit_testsFailed=0
+
+# counts of asserts
+__shunit_assertsTotal=0
+__shunit_assertsPassed=0
+__shunit_assertsFailed=0
+__shunit_assertsSkipped=0
+
+# macros
+_SHUNIT_LINENO_='eval __shunit_lineno=""; if [ "${1:-}" = "--lineno" ]; then [ -n "$2" ] && __shunit_lineno="[$2] "; shift 2; fi'
+
+#-----------------------------------------------------------------------------
+# assert functions
+#
+
+# Assert that two values are equal to one another.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertEquals()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertEquals() requires two or three arguments; $# given"
+ _shunit_error "1: ${1:+$1} 2: ${2:+$2} 3: ${3:+$3}${4:+ 4: $4}"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if [ "${shunit_expected_}" = "${shunit_actual_}" ]; then
+ _shunit_assertPass
+ else
+ failNotEquals "${shunit_message_}" "${shunit_expected_}" "${shunit_actual_}"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${shunit_return}
+}
+_ASSERT_EQUALS_='eval assertEquals --lineno "${LINENO:-}"'
+
+# Assert that two values are not equal to one another.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotEquals()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotEquals() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if [ "${shunit_expected_}" != "${shunit_actual_}" ]; then
+ _shunit_assertPass
+ else
+ failSame "${shunit_message_}" "$@"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${shunit_return}
+}
+_ASSERT_NOT_EQUALS_='eval assertNotEquals --lineno "${LINENO:-}"'
+
+# Assert that a value is null (i.e. an empty string)
+#
+# Args:
+# message: string: failure message [optional]
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNull()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertNull() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ assertTrue "${shunit_message_}" "[ -z '$1' ]"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_NULL_='eval assertNull --lineno "${LINENO:-}"'
+
+# Assert that a value is not null (i.e. a non-empty string)
+#
+# Args:
+# message: string: failure message [optional]
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotNull()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -gt 2 ]; then # allowing 0 arguments as $1 might actually be null
+ _shunit_error "assertNotNull() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_actual_=`_shunit_escapeCharactersInString "${1:-}"`
+ test -n "${shunit_actual_}"
+ assertTrue "${shunit_message_}" $?
+ shunit_return=$?
+
+ unset shunit_actual_ shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_NOT_NULL_='eval assertNotNull --lineno "${LINENO:-}"'
+
+# Assert that two values are the same (i.e. equal to one another).
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ assertEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_SAME_='eval assertSame --lineno "${LINENO:-}"'
+
+# Assert that two values are not the same (i.e. not equal to one another).
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_:-}$1"
+ shift
+ fi
+ assertNotEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_ASSERT_NOT_SAME_='eval assertNotSame --lineno "${LINENO:-}"'
+
+# Assert that a value or shell test condition is true.
+#
+# In shell, a value of 0 is true and a non-zero value is false. Any integer
+# value passed can thereby be tested.
+#
+# Shell supports much more complicated tests though, and a means to support
+# them was needed. As such, this function tests that conditions are true or
+# false through evaluation rather than just looking for a true or false.
+#
+# The following test will succeed:
+# assertTrue 0
+# assertTrue "[ 34 -gt 23 ]"
+# The folloing test will fail with a message:
+# assertTrue 123
+# assertTrue "test failed" "[ -r '/non/existant/file' ]"
+#
+# Args:
+# message: string: failure message [optional]
+# condition: string: integer value or shell conditional statement
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertTrue()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -gt 2 ]; then
+ _shunit_error "assertTrue() takes one two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_condition_=$1
+
+ # see if condition is an integer, i.e. a return value
+ shunit_match_=`expr "${shunit_condition_}" : '\([0-9]*\)'`
+ shunit_return=${SHUNIT_TRUE}
+ if [ -z "${shunit_condition_}" ]; then
+ # null condition
+ shunit_return=${SHUNIT_FALSE}
+ elif [ -n "${shunit_match_}" -a "${shunit_condition_}" = "${shunit_match_}" ]
+ then
+ # possible return value. treating 0 as true, and non-zero as false.
+ [ ${shunit_condition_} -ne 0 ] && shunit_return=${SHUNIT_FALSE}
+ else
+ # (hopefully) a condition
+ ( eval ${shunit_condition_} ) >/dev/null 2>&1
+ [ $? -ne 0 ] && shunit_return=${SHUNIT_FALSE}
+ fi
+
+ # record the test
+ if [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_assertPass
+ else
+ _shunit_assertFail "${shunit_message_}"
+ fi
+
+ unset shunit_message_ shunit_condition_ shunit_match_
+ return ${shunit_return}
+}
+_ASSERT_TRUE_='eval assertTrue --lineno "${LINENO:-}"'
+
+# Assert that a value or shell test condition is false.
+#
+# In shell, a value of 0 is true and a non-zero value is false. Any integer
+# value passed can thereby be tested.
+#
+# Shell supports much more complicated tests though, and a means to support
+# them was needed. As such, this function tests that conditions are true or
+# false through evaluation rather than just looking for a true or false.
+#
+# The following test will succeed:
+# assertFalse 1
+# assertFalse "[ 'apples' = 'oranges' ]"
+# The folloing test will fail with a message:
+# assertFalse 0
+# assertFalse "test failed" "[ 1 -eq 1 -a 2 -eq 2 ]"
+#
+# Args:
+# message: string: failure message [optional]
+# condition: string: integer value or shell conditional statement
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertFalse()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertFalse() quires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_condition_=$1
+
+ # see if condition is an integer, i.e. a return value
+ shunit_match_=`expr "${shunit_condition_}" : '\([0-9]*\)'`
+ shunit_return=${SHUNIT_TRUE}
+ if [ -z "${shunit_condition_}" ]; then
+ # null condition
+ shunit_return=${SHUNIT_FALSE}
+ elif [ -n "${shunit_match_}" -a "${shunit_condition_}" = "${shunit_match_}" ]
+ then
+ # possible return value. treating 0 as true, and non-zero as false.
+ [ ${shunit_condition_} -eq 0 ] && shunit_return=${SHUNIT_FALSE}
+ else
+ # (hopefully) a condition
+ ( eval ${shunit_condition_} ) >/dev/null 2>&1
+ [ $? -eq 0 ] && shunit_return=${SHUNIT_FALSE}
+ fi
+
+ # record the test
+ if [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_assertPass
+ else
+ _shunit_assertFail "${shunit_message_}"
+ fi
+
+ unset shunit_message_ shunit_condition_ shunit_match_
+ return ${shunit_return}
+}
+_ASSERT_FALSE_='eval assertFalse --lineno "${LINENO:-}"'
+
+#-----------------------------------------------------------------------------
+# failure functions
+#
+
+# Records a test failure.
+#
+# Args:
+# message: string: failure message [optional]
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+fail()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -gt 1 ]; then
+ _shunit_error "fail() requires zero or one arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 1 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ _shunit_assertFail "${shunit_message_}"
+
+ unset shunit_message_
+ return ${SHUNIT_FALSE}
+}
+_FAIL_='eval fail --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values were not equal.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotEquals()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failNotEquals() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected:<${shunit_expected_}> but was:<${shunit_actual_}>"
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${SHUNIT_FALSE}
+}
+_FAIL_NOT_EQUALS_='eval failNotEquals --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values should have been the same.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected not same"
+
+ unset shunit_message_
+ return ${SHUNIT_FALSE}
+}
+_FAIL_SAME_='eval failSame --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values were not equal.
+#
+# This is functionally equivalent to calling failNotEquals().
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotSame()
+{
+ ${_SHUNIT_LINENO_}
+ if [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failNotEquals() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ _shunit_shouldSkip && return ${SHUNIT_TRUE}
+
+ shunit_message_=${__shunit_lineno}
+ if [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ failNotEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+_FAIL_NOT_SAME_='eval failNotSame --lineno "${LINENO:-}"'
+
+#-----------------------------------------------------------------------------
+# skipping functions
+#
+
+# Force remaining assert and fail functions to be "skipped".
+#
+# This function forces the remaining assert and fail functions to be "skipped",
+# i.e. they will have no effect. Each function skipped will be recorded so that
+# the total of asserts and fails will not be altered.
+#
+# Args:
+# None
+startSkipping()
+{
+ __shunit_skip=${SHUNIT_TRUE}
+}
+
+# Resume the normal recording behavior of assert and fail calls.
+#
+# Args:
+# None
+endSkipping()
+{
+ __shunit_skip=${SHUNIT_FALSE}
+}
+
+# Returns the state of assert and fail call skipping.
+#
+# Args:
+# None
+# Returns:
+# boolean: (TRUE/FALSE constant)
+isSkipping()
+{
+ return ${__shunit_skip}
+}
+
+#-----------------------------------------------------------------------------
+# suite functions
+#
+
+# Stub. This function should contains all unit test calls to be made.
+#
+# DEPRECATED (as of 2.1.0)
+#
+# This function can be optionally overridden by the user in their test suite.
+#
+# If this function exists, it will be called when shunit2 is sourced. If it
+# does not exist, shunit2 will search the parent script for all functions
+# beginning with the word 'test', and they will be added dynamically to the
+# test suite.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#suite() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Adds a function name to the list of tests schedule for execution.
+#
+# This function should only be called from within the suite() function.
+#
+# Args:
+# function: string: name of a function to add to current unit test suite
+suite_addTest()
+{
+ shunit_func_=${1:-}
+
+ __shunit_suite="${__shunit_suite:+${__shunit_suite} }${shunit_func_}"
+ __shunit_testsTotal=`expr ${__shunit_testsTotal} + 1`
+
+ unset shunit_func_
+}
+
+# Stub. This function will be called once before any tests are run.
+#
+# Common one-time environment preparation tasks shared by all tests can be
+# defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#oneTimeSetUp() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Stub. This function will be called once after all tests are finished.
+#
+# Common one-time environment cleanup tasks shared by all tests can be defined
+# here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#oneTimeTearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Stub. This function will be called before each test is run.
+#
+# Common environment preparation tasks shared by all tests can be defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#setUp() { :; }
+
+# Note: see _shunit_mktempFunc() for actual implementation
+# Stub. This function will be called after each test is run.
+#
+# Common environment cleanup tasks shared by all tests can be defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#tearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+#------------------------------------------------------------------------------
+# internal shUnit2 functions
+#
+
+# Create a temporary directory to store various run-time files in.
+#
+# This function is a cross-platform temporary directory creation tool. Not all
+# OSes have the mktemp function, so one is included here.
+#
+# Args:
+# None
+# Outputs:
+# string: the temporary directory that was created
+_shunit_mktempDir()
+{
+ # try the standard mktemp function
+ ( exec mktemp -dqt shunit.XXXXXX 2>/dev/null ) && return
+
+ # the standard mktemp didn't work. doing our own.
+ if [ -r '/dev/urandom' -a -x '/usr/bin/od' ]; then
+ _shunit_random_=`/usr/bin/od -vAn -N4 -tx4 </dev/urandom \
+ |sed 's/^[^0-9a-f]*//'`
+ elif [ -n "${RANDOM:-}" ]; then
+ # $RANDOM works
+ _shunit_random_=${RANDOM}${RANDOM}${RANDOM}$$
+ else
+ # $RANDOM doesn't work
+ _shunit_date_=`date '+%Y%m%d%H%M%S'`
+ _shunit_random_=`expr ${_shunit_date_} / $$`
+ fi
+
+ _shunit_tmpDir_="${TMPDIR:-/tmp}/shunit.${_shunit_random_}"
+ ( umask 077 && mkdir "${_shunit_tmpDir_}" ) || \
+ _shunit_fatal 'could not create temporary directory! exiting'
+