Created
February 14, 2016 18:24
Provide git repository info in a configurable format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# # 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