Files
bash-git-prompt/bash-git-prompt

297 lines
8.8 KiB
Plaintext

# SPDX-License-Identifier: MIT
# Author : Allan Christensen
# First Created : 24062025 (DD-MM-YYYY)
# Description : Bash Git prompt for Linux
# License : MIT License
# Default to theme 1 if not set
GIT_PROMPT_THEME="${GIT_PROMPT_THEME:-1}"
# -------------------------------
# THEME ICON DEFINITIONS
# -------------------------------
set_git_prompt_theme_icons() {
case "$GIT_PROMPT_THEME" in
2|3)
BRANCH_ICON=" "
STAGED_ICON=""
CONFLICT_ICON="✘"
CHANGED_ICON="±"
UNTRACKED_ICON=""
STASHED_ICON=""
AHEAD_ICON=""
BEHIND_ICON=""
NO_REMOTE_ICON=""
CLEAN_ICON="✔"
DIRTY_ICON="✘"
;;
4)
BRANCH_ICON="⎇ "
STAGED_ICON="o"
CONFLICT_ICON="!"
CHANGED_ICON="±"
UNTRACKED_ICON="…"
STASHED_ICON="☰"
AHEAD_ICON="⇡"
BEHIND_ICON="⇣"
NO_REMOTE_ICON="-"
CLEAN_ICON="✓"
DIRTY_ICON="✗"
;;
5)
BRANCH_ICON=":: "
STAGED_ICON="+"
CONFLICT_ICON="x"
CHANGED_ICON="*"
UNTRACKED_ICON="?"
STASHED_ICON="s"
AHEAD_ICON="a"
BEHIND_ICON="b"
NO_REMOTE_ICON="no"
CLEAN_ICON="ok"
DIRTY_ICON="!"
;;
*)
BRANCH_ICON=" "
STAGED_ICON=""
CONFLICT_ICON="✘"
CHANGED_ICON="±"
UNTRACKED_ICON=""
STASHED_ICON=""
AHEAD_ICON=""
BEHIND_ICON=""
NO_REMOTE_ICON=""
CLEAN_ICON="✔"
DIRTY_ICON="✘"
;;
esac
}
set_git_prompt_theme_icons
# -------------------------------
# CACHING SETUP
# -------------------------------
GIT_CACHE_FILE="/tmp/git_prompt_cache.$$"
# Collected state variables
GIT_STATE_HEAD=""
GIT_STATE_STATUS=""
GIT_STATE_STASH_LIST=""
GIT_STATE_UNTRACKED=""
GIT_STATE_FINGERPRINT=""
# Directories to skip for speed (Option 2A)
HEAVY_DIRS=(
node_modules
vendor
dist
build
cache
uploads
public/uploads
tmp
.terraform
.cache
)
# -------------------------------
# SMART STATE COLLECTION (Option 1B + 2A)
# -------------------------------
git_collect_state() {
# HEAD
GIT_STATE_HEAD=$(git rev-parse HEAD 2>/dev/null) || return 1
# Full status (branch + staged/modified)
GIT_STATE_STATUS=$(GIT_OPTIONAL_LOCKS=0 git status --porcelain --branch 2>/dev/null) || return 1
# Stash list
GIT_STATE_STASH_LIST=$(GIT_OPTIONAL_LOCKS=0 git stash list 2>/dev/null || true)
# Smart untracked detection (Option 1B)
# Much faster than git status because it only lists untracked, not modified
local untracked_raw
untracked_raw=$(git ls-files --others --exclude-standard 2>/dev/null || true)
# Apply directory skipping (Option 2A)
GIT_STATE_UNTRACKED=""
while IFS= read -r f; do
skip=0
for d in "${HEAVY_DIRS[@]}"; do
[[ "$f" == "$d/"* ]] && skip=1 && break
done
(( skip == 0 )) && GIT_STATE_UNTRACKED+="$f"$'\n'
done <<< "$untracked_raw"
# Build fingerprint
if command -v sha1sum >/dev/null; then
GIT_STATE_FINGERPRINT=$(
printf '%s\n%s\n%s\n%s\n%s\n' \
"$GIT_STATE_HEAD" \
"$GIT_STATE_STATUS" \
"$GIT_STATE_STASH_LIST" \
"$GIT_STATE_UNTRACKED" \
"$GIT_PROMPT_THEME" \
| sha1sum | awk '{print $1}'
)
else
GIT_STATE_FINGERPRINT=$(printf '%s\n%s\n%s\n%s\n' "$GIT_STATE_HEAD" "$GIT_STATE_STATUS" "$GIT_STATE_STASH_LIST" "$GIT_STATE_UNTRACKED")
fi
return 0
}
# -------------------------------
# RENDER LOGIC
# -------------------------------
actual_git_prompt_info_logic() {
local branch
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "(detached)")
local status="$GIT_STATE_STATUS"
local stash_list="$GIT_STATE_STASH_LIST"
local staged conflicts changed untracked stashed is_clean has_remote
staged=$(grep -cE '^[AMDR] ' <<<"$status")
conflicts=$(grep -cE '^UU ' <<<"$status")
changed=$(grep -cE '^.[MD] ' <<<"$status")
untracked=$(grep -c . <<<"$GIT_STATE_UNTRACKED")
stashed=$(grep -c . <<<"$stash_list")
# Parse branch/remote info
local first_line=${status%%$'\n'*}
local ahead=0 behind=0
has_remote=0
if [[ "$first_line" == "## "* ]]; then
[[ "$first_line" == *"..."* ]] && has_remote=1
[[ "$first_line" =~ ahead\ ([0-9]+) ]] && ahead=${BASH_REMATCH[1]}
[[ "$first_line" =~ behind\ ([0-9]+) ]] && behind=${BASH_REMATCH[1]}
fi
is_clean=0
(( staged == 0 && conflicts == 0 && changed == 0 && untracked == 0 )) && is_clean=1
# -------------------------
# (Themes kept as-is)
# -------------------------
if [[ "$GIT_PROMPT_THEME" == "2" ]]; then
printf "\[\e[30;44m\]"
printf "\[\e[97;44m\] %s%s " "$BRANCH_ICON" "$branch"
((staged > 0)) && printf " %s%d" "$STAGED_ICON" "$staged"
((conflicts > 0)) && printf " %s%d" "$CONFLICT_ICON" "$conflicts"
((changed > 0)) && printf " %s%d" "$CHANGED_ICON" "$changed"
((untracked > 0)) && printf " %s%d" "$UNTRACKED_ICON" "$untracked"
((stashed > 0)) && printf " %s%d" "$STASHED_ICON" "$stashed"
((ahead > 0)) && printf " %s%d" "$AHEAD_ICON" "$ahead"
((behind > 0)) && printf " %s%d" "$BEHIND_ICON" "$behind"
((has_remote == 0)) && printf " %s" "$NO_REMOTE_ICON"
((is_clean)) && printf " %s" "$CLEAN_ICON" || printf " %s" "$DIRTY_ICON"
printf "\[\e[34;107m\]"
printf "\[\e[30;107m\] \w"
printf "\[\e[97;49m\]"
printf "\[\e[0m\]"
elif [[ "$GIT_PROMPT_THEME" == "3" ]]; then
printf "\n\[\e[0;37m\]┌──[\e[0m"
printf "\e[38;5;117m%s%s\e[0m" "$BRANCH_ICON" "$branch"
((staged > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$STAGED_ICON" "$staged"
((conflicts > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$CONFLICT_ICON" "$conflicts"
((changed > 0)) && printf " \e[38;5;69m%s%d\e[0m" "$CHANGED_ICON" "$changed"
((untracked > 0)) && printf " \e[38;5;41m%s%d\e[0m" "$UNTRACKED_ICON" "$untracked"
((stashed > 0)) && printf " \e[38;5;226m%s%d\e[0m" "$STASHED_ICON" "$stashed"
((ahead > 0)) && printf " \e[0;37m%s%d\e[0m" "$AHEAD_ICON" "$ahead"
((behind > 0)) && printf " \e[0;37m%s%d\e[0m" "$BEHIND_ICON" "$behind"
((has_remote == 0)) && printf " \e[38;5;250m%s\e[0m" "$NO_REMOTE_ICON"
((is_clean)) && printf " \e[0;32m%s\e[0m" "$CLEAN_ICON" || printf " \e[38;5;196m%s\e[0m" "$DIRTY_ICON"
printf "\e[0;37m]\e[0m \[\e[38;5;178m\]\w\[\e[0m\]"
printf "\n\[\e[0;37m\]└──\[\e[0m\]"
else
printf "\e[0;37m[\e[0m"
printf "\e[38;5;117m%s%s\e[0m" "$BRANCH_ICON" "$branch"
((staged > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$STAGED_ICON" "$staged"
((conflicts > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$CONFLICT_ICON" "$conflicts"
((changed > 0)) && printf " \e[38;5;69m%s%d\e[0m" "$CHANGED_ICON" "$changed"
((untracked > 0)) && printf " \e[38;5;41m%s%d\e[0m" "$UNTRACKED_ICON" "$untracked"
((stashed > 0)) && printf " \e[38;5;226m%s%d\e[0m" "$STASHED_ICON" "$stashed"
((ahead > 0)) && printf " \e[0;37m%s%d\e[0m" "$AHEAD_ICON" "$ahead"
((behind > 0)) && printf " \e[0;37m%s%d\e[0m" "$BEHIND_ICON" "$behind"
((has_remote == 0)) && printf " \e[38;5;250m%s\e[0m" "$NO_REMOTE_ICON"
((is_clean)) && printf " \e[0;32m%s\e[0m" "$CLEAN_ICON" || printf " \e[38;5;196m%s\e[0m" "$DIRTY_ICON"
printf "\e[0;37m]\e[0m"
fi
}
# -------------------------------
# MAIN PROMPT LOGIC
# -------------------------------
git_prompt_info() {
git rev-parse --is-inside-work-tree &>/dev/null || return
git_collect_state || return
local fingerprint="$GIT_STATE_FINGERPRINT"
if [[ -f "$GIT_CACHE_FILE" ]]; then
local cached_fp
IFS= read -r cached_fp < "$GIT_CACHE_FILE"
if [[ -n "$cached_fp" && "$cached_fp" == "$fingerprint" ]]; then
sed '1d' "$GIT_CACHE_FILE"
return
fi
fi
local output
output=$(actual_git_prompt_info_logic) || return
{
echo "$fingerprint"
echo "$output"
} > "$GIT_CACHE_FILE"
echo "$output"
}
update_git_prompt() {
local PROMPT_CHAR='$'
[[ $EUID -eq 0 ]] && PROMPT_CHAR='#'
GIT_PS1="$(git_prompt_info)"
#
# PS1 custom for all themes inside a git repository
#
local PS1_CUSTOM='\[\e[0;32m\]\u@\h\[\e[0m\]'
if [[ -n "$GIT_PS1" ]]; then
if [[ "$GIT_PROMPT_THEME" == "3" ]]; then
PS1="${PS1_CUSTOM} ${GIT_PS1} ${PROMPT_CHAR} "
elif [[ "$GIT_PROMPT_THEME" == "2" ]]; then
PS1="${PS1_CUSTOM} ${GIT_PS1}\[\e[0m\] ${PROMPT_CHAR} "
else
PS1="${PS1_CUSTOM} \[${GIT_PS1}\] \[\e[38;5;178m\]\w\[\e[0m\] ${PROMPT_CHAR} "
fi
else
#
# PS1 cusom for all themes outside a git repository
#
PS1='\[\e[0;32m\]\u@\h\[\e[0m\]:\[\e[38;5;178m\]\w\[\e[0m\] '"${PROMPT_CHAR} "
fi
}
PROMPT_COMMAND=update_git_prompt
gpchange() {
local theme="${1:-}"
if [[ "$theme" =~ ^[1-5]$ ]]; then
export GIT_PROMPT_THEME="$theme"
set_git_prompt_theme_icons
update_git_prompt
echo "Switched to Git prompt theme $theme"
else
echo "Usage: gpchange <1-5>"
fi
}