Skip to content

Instantly share code, notes, and snippets.

Created February 14, 2016 18:24
Provide git repository info in a configurable format
# # Provide git repository info in a configurable format
#
# All information is collected by a single git command and parsed in pure zsh
# with no external tools (sed, grep, cut, etc).
#
#
# ## What it looks like
#
# #1+2-3↪4&5 ↑6↓7 ⚡8 master
# | | | | | | | | |
# | | | | | | | | `- Current branch, tag or hash
# | | | | | | | `----- Conflicts
# | | | | | | |
# | | | | | | `-------- Commits behind origin
# | | | | | `---------- Commits ahead of origin
# | | | | |
# | | | | `------------- Untracked files
# | | | `--------------- Files renamed
# | | `----------------- Files deleted
# | `------------------- Files added
# `--------------------- Files modified
#
#
# ## Quickstart
#
# Add the following lines to your ~/.zshrc:
#
# source path/to/zsh-git-info.zsh
# setopt -o prompt_subst # Enable command substitution in (R)PROMPT
# RPROMPT='$(zsh-git-info)' # Show git info in right prompt
# autoload -Uz add-zsh-hook
# add-zsh-hook precmd zsh-git-info-update # Update GIT_* variables before new prompt
#
# You also might want to set ZLE_RPROMPT_INDENT=0 to remove the annoying
# single space on the left of RPROMPT.
#
#
# ## Configuration
#
# The function zsh-git-info-update sets the following environment variables:
#
# * GIT_BRANCH
# * GIT_AHEAD
# * GIT_BEHIND
# * GIT_CONFLICTS
# * GIT_ADDED
# * GIT_RENAMED
# * GIT_DELETED
# * GIT_MODIFIED
# * GIT_UNTRACKED
#
# The function zsh-git-info assembles the environment variables above into a
# string with these sections:
#
# [files] [divergence] [conflicts] [branch]
#
# Sections are separated by the ZSH_GIT_INFO_SECTION_SEPARATOR environment
# variable (default: " "). Each section item is separated by
# ZSH_GIT_INFO_SEPARATOR (default: "").
#
# Colors and icons can be configured by setting ZSH_GIT_INFO_X_PREFIX and
# ZSH_GIT_INFO_X_SUFFIX, where "X" is one of "BRANCH", "AHEAD", "BEHIND", etc.
typeset -g ZSH_GIT_INFO_SEPARATOR="${ZSH_GIT_INFO_SEPARATOR=}"
typeset -g ZSH_GIT_INFO_SECTION_SEPARATOR="${ZSH_GIT_INFO_SECTION_SEPARATOR= }"
typeset -g ZSH_GIT_INFO_BRANCH_PREFIX="${ZSH_GIT_INFO_BRANCH_PREFIX=%{%b%F{blue\}%\}}"
typeset -g ZSH_GIT_INFO_MODIFIED_PREFIX="${ZSH_GIT_INFO_MODIFIED_PREFIX=%{%F{yellow\}%\}#}"
typeset -g ZSH_GIT_INFO_ADDED_PREFIX="${ZSH_GIT_INFO_ADDED_PREFIX=%{%F{yellow\}%\}+}"
typeset -g ZSH_GIT_INFO_DELETED_PREFIX="${ZSH_GIT_INFO_DELETED_PREFIX=%{%F{yellow\}%\}-}"
typeset -g ZSH_GIT_INFO_RENAMED_PREFIX="${ZSH_GIT_INFO_RENAMED_PREFIX=%{%F{yellow\}%\}↪}"
typeset -g ZSH_GIT_INFO_UNTRACKED_PREFIX="${ZSH_GIT_INFO_UNTRACKED_PREFIX=%{%F{yellow\}%\}&}"
typeset -g ZSH_GIT_INFO_AHEAD_PREFIX="${ZSH_GIT_INFO_AHEAD_PREFIX=%{%F{cyan\}%\}↑}"
typeset -g ZSH_GIT_INFO_BEHIND_PREFIX="${ZSH_GIT_INFO_BEHIND_PREFIX=%{%F{cyan\}%\}↓}"
typeset -g ZSH_GIT_INFO_CONFLICTS_PREFIX="${ZSH_GIT_INFO_CONFLICTS_PREFIX=%{%F{red\}%B%\}⚡}"
typeset -g ZSH_GIT_INFO_BRANCH_SUFFIX="${ZSH_GIT_INFO_BRANCH_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_MODIFIED_SUFFIX="${ZSH_GIT_INFO_MODIFIED_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_ADDED_SUFFIX="${ZSH_GIT_INFO_ADDED_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_DELETED_SUFFIX="${ZSH_GIT_INFO_DELETED_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_RENAMED_SUFFIX="${ZSH_GIT_INFO_RENAMED_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_UNTRACKED_SUFFIX="${ZSH_GIT_INFO_UNTRACKED_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_AHEAD_SUFFIX="${ZSH_GIT_INFO_AHEAD_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_BEHIND_SUFFIX="${ZSH_GIT_INFO_BEHIND_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g ZSH_GIT_INFO_CONFLICTS_SUFFIX="${ZSH_GIT_INFO_CONFLICTS_SUFFIX=%{%f%k%b%u%s%\}}"
typeset -g GIT_BRANCH='' GIT_AHEAD=0 GIT_BEHIND=0 GIT_CONFLICTS=0 \
GIT_ADDED=0 GIT_RENAMED=0 GIT_DELETED=0 GIT_MODIFIED=0 GIT_UNTRACKED=0
function zsh-git-info-update {
emulate -RL zsh
GIT_BRANCH='' GIT_AHEAD=0 GIT_BEHIND=0 GIT_CONFLICTS=0
GIT_ADDED=0 GIT_RENAMED=0 GIT_DELETED=0 GIT_MODIFIED=0 GIT_UNTRACKED=0
local -a lines
lines=( ${(f)"$(git status --porcelain --branch --ignore-submodules 2>/dev/null)"} )
if [ ${#lines} -gt 0 ]; then
# Parse name of current branch. If HEAD is detached, look for a tag and fall
# back to commit hash.
local branch="$lines[1]"
if [[ "$branch" = *'(no branch)' ]]; then
# Detached HEAD state; use tag or abbreviated hash
log="$(git log -1 --format='%h %D')"
if [[ "$log" =~ 'tag: (.*)$' ]]; then
GIT_BRANCH="$match[1]"
else
GIT_BRANCH="${log%[a-f0-9]*}"
fi
else
branch="${branch:3}" # Remove '## ' from start
branch="${branch%...*}" # Remove '...' and everything after that
GIT_BRANCH="$branch"
fi
# Number of commits ahead/behind origin
local div="$lines[1]"
if [[ "$div" = *'['* ]]; then
local MATCH MBEGIN MEND match mbegin mend
div="${div#*\[}" # Remove '[' and anything before it
div="${div%\]*}" # Remove ']' and anything after it
[[ "$div" =~ 'ahead[[:space:]]*([[:digit:]]*)' ]] && GIT_AHEAD="$match[1]"
[[ "$div" =~ 'behind[[:space:]]*([[:digit:]]*)' ]] && GIT_BEHIND="$match[1]"
fi
# Count stuff in staging area and working directory
local line
for line in ${lines:1}; do
if [[ "${line[1,2]}" == '??' ]]; then
let 'GIT_UNTRACKED += 1'
else
if [[ "${line[2]}" == 'M' ]]; then
let 'GIT_MODIFIED += 1'
elif [[ "${line[1]}" == 'U' ]]; then
let 'GIT_CONFLICTS += 1'
elif [[ "${line[1]}" == 'D' ]]; then
let 'GIT_DELETED += 1'
elif [[ "${line[1]}" == 'R' ]]; then
let 'GIT_RENAMED += 1'
elif [[ "${line[1]}" == 'A' ]]; then
let 'GIT_ADDED += 1'
fi
fi
done
fi
}
# Concatenate everything into the final info string
function zsh-git-info {
if [[ "$GIT_BRANCH" != '' ]]; then
emulate -RL zsh
local -a files
[ $GIT_MODIFIED -gt 0 ] && \
files+=( "$ZSH_GIT_INFO_MODIFIED_PREFIX$GIT_MODIFIED$ZSH_GIT_INFO_MODIFIED_SUFFIX" )
[ $GIT_ADDED -gt 0 ] && \
files+=( "$ZSH_GIT_INFO_ADDED_PREFIX$GIT_ADDED$ZSH_GIT_INFO_ADDED_SUFFIX" )
[ $GIT_DELETED -gt 0 ] && \
files+=( "$ZSH_GIT_INFO_DELETED_PREFIX$GIT_DELETED$ZSH_GIT_INFO_DELETED_SUFFIX" )
[ $GIT_RENAMED -gt 0 ] && \
files+=( "$ZSH_GIT_INFO_RENAMED_PREFIX$GIT_RENAMED$ZSH_GIT_INFO_RENAMED_SUFFIX" )
[ $GIT_UNTRACKED -gt 0 ] && \
files+=( "$ZSH_GIT_INFO_UNTRACKED_PREFIX$GIT_UNTRACKED$ZSH_GIT_INFO_UNTRACKED_SUFFIX" )
local -a div
[ $GIT_AHEAD -gt 0 ] && \
div+=( "$ZSH_GIT_INFO_AHEAD_PREFIX$GIT_AHEAD$ZSH_GIT_INFO_AHEAD_SUFFIX" )
[ $GIT_BEHIND -gt 0 ] && \
div+=( "$ZSH_GIT_INFO_BEHIND_PREFIX$GIT_BEHIND$ZSH_GIT_INFO_BEHIND_SUFFIX" )
local conflicts
[ $GIT_CONFLICTS -gt 0 ] && \
conflicts="$ZSH_GIT_INFO_CONFLICTS_PREFIX$GIT_CONFLICTS$ZSH_GIT_INFO_CONFLICTS_SUFFIX"
local -a final
[ ${#files} -gt 0 ] && final+=( "${(ej:$ZSH_GIT_INFO_SEPARATOR:)files}" )
[ ${#div} -gt 0 ] && final+=( "${(ej:$ZSH_GIT_INFO_SEPARATOR:)div}" )
[ -n "$conflicts" ] && final+=( "$conflicts" )
final+=( "$ZSH_GIT_INFO_BRANCH_PREFIX$GIT_BRANCH$ZSH_GIT_INFO_BRANCH_SUFFIX" )
echo -n "${(pj:$ZSH_GIT_INFO_SECTION_SEPARATOR:)final}"
fi
}
# Test parsers
if [[ "$1" == 'test' ]]; then
local -a testcase
function git { echo "${(pj:\n:)testcase}" }
function assertEqual {
if [[ "$1" != "$2" ]]; then
echo "Test failed: '$1' != '$2'" >&2
exit 1
fi
}
function test_branch_and_divergence {
local testcase=( "$1" )
local expected_branch="$2"
local expected_ahead="$3"
local expected_behind="$4"
zsh-git-info-update
echo "Test case: $testcase[1]" >&2
assertEqual "$GIT_BRANCH" "$expected_branch"
assertEqual "$GIT_AHEAD" "$expected_ahead"
assertEqual "$GIT_BEHIND" "$expected_behind"
}
test_branch_and_divergence '## master' master 0 0
test_branch_and_divergence '## ding/dong...origin/dev' ding/dong 0 0
test_branch_and_divergence '## ma.ster...origin/feature [ahead 27]' ma.ster 27 0
test_branch_and_divergence '## dev...origin/master [behind 1]' dev 0 1
test_branch_and_divergence '## x...origin/x [ahead 2, behind 13]' x 2 13
# Test other counters
testcase=( '## master...origin/master [ahead 4, behind 2]'
'?? untracked_file1'
' M modified_file'
'A added_file'
'D deleted_file'
'R renamed_file'
'?? untracked_file2'
'U conflicted_file' )
echo -e "Test case: ${(pj:\n :)testcase}" >&2
zsh-git-info-update
assertEqual "$GIT_UNTRACKED" '2'
assertEqual "$GIT_ADDED" '1'
assertEqual "$GIT_DELETED" '1'
assertEqual "$GIT_RENAMED" '1'
assertEqual "$GIT_MODIFIED" '1'
assertEqual "$GIT_CONFLICTS" '1'
echo 'All tests passed.' >&2
elif [[ "$1" == 'profile' ]]; then
local times="${2=1000}"
time (for x in $(seq 1 $times); do
zsh-git-info-update
zsh-git-info >/dev/null
done)
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment