diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c8b204 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +default: help + +help: + @echo make install - Install the scripts to $(HOME)/bin + @echo make clean - Remove the scripts + +clean: + rm -f "$(HOME)/bin/git-state" + rm -f "$(HOME)/bin/git-ps1.sh" + +install: + @PWD=`pwd` + ln -s "$(PWD)/bin/git-state" "$(HOME)/bin/git-state" + ln -s "$(PWD)/git-ps1.sh" "$(HOME)/bin/git-ps1.sh" + +.PHONY: clean install + diff --git a/README b/README deleted file mode 100644 index 2dfdb88..0000000 --- a/README +++ /dev/null @@ -1,33 +0,0 @@ -git-ps1 - git-augmented PS1 for BASH - -SETUP ------ -To use, modify your PS1 variable (e.g. in ~/.bashrc) to include the script: - PS1="$PS1\$($( cat git-ps1.sh ))" - -This script produces output in a number of different colors. To ensure the -prompt operates correctly, non-printable characters must be escaped (\[\]). -However, these characters are not recognized when output from an external -script. Therefore, the script must be inserted directly into the PS1 string, -where echoing the escape sequence will produce the intended result. - -CONFIGURATION -------------- -All configuration is done via GITPS1_* environment variables. - -Indicators (set to '0' to disable) - GITPS1_IND_STAGED - Staged changes - GITPS1_IND_UNSTAGED - Unstaged changes - GITPS1_IND_UNTRACKED - Untracked files - GITPS1_IND_AHEAD - Ahead of tracking branch - GITPS1_IND_AHEAD_COUNT - Whether to display number of commits ahead (e.g. @5) - -Colors: - GITPS1_COLOR_DEFAULT - Default color, used to display brackets and branch - GITPS1_COLOR_FASTFWD - Color used for fast-forward indicator and used to - display brackets and hash when not on a branch - GITPS1_COLOR_STAGED - Color used to for staged changes indicator - GITPS1_COLOR_UNTRACKED - Color used for untracked files indicator - GITPS1_COLOR_UNSTAGED - Color used for unstaged changes indicator - GITPS1_COLOR_AHEAD - Color used for ahead indicator (ahead of tracking) - diff --git a/README.md b/README.md new file mode 100644 index 0000000..429654a --- /dev/null +++ b/README.md @@ -0,0 +1,181 @@ +git-supp is a package of supplemental scripts and enhancements for Git. They +are intended to solve common issues or reduce repetitiveness for common +day-to-day Git tasks. + +The name "git-supp" is not the name of a project. Rather, it is simply a name +given to the repository. + + +# Git PS1 +git-ps1 is a Git-augmented PS1 for BASH. It uses colors and various identifiers +to display information about the status of the current branch, including the +branch name, staged and unstaged changes, untracked files, commits ahead of the +tracking branch and more. + +## Setup +To use, modify your PS1 variable (e.g. in ~/.bashrc) to include the script: + PS1="$PS1\$($( cat git-ps1.sh ))" + +This script produces output in a number of different colors. To ensure the +prompt operates correctly, non-printable characters must be escaped (\[\]). +However, these characters are not recognized when output from an external +script. Therefore, the script must be inserted directly into the PS1 string, +where echoing the escape sequence will produce the intended result. + +## Configuration +All configuration is done via `GITPS1_*` environment variables. + +Indicators (set to '0' to disable) + +* `GITPS1_IND_STAGED` - Staged changes +* `GITPS1_IND_UNSTAGED` - Unstaged changes +* `GITPS1_IND_UNTRACKED` - Untracked files +* `GITPS1_IND_AHEAD` - Ahead of tracking branch +* `GITPS1_IND_AHEAD_COUNT` - Whether to display number of commits ahead (e.g. @5) +* `GITPS1_IND_STATE` - Whether to display state string (see git-supp) + +Colors: + +* `GITPS1_COLOR_DEFAULT` - Default color, used to display brackets and branch +* `GITPS1_COLOR_FASTFWD` - Color used for fast-forward indicator and used to + display brackets and hash when not on a branch +* `GITPS1_COLOR_STAGED` - Color used to for staged changes indicator +* `GITPS1_COLOR_UNTRACKED` - Color used for untracked files indicator +* `GITPS1_COLOR_UNSTAGED` - Color used for unstaged changes indicator +* `GITPS1_COLOR_AHEAD` - Color used for ahead indicator (ahead of tracking) +* `GITPS1_COLOR_STATE` - Color used for state string (see git-supp) + + +# shortmaps / BASH Completion +The `bash_completion` file contains BASH completion for custom commands and +"shortmaps", which provide single or double-character aliases to common Git +commands. + +## Setup +Source the `bash_completion` file (e.g. place in `.bashrc` or in +`/etc/bash_completion.d/` on Debian systems), with the path to the provided +`shortmaps` file as the only argument: + +``` +$ . bash_completion ./shortmaps +``` + +You may also add your own mappings to `~/.git-ps1-shortmaps`. + +## Usage +By default, the following mappings are available, each with tab completion: + +* `a` - git add +* `A` - git add -A +* `B` - git bisect +* `Bs` - git bisect start +* `Bg` - git bisect good +* `Bb` - git bisect bad +* `Br` - git bisect reset +* `c` - git commit +* `C` - git commit -am +* `co` - git checkout +* `d` - git diff +* `f` - git fetch +* `m` - git merge +* `p` - git push +* `P` - git pull +* `R` - git rebase +* `Ri` - git rebase --interactive +* `Ra` - git rebase --abort +* `Rc` - git rebase --continue +* `s` - git status +* `S` - git stash +* `t` - execute tig +* `T` - git tag +* `-` - git checkout - +* `--` - `cd` to root dir of repository + +The shortmaps may only be used within a git repository. Otherwise, they will +invoke the actual command on the system. + +If a command conflicts with an existing command on your system, wrap the command +in quotes to invoke the actual command. + +## Configuration +The file format is as follows: + +``` +KEY COMPLETION :CMD +KEY COMPLETION |CMD +KEY COMPLETION CMD +``` + +If `CMD` contains a colon (`:`) prefix, the command will be prefixed with `git`. If +prefixed with a pipe (`|`), the command will be sent to `eval` (needed for +certain features like subshells). Commands without either prefix will be +executed normally. + + +# git state +Adds the concept of "states" to branches. The state, which is represented as a +string, can be assigned to a branch and will be prepended to any commit on that +branch. Distinct states can be assigned to separate branches. + +This concept is intended to aid in the following scenarios: + +* Branches are often used to identify a certain feature or fix. However, once + the branch is deleted, the only remaining identifying information is the merge + commit. States allow a specific string (e.g. the bug number) to be prepended + to each commit message automatically, which may be otherwise forgotten or + infrequent. +* During large refactorings, one may need to commit during an unstable state in + order to prevent one massive commit. However, this complicates operations + like `git bisect`. One could use states to clearly mark each commit as + unstable until the process is complete. +* The state can be used in conjunction with git-ps1 in order to clearly state + the current state of the branch. + +## Usage +```sh +$ git state foo # sets the state to "foo" +$ git state # retrieve the current state +foo +$ git state --clear # clear the state +``` + +The previous state is stored for each branch, allowing for quick switches +between states using `-` as the message (much like `cd -`): + +```sh +$ git state foo # sets the state to "foo" +$ git state bar # sets the state to "bar" +$ git state - # sets the state to "foo" +$ git state - # sets the state to "bar" +``` + +Remember - states are tied to the current branch: + +```sh +$ git state foo +$ git state +foo +$ git checkout -b newbranch +Switched to branch 'newbranch' +$ git state # no state +$ git state bar +$ git state +bar +$ git checkout master +Switched to branch 'master' +$ git state +foo +``` + +## Setup +Add the repository's `bin/` directory to your `PATH` environment variable, or +copy the script into your `PATH`. + +## Configuration +Configuration can be done via `git config`. The following options are available: + +* `state.delim.left` - String to be used for left portion of delimiter (default + '[') +* `state.delim.right` - String to be used for right portion of delimiter + (default: ']') + diff --git a/bash_completion b/bash_completion new file mode 100644 index 0000000..6ecab47 --- /dev/null +++ b/bash_completion @@ -0,0 +1,119 @@ +#!/bin/bash +# +# Provides short mappings for common Git commands +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# # + + +_git_state () +{ + _get_comp_words_by_ref -n =: cur + + case "$cur" in + --*) + __gitcomp "--clear";; + esac +} + +# override their config function to provide our own options +eval "$( echo '_prev_git_config ()'; declare -f _git_config | tail -n +2 )" +_git_config () +{ + _prev_git_config + + _get_comp_words_by_ref -n =: cur prev + + # add our configuration options + COMPREPLY=( ${COMPREPLY[@]-} state.delim.left state.delim.right ) +} + + +__git-supp_docomplete () +{ + # ignore problem commands + grep -q '^-' <<< "$1" && return + + complete -o bashdefault -o default -o nospace -F $2 $1 2>/dev/null \ + || complete -o default -o nospace -F $2 $1 +} + +__git-supp_shortmap () +{ + # only perform completion when within a git dir + __gitdir >/dev/null || return $? + + # execute the associated completion function (column two of the shortmaps + # file) + $( awk "/^$1 / { print \$2 }" <<< "$__git_supp_maps" ) +} + +__git-supp_register_alias () +{ + # ignore invalid aliases (for which we define functions to handle them + # instead) + grep -q '^-' <<< "$1" && return + + alias $1="__git-supp_shortalias $1" +} + +__git-supp_shortalias () +{ + shortcmd=$1 + shift + + # if we're not within a git dir, fall back to an actual command of this name + __gitdir >/dev/null || { + " $shortcmd $@" + return $? + } + + # execute the command + cmd="$( grep "^$shortcmd " <<< "$__git_supp_maps" | cut -d' ' -f3- )" + if [ -z "$cmd" ]; then + return + elif [ "$( grep '^|' <<< "$cmd")" ]; then + eval "$( sed 's/^|//' <<< "$cmd" ) $@" + return $? + fi + + $cmd "$@" +} + +# functions that cannot be aliased +- () { __git-supp_shortalias - "$@"; } +-- () { __git-supp_shortalias -- "$@"; } + +# load shortmaps from cwd (or provided path) and home dir (if available) +__git_supp_maps=$( + cat ${1:-./shortmaps} ~/.git-supp-shortmaps 2>/dev/null \ + | sed 's/^\([^ ]\+ [^ ]\+\) :/\1 git /' +) + +oldifs="$IFS" + +# register each shortmap +IFS=$'\n' +for line in $__git_supp_maps; do + IFS=$' ' + set -- $line + + [ -z "$1" ] && continue + + __git-supp_docomplete "$1" __git-supp_shortmap + __git-supp_register_alias "$1" +done + +IFS="$oldifs" + diff --git a/bin/git-state b/bin/git-state new file mode 100755 index 0000000..b794c4c --- /dev/null +++ b/bin/git-state @@ -0,0 +1,169 @@ +#!/bin/bash +# +# Adds "state" command to prepend state strings to commits +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# # + +gitdir=$( git rev-parse --git-dir 2>/dev/null ) +msgpath="$gitdir/COMMIT_EDITMSG_PREFIX" + +if [ -z "$gitdir" ]; then + echo "fatal: Not in a git repository" >&2 + exit 1 +fi + +branch=$( git symbolic-ref HEAD 2>/dev/null ) +branch=${branch#refs/heads/} +if [ -z "$branch" ]; then + echo "fatal: Not currently on any branch" >&2 + exit 1 +fi + + +## +# Stores current state for current branch and removes any associated stale +# states (providing one historical state for the branch) +## +_clearmsg() +{ + local bname="${branch//\//\/}" + + # remove stale branch state and mark current branch state as previous (which + # will be removed next time the state is set for this branch again) + [ -f "$msgpath" ] && \ + sed -i "/^-$bname/d;s/^$bname/-&/" "$msgpath" +} + + +## +# Retrieve the current state message for the current branch, or an alternate +# state as specified by an optional prefix +_getmsg() +{ + local pre="$1" + grep "^${pre}$branch" "$msgpath" 2>/dev/null \ + | cut -d' ' -f2- +} + + +## +# Initialize the prepare-commit-msg hook +# +# If it does not exist, one will be created. Otherwise, the actions will be +# appended to the existing hook to ensure that the process can be properly +# terminated by any existing hook. +## +_hookinit() +{ + local hookpath="$gitdir/hooks/prepare-commit-msg" + local hookflag="$gitdir/hooks/.stateinit" + + # do nothing if we've already initialized the hook + if [ -e "$hookflag" ]; then + return 0; + fi + + # if the hook does not yet exist, create it + if [ ! -f "$hookpath" ]; then + echo '#!/bin/bash' > "$hookpath" \ + && chmod +x "$hookpath" \ + || return 1 + fi + + # append our hook code to prepend the state string to each commit (if one is + # set for the current branch) + cat - >> "$hookpath" <<'EOF' + +# +# added automatically by git-state +# +branch=$( git symbolic-ref HEAD 2>/dev/null ) +branch=${branch#refs/heads/} + +msg=$( cat "$1" ) +prefix=$( git state | tr -d '\n' ) +dl=$( git config state.delim.left || echo '[' ) +dr=$( git config state.delim.right || echo ']' ) +statemsg="$dl$prefix$dr " + +# do not include statemsg if it's already found in the message +grep -qoF "$statemsg" <<< "$msg" && statemsg="" + +if [ -n "$prefix" ]; then + echo "$statemsg$msg" > "$1" +fi +EOF + + # if the append failed, we cannot continue + test $? -gt 0 && return 1 + + # prevent us from initializing again in the future + touch "$hookflag" +} + + +# parse options (with long option support) +while getopts ":-:" opt; do + case "$opt" in + -) + # parse --long options + case "$OPTARG" in + clear) + _clearmsg + exit + ;; + + *) + echo "fatal: Unknown option: $OPTARG" >&2 + exit 64 # EX_USAGE + ;; + esac;; + + ?) + echo "fatal: Unknown option: $OPTARG" >&2 + exit 64 # EX_USAGE + ;; + esac +done + +# attempt to initialize the hook +_hookinit || { + echo "fatal: Could not initialize prepare-commit-msg hook" >&2 + exit 1 +} + +# if no message was provided, simply output the current value +if [ -z "$*" ]; then + statemsg=$( _getmsg ) + + # output state message and exit with 0 status if state message is set + if [ -n "$statemsg" ]; then + echo "$statemsg" + exit + fi + + # no state message; exit with non-zero status to indicate absence + exit 1 +fi + +# the message will be, by default, the remainder of our arguments; dash will +# indicate that the previous state should be used in place of the message +msg="$@" +[ "$msg" == '-' ] && msg=$( _getmsg - ) + +# clear the previous message and set the new +_clearmsg +echo "$branch $msg" >> "$msgpath" + diff --git a/git-ps1.sh b/git-ps1.sh old mode 100644 new mode 100755 index d805a22..4b2d407 --- a/git-ps1.sh +++ b/git-ps1.sh @@ -18,12 +18,12 @@ # along with this program. If not, see . # # -BRANCH=$(git symbolic-ref HEAD 2>/dev/null \ +branch=$(git symbolic-ref HEAD 2>/dev/null \ || git rev-parse HEAD 2>/dev/null | cut -c1-10 \ ) # if no branch or hash was returned, then we're not in a repository -if [ -z "$BRANCH" ]; then +if [ -z "$branch" ]; then exit fi @@ -33,69 +33,80 @@ mkcolor() echo "\[\033[00;$1m\]" } -BRANCH=${BRANCH#refs/heads/} -GIT_STATUS=$( git status 2>/dev/null ) +branch=${branch#refs/heads/} +git_status=$( git status 2>/dev/null ) +if [ "$?" != '0' ]; then + exit +fi # colors can be overridden via the GITPS1_COLOR_* environment variables -COLOR_DEFAULT=$( mkcolor ${GITPS1_COLOR_DEFAULT:-33} ) -COLOR_FASTFWD=$( mkcolor ${GITPS1_COLOR_FASTFWD:-31} ) -COLOR_STAGED=$( mkcolor ${GITPS1_COLOR_STAGED:-32} ) -COLOR_UNTRACKED=$( mkcolor ${GITPS1_COLOR_UNTRACKED:-31} ) -COLOR_UNSTAGED=$( mkcolor ${GITPS1_COLOR_UNSTAGED:-33} ) -COLOR_AHEAD=$( mkcolor ${GITPS1_COLOR_AHEAD:-33} ) +color_default=$( mkcolor ${GITPS1_COLOR_DEFAULT:-33} ) +color_fastfwd=$( mkcolor ${GITPS1_COLOR_FASTFWD:-31} ) +color_staged=$( mkcolor ${GITPS1_COLOR_STAGED:-32} ) +color_untracked=$( mkcolor ${GITPS1_COLOR_UNTRACKED:-31} ) +color_unstaged=$( mkcolor ${GITPS1_COLOR_UNSTAGED:-33} ) +color_ahead=$( mkcolor ${GITPS1_COLOR_AHEAD:-33} ) +color_state=$( mkcolor ${GITPS1_COLOR_STATE:-35} ) +color_clr=$( mkcolor 0 ) # indicators may be overridden via the GITPS1_IND_* environment vars; set to # '0' to disable -IND_STAGED=${GITPS1_IND_STAGED:-*} -IND_UNSTAGED=${GITPS1_IND_UNSTAGED:-*} -IND_UNTRACKED=${GITPS1_IND_UNTRACKED:-*} -IND_AHEAD=${GITPS1_IND_AHEAD:-@} -IND_AHEAD_COUNT=${GITPS1_IND_AHEAD_COUNT:-@} +ind_staged=${GITPS1_IND_STAGED:-*} +ind_unstaged=${GITPS1_IND_UNSTAGED:-*} +ind_untracked=${GITPS1_IND_UNTRACKED:-*} +ind_ahead=${GITPS1_IND_AHEAD:-@} +ind_ahead_count=${GITPS1_IND_AHEAD_COUNT:-@} +ind_state=${GITPS1_IND_STATE:-1} -STATUS='' -COLOR=$COLOR_DEFAULT +statusmsg='' +statemsg='' +color=$color_default # uncommited files -if [ "$IND_UNSTAGED" != '0' ]; then - if [ "$( echo $GIT_STATUS | grep 'Changed\|uncommitted' )" ]; then - STATUS="${STATUS}${COLOR_UNSTAGED}${IND_UNSTAGED}" - fi +if [ "$ind_unstaged" != '0' ]; then + git diff --no-ext-diff --quiet --exit-code 2>/dev/null || \ + statusmsg="${statusmsg}${color_unstaged}${ind_unstaged}" fi # not on branch/behind origin -if [ "$( echo $GIT_STATUS | grep 'Not currently on\|is behind' )" ]; then - COLOR=$COLOR_FASTFWD +if [ "$( echo $git_status | grep 'Not currently on\|is behind' )" ]; then + color=$color_fastfwd fi # staged -if [ "$IND_STAGED" != '0' ]; then - if [ "$( echo $GIT_STATUS | grep 'to be committed' )" ]; then - STATUS="${STATUS}${COLOR_STAGED}${IND_STAGED}" +if [ "$ind_staged" != '0' ]; then + if [ "$( echo $git_status | grep 'to be committed' )" ]; then + statusmsg="${statusmsg}${color_staged}${ind_staged}" fi fi # untracked -if [ "$IND_UNTRACKED" != '0' ]; then - if [ "$( echo $GIT_STATUS | grep 'Untracked' )" ]; then - STATUS="${STATUS}${COLOR_UNTRACKED}${IND_UNTRACKED}" +if [ "$ind_untracked" != '0' ]; then + if [ -n "$( git ls-files --others --exclude-standard 2>/dev/null )" ]; then + statusmsg="${statusmsg}${color_untracked}${ind_untracked}" fi fi # ahead of tracking -if [ "$IND_AHEAD" != '0' ]; then - if [ "$( echo $GIT_STATUS | grep 'is ahead' )" ]; then - STATUS="${STATUS}${COLOR_AHEAD}${IND_AHEAD}" +if [ "$ind_ahead" != '0' ]; then + grep -q 'is ahead' <<< "$git_status" && { + statusmsg="${statusmsg}${color_ahead}${ind_ahead}" # append count? - if [ "$IND_AHEAD_COUNT" != '0' ]; then - AHEAD_COUNT=$( echo $GIT_STATUS \ + if [ "$ind_ahead_count" != '0' ]; then + ahead_count=$( echo $git_status \ | grep -o 'by [0-9]\+ commits\?' \ | cut -d' ' -f2 \ ) - STATUS="${STATUS}${AHEAD_COUNT}" + statusmsg="${statusmsg}${ahead_count}" fi - fi + } +fi + +# state message +if [ "$ind_state" != '0' ]; then + statemsg=$( git state 2>/dev/null ) && state=" $color_state($statemsg)" fi # output the status string -echo " $COLOR[${BRANCH}${STATUS}${COLOR}]" +echo "$color[${branch}${statusmsg}${color}]$state$color_clr " diff --git a/shortmaps b/shortmaps new file mode 100644 index 0000000..5e27b01 --- /dev/null +++ b/shortmaps @@ -0,0 +1,25 @@ +a _git_add :add +A _git_add :add -A +B _git_bisect :bisect +Bs _git_bisect :bisect start +Bg _git_bisect :bisect good +Bb _git_bisect :bisect bad +Br _git_bisect :bisect reset +c _git_commit :commit +C _git_commit :commit -am +co _git_checkout :checkout +d _git_diff :diff +f _git_fetch :fetch +m _git_merge :merge +p _git_push :push +P _git_pull :pull +R __git_rebase :rebase +Ri _git_rebase :rebase --interactive +Ra _git_rebase :rebase --abort +Rc _git_rebase :rebase --continue +s : :status +S _git_stash :stash +t : tig +T _git_tag :tag +- : :checkout - +-- : |cd "$( git rev-parse --git-dir 2>/dev/null )/../"