diff --git a/bash-git-prompt b/bash-git-prompt index 7e01dc2..b36ad8a 100644 --- a/bash-git-prompt +++ b/bash-git-prompt @@ -1,12 +1,14 @@ # Author : Allan Christensen # First Created : 24062025 (DD-MM-YYYY) -# Description : Provides a bash git prompt for Ubuntu -# License : MIT License (see LICENSE file for details) +# Description : Optimized Bash Git Prompt with Smart Fingerprint Caching +# License : MIT License # Default to theme 1 if not set GIT_PROMPT_THEME="${GIT_PROMPT_THEME:-1}" -# Define theme icons +# ------------------------------- +# THEME ICON DEFINITIONS +# ------------------------------- set_git_prompt_theme_icons() { case "$GIT_PROMPT_THEME" in 2|3) @@ -65,46 +67,82 @@ set_git_prompt_theme_icons() { } set_git_prompt_theme_icons -# Caching mechanism (per-shell cache file) +# ------------------------------- +# CACHING SETUP +# ------------------------------- GIT_CACHE_FILE="/tmp/git_prompt_cache.$$" -# Internal state used for caching +# Collected state variables GIT_STATE_HEAD="" GIT_STATE_STATUS="" GIT_STATE_STASH_LIST="" +GIT_STATE_UNTRACKED="" GIT_STATE_FINGERPRINT="" -# Collect git state and build a fingerprint of the working tree +# 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 (commit hash) + + # HEAD GIT_STATE_HEAD=$(git rev-parse HEAD 2>/dev/null) || return 1 - # Full status including branch / ahead / behind + # Full status (branch + staged/modified) GIT_STATE_STATUS=$(GIT_OPTIONAL_LOCKS=0 git status --porcelain --branch 2>/dev/null) || return 1 - # Stash list (for stash count) + # Stash list GIT_STATE_STASH_LIST=$(GIT_OPTIONAL_LOCKS=0 git stash list 2>/dev/null || true) - # Build fingerprint from head, status and stash list - if command -v sha1sum >/dev/null 2>&1; then + # 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' \ + printf '%s\n%s\n%s\n%s\n' \ "$GIT_STATE_HEAD" \ "$GIT_STATE_STATUS" \ "$GIT_STATE_STASH_LIST" \ - | sha1sum 2>/dev/null | awk '{print $1}' + "$GIT_STATE_UNTRACKED" \ + | sha1sum | awk '{print $1}' ) else - # Fallback if sha1sum is missing (very rare on Ubuntu) - GIT_STATE_FINGERPRINT=$(printf '%s\n%s\n%s\n' "$GIT_STATE_HEAD" "$GIT_STATE_STATUS" "$GIT_STATE_STASH_LIST") + 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() { - # Require collected state - [[ -n "$GIT_STATE_STATUS" ]] || return local branch branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "(detached)") @@ -116,44 +154,36 @@ actual_git_prompt_info_logic() { staged=$(grep -cE '^[AMDR] ' <<<"$status") conflicts=$(grep -cE '^UU ' <<<"$status") changed=$(grep -cE '^.[MD] ' <<<"$status") - untracked=$(grep -cE '^\?\? ' <<<"$status") + untracked=$(grep -c . <<<"$GIT_STATE_UNTRACKED") stashed=$(grep -c . <<<"$stash_list") - # Parse the first status line to detect remote and ahead/behind - local first_line - first_line=${status%%$'\n'*} - - local ahead=0 - local behind=0 + # Parse branch/remote info + local first_line=${status%%$'\n'*} + local ahead=0 behind=0 has_remote=0 if [[ "$first_line" == "## "* ]]; then - # If there's a tracking branch, the line will contain "..." [[ "$first_line" == *"..."* ]] && has_remote=1 - - if [[ "$first_line" =~ ahead\ ([0-9]+) ]]; then - ahead=${BASH_REMATCH[1]} - fi - if [[ "$first_line" =~ behind\ ([0-9]+) ]]; then - behind=${BASH_REMATCH[1]} - fi + [[ "$first_line" =~ ahead\ ([0-9]+) ]] && ahead=${BASH_REMATCH[1]} + [[ "$first_line" =~ behind\ ([0-9]+) ]] && behind=${BASH_REMATCH[1]} fi is_clean=0 - if (( staged == 0 && conflicts == 0 && changed == 0 && untracked == 0 )); then - is_clean=1 - fi + (( 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" + ((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\]" @@ -163,33 +193,36 @@ actual_git_prompt_info_logic() { 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" + ((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" + ((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 @@ -201,13 +234,11 @@ git_prompt_info() { local cached_fp IFS= read -r cached_fp < "$GIT_CACHE_FILE" if [[ -n "$cached_fp" && "$cached_fp" == "$fingerprint" ]]; then - # Fingerprint unchanged → reuse cached rendered prompt sed '1d' "$GIT_CACHE_FILE" return fi fi - # Recompute prompt and refresh cache local output output=$(actual_git_prompt_info_logic) || return @@ -224,6 +255,7 @@ update_git_prompt() { [[ $EUID -eq 0 ]] && PROMPT_CHAR='#' GIT_PS1="$(git_prompt_info)" + if [[ -n "$GIT_PS1" ]]; then if [[ "$GIT_PROMPT_THEME" == "3" ]]; then PS1="${GIT_PS1} ${PROMPT_CHAR} "