From cee3f0443ff992a8f64f13d66da4867d1db836de Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Sun, 19 Apr 2026 18:49:35 -0400 Subject: Bulk rewrite of git-sonar script Recent changes to git-sonar have made a top-to-bottom redesign and reimplementation more approachable. This patch is a full rewrite of git-sonar, with a focus on simplicity and further breaking ties with legacy git-radar stuff. A high-level itemization of changes is below: - Removed shell selector options - Removed '-f' short option (use '--fetch') - Removed support for rc configuration files - Removed remote commits "master symbol" - Removed context-specific color reset codes (now always \e[0m) - Removed support for git-config keys (determine_default_branch) This is now determined automatically. - Added feature opt-out options - Added PROMPT_COLOR configuration variable - Configurable icons/colors are globally merged into single variables (the user must insert \001 and \002) - Configuration variables renamed from GIT_RADAR_ to GIT_SONAR_ - Most configuration variables are renamed - Configuration for remote and local commits are merged together - Terminology "changes conflicted" is now "changes unmerged" - %{changes} formatter is replaced with individual stage elements - Unmerged status now shows A/D instead of us-vs-them Signed-off-by: Matt Hunter --- git-sonar | 653 +++++++++++++++++++++----------------------------------------- 1 file changed, 216 insertions(+), 437 deletions(-) diff --git a/git-sonar b/git-sonar index 25772e8..4978818 100755 --- a/git-sonar +++ b/git-sonar @@ -1,481 +1,260 @@ #!/bin/sh -# -# git-sonar -# -# A heads up display for git -GIT_SONAR_VERSION="v0.8.1" +GIT_SONAR_VERSION="v0.9.0-dev" -prepare_colors() { - PRINT_F_OPTION="" - - COLOR_REMOTE_AHEAD="\001${GIT_RADAR_COLOR_REMOTE_AHEAD:-"\\033[1;32m"}\002" - COLOR_REMOTE_BEHIND="\001${GIT_RADAR_COLOR_REMOTE_BEHIND:-"\\033[1;31m"}\002" - COLOR_REMOTE_DIVERGED="\001${GIT_RADAR_COLOR_REMOTE_DIVERGED:-"\\033[1;33m"}\002" - COLOR_REMOTE_NOT_UPSTREAM="\001${GIT_RADAR_COLOR_REMOTE_NOT_UPSTREAM:-"\\033[1;33m"}\002" - - COLOR_LOCAL_AHEAD="\001${GIT_RADAR_COLOR_LOCAL_AHEAD:-"\\033[1;32m"}\002" - COLOR_LOCAL_BEHIND="\001${GIT_RADAR_COLOR_LOCAL_BEHIND:-"\\033[1;31m"}\002" - COLOR_LOCAL_DIVERGED="\001${GIT_RADAR_COLOR_LOCAL_DIVERGED:-"\\033[1;33m"}\002" - - COLOR_CHANGES_STAGED="\001${GIT_RADAR_COLOR_CHANGES_STAGED:-"\\033[1;32m"}\002" - COLOR_CHANGES_UNSTAGED="\001${GIT_RADAR_COLOR_CHANGES_UNSTAGED:-"\\033[1;31m"}\002" - COLOR_CHANGES_CONFLICTED="\001${GIT_RADAR_COLOR_CHANGES_CONFLICTED:-"\\033[1;33m"}\002" - COLOR_CHANGES_UNTRACKED="\001${GIT_RADAR_COLOR_CHANGES_UNTRACKED:-"\\033[1;37m"}\002" - - COLOR_ALERT="\001${GIT_RADAR_COLOR_ALERT:-"\\033[1;33m"}\002" - - COLOR_STASH="\001${GIT_RADAR_COLOR_STASH:-"\\033[1;33m"}\002" - - COLOR_BRANCH="\001${GIT_RADAR_COLOR_BRANCH:-"\\033[0m"}\002" - MASTER_SYMBOL="${GIT_RADAR_MASTER_SYMBOL:-""}" # \\x01\\033[0m\\x02\\xF0\\x9D\\x98\\xAE\\x01\\033[0m\\x02 - - PROMPT_FORMAT="${GIT_RADAR_FORMAT:-" \\001\\033[1;30m\\002git:(\\001\\033[0m\\002%{alert}%{remote: }%{branch}%{ :local}\\001\\033[1;30m\\002)\\001\\033[0m\\002%{ :stash}%{ :changes}"}" - - RESET_COLOR_LOCAL="\001${GIT_RADAR_COLOR_LOCAL_RESET:-"\\033[0m"}\002" - RESET_COLOR_REMOTE="\001${GIT_RADAR_COLOR_REMOTE_RESET:-"\\033[0m"}\002" - RESET_COLOR_CHANGES="\001${GIT_RADAR_COLOR_CHANGES_RESET:-"\\033[0m"}\002" - RESET_COLOR_BRANCH="\001${GIT_RADAR_COLOR_BRANCH_RESET:-"\\033[0m"}\002" - RESET_COLOR_STASH="\001${GIT_RADAR_COLOR_STASH_RESET:-"\\033[0m"}\002" -} - -fetch() { - FETCH_TIME="${GIT_RADAR_FETCH_TIME:-"$((5 * 60))"}" - TS_FILE="$(git rev-parse --git-path lastupdatetime 2>/dev/null)" - - now="$(date '+%s')" - timestamp="$(cat "$TS_FILE" 2>/dev/null)" - [ "$timestamp" -eq "$timestamp" ] >/dev/null 2>&1 || timestamp="0" - - if [ "$((now - timestamp))" -ge "$FETCH_TIME" ]; then - nohup git fetch --quiet >/dev/null 2>&1 & - echo "$now" >"$TS_FILE" - fi -} - -commit_short_sha() { - git rev-parse --short HEAD 2>/dev/null -} - -branch_name() { - git symbolic-ref --short HEAD 2>/dev/null -} - -remote_branch_name() { - localRef="$(branch_name)" - remote="$(git config --get "branch.${localRef}.remote")" - if [ -n "$remote" ]; then - remoteBranch="$(git config --get "branch.${localRef}.merge" | sed -e 's/^refs\/heads\///')" - if [ -n "$remoteBranch" ]; then - printf '%s/%s' "$remote" "$remoteBranch" - return 0 - else - return 1 - fi - else - return 1 - fi -} - -commits_behind_of_remote() { - remote_branch="$1" - [ -n "$remote_branch" ] \ - && git rev-list --count "HEAD..${remote_branch}" 2>/dev/null \ - || printf '0\n' -} - -commits_ahead_of_remote() { - remote_branch="$1" - [ -n "$remote_branch" ] \ - && git rev-list --count "${remote_branch}..HEAD" 2>/dev/null \ - || printf '0\n' +usage() { + echo "git-sonar [--fetch] [--no-remote-commits] [--no-local-commits]" + echo " [--no-status] [--no-stash]" + echo "" + echo "git-sonar - a heads up display for git" + echo " $GIT_SONAR_VERSION" + echo "" + echo " --fetch" + echo " Periodically fetch from remote repositories asynchronously in" + echo " the background." + echo "" + echo " --no-remote-commits" + echo " Disable showing commit counts between current upstream branch" + echo " and the project default branch." + echo "" + echo " --no-local-commits" + echo " Disable showing commit counts between the current local and" + echo " upstream branches." + echo "" + echo " --no-status" + echo " Disable showing summary of uncommitted changes. Often faster," + echo " but omits a lot of information." + echo "" + echo " --no-stash" + echo " Disable showing git stash count." + echo "" + echo "git-sonar is intended to be invoked by your shell prompt and may be" + echo "configured through the use of various environment variables. See" + echo "the documentation for more details." + exit 0 } -determine_tracked_remote() { - by_branch=$(git config --local branch."$(git rev-parse --abbrev-ref HEAD)".git-radar-tracked-remote) - [ -n "$by_branch" ] && echo "$by_branch" && return 0 +opt_fetch="" +opt_remote="true" +opt_local="true" +opt_status="true" +opt_stash="true" - by_config=$(git config --local git-radar.tracked-remote) - [ -n "$by_config" ] && echo "$by_config" && return 0 +while true; do + case "$1" in + --fetch) opt_fetch="true" ;; + --no-remote-commits) opt_remote="" ;; + --no-local-commits) opt_local="" ;; + --no-status) opt_status="" ;; + --no-stash) opt_stash="" ;; + --help) usage ;; + -h) usage ;; + *) break + esac + shift +done - echo "origin/master" -} +if [ $# -ne 0 ]; then + printf 'git-sonar: Unrecognized option given: %s\n' "$1" + exit 1 +fi -remote_behind_of_master() { - remote_branch="$1" - tracked_remote="$(determine_tracked_remote)" - [ -n "$remote_branch" ] \ - && [ "$remote_branch" != "$tracked_remote" ] \ - && git rev-list --count "${remote_branch}..${tracked_remote}" 2>/dev/null \ - || printf '0\n' +# git-precheck will determine whether we are currently inside a git repository, +# as well as whether the repo is in any abnormal state. If outside a repo, exit +# and do not process any remainder of this script. +git-precheck --quiet --ignore-dirty --ignore-untracked >/dev/null 2>&1 +precheck_status=$? +[ "$precheck_status" -ge 4 ] && exit 0 + +# Initialize configuration variables and set default values if not provided by +# the environment. Note: ANSI escape codes (and other unprintable sequences) +# must be enclosed within a \001 byte (ASCII start of heading) at the start and +# a \002 byte (ASCII start of text) at the end - some shells rely on this to +# correctly track the length of their rendered prompt. +COLOR_GRAY="\001\\033[1;30m\002" +COLOR_RED="\001\\033[1;31m\002" +COLOR_GREEN="\001\\033[1;32m\002" +COLOR_YELLOW="\001\\033[1;33m\002" +COLOR_WHITE="\001\\033[1;37m\002" +COLOR_DEFAULT="\001\\033[0m\002" + +FETCH_TIME="${GIT_SONAR_FETCH_TIME:-"300"}" + +ALERT_ICON="${GIT_SONAR_ALERT_ICON:-"${COLOR_YELLOW}⚡"}" +STASH_ICON="${GIT_SONAR_STASH_ICON:-"${COLOR_YELLOW}≡"}" +BRANCH_COLOR="${GIT_SONAR_BRANCH_COLOR:-"$COLOR_DEFAULT"}" + +COMMIT_ICON_AHEAD="${GIT_SONAR_COMMIT_ICON_AHEAD:-"${COLOR_GREEN}↑"}" # "←" +COMMIT_ICON_BEHIND="${GIT_SONAR_COMMIT_ICON_BEHIND:-"${COLOR_RED}↓"}" # "→" +COMMIT_ICON_DIVERGED="${GIT_SONAR_COMMIT_ICON_DIVERGED:-"${COLOR_YELLOW}⇵"}" # "⇄" + +STATUS_COLOR_STAGED="${GIT_SONAR_STATUS_COLOR_STAGED:-"$COLOR_GREEN"}" +STATUS_COLOR_UNSTAGED="${GIT_SONAR_STATUS_COLOR_UNSTAGED:-"$COLOR_RED"}" +STATUS_COLOR_UNMERGED="${GIT_SONAR_STATUS_COLOR_UNMERGED:-"$COLOR_YELLOW"}" +STATUS_COLOR_UNTRACKED="${GIT_SONAR_STATUS_COLOR_UNTRACKED:-"$COLOR_WHITE"}" + +PROMPT_COLOR="${GIT_SONAR_PROMPT_COLOR:-"$COLOR_GRAY"}" +PROMPT_FORMAT="${GIT_SONAR_PROMPT_FORMAT:-" ${PROMPT_COLOR}git:(${COLOR_DEFAULT}%{alert}%{remote: }%{branch}%{ :local}${PROMPT_COLOR})${COLOR_DEFAULT}%{ :stash}%{ :staged}%{ :unmerged}%{ :unstaged}%{ :untracked}"}" + +# Gather current git status and branch information. git porcelain status can +# be especially expensive to compute, so bypass it if the prompt disables status +# information. +[ -n "$opt_status" ] && git_status="$(git status --porcelain 2>/dev/null)" || git_status="" +upstream_name="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" +branch_name="$(git symbolic-ref --short HEAD 2>/dev/null)" +commit_hash="$(git rev-parse --short HEAD 2>/dev/null)" + +# Helper functions + +determine_default_branch() { + # todo: handle remotes other than 'origin' + for ref in origin/HEAD origin/master origin/main; do + if git rev-parse --verify "$ref" >/dev/null 2>&1; then + echo "$ref" + break + fi + done } -remote_ahead_of_master() { - remote_branch="$1" - tracked_remote="$(determine_tracked_remote)" - [ -n "$remote_branch" ] \ - && [ "$remote_branch" != "$tracked_remote" ] \ - && git rev-list --count "${tracked_remote}..${remote_branch}" 2>/dev/null \ - || printf '0\n' +line_count() { + # macos 'wc' produces odd formatting (extra spaces) + # The grep is present to deal with this, filter out cases where the count + # is zero, and provide a return value (true if count is non-zero). + wc -l 2>/dev/null | grep -oE '[1-9][0-9]*' } -# Diacritic marks for overlaying an arrow over A D C etc -#us="\xE2\x83\x97{$reset_color%}" -#them="\xE2\x83\x96%{$reset_color%}" -#both="\xE2\x83\xA1%{$reset_color%}" - -porcelain_status() { - git status --porcelain 2>/dev/null +status_count() { + echo "$git_status" | grep -E "$1" | line_count } -staged_status() { - gitStatus="${1:-"$(porcelain_status)"}" - prefix="${2:-""}" - suffix="${3:-""}" - staged_string="" - - filesModified="$(printf '%s' "$gitStatus" | grep -oE "M[ACDRM ] " | wc -l | grep -oEi '[1-9][0-9]*')" - filesAdded="$(printf '%s' "$gitStatus" | grep -oE "A[MCDR ] " | wc -l | grep -oEi '[1-9][0-9]*')" - filesDeleted="$(printf '%s' "$gitStatus" | grep -oE "D[AMCR ] " | wc -l | grep -oEi '[1-9][0-9]*')" - filesRenamed="$(printf '%s' "$gitStatus" | grep -oE "R[AMCD ] " | wc -l | grep -oEi '[1-9][0-9]*')" - filesCopied="$(printf '%s' "$gitStatus" | grep -oE "C[AMDR ] " | wc -l | grep -oEi '[1-9][0-9]*')" - typeChanged="$(printf '%s' "$gitStatus" | grep -oE "T[AMDR ] " | wc -l | grep -oEi '[1-9][0-9]*')" - - if [ -n "$filesAdded" ]; then - staged_string="$staged_string$filesAdded${prefix}A${suffix}" - fi - if [ -n "$filesDeleted" ]; then - staged_string="$staged_string$filesDeleted${prefix}D${suffix}" - fi - if [ -n "$filesModified" ]; then - staged_string="$staged_string$filesModified${prefix}M${suffix}" - fi - if [ -n "$filesRenamed" ]; then - staged_string="$staged_string$filesRenamed${prefix}R${suffix}" - fi - if [ -n "$filesCopied" ]; then - staged_string="$staged_string$filesCopied${prefix}C${suffix}" - fi - if [ -n "$typeChanged" ]; then - staged_string="$staged_string$typeChanged${prefix}TC${suffix}" - fi - printf '%s' "$staged_string" +print_commit_range() { + if [ -n "$1" ] && [ -n "$2" ]; then + ahead="$(git rev-list --count "${1}..${2}" 2>/dev/null)" || ahead="0" + behind="$(git rev-list --count "${2}..${1}" 2>/dev/null)" || behind="0" + + if [ "$behind" -ne 0 ] && [ "$ahead" -ne 0 ]; then + printf '%s%b%b%s' "$behind" "$COMMIT_ICON_DIVERGED" "$COLOR_DEFAULT" "$ahead" + elif [ "$behind" -ne 0 ]; then + printf '%s%b%b' "$behind" "$COMMIT_ICON_BEHIND" "$COLOR_DEFAULT" + elif [ "$ahead" -ne 0 ]; then + printf '%b%b%s' "$COMMIT_ICON_AHEAD" "$COLOR_DEFAULT" "$ahead" + fi + fi } -conflicted_status() { - gitStatus="${1:-"$(porcelain_status)"}" - prefix="${2:-""}" - suffix="${3:-""}" - conflicted_string="" - - filesUs="$(printf '%s' "$gitStatus" | grep -oE "[AD]U " | wc -l | grep -oEi '[1-9][0-9]*')" - filesThem="$(printf '%s' "$gitStatus" | grep -oE "U[AD] " | wc -l | grep -oEi '[1-9][0-9]*')" - filesBoth="$(printf '%s' "$gitStatus" | grep -oE "(UU|AA|DD) " | wc -l | grep -oEi '[1-9][0-9]*')" - - if [ -n "$filesUs" ]; then - conflicted_string="$conflicted_string$filesUs${prefix}U${suffix}" - fi - if [ -n "$filesThem" ]; then - conflicted_string="$conflicted_string$filesThem${prefix}T${suffix}" - fi - if [ -n "$filesBoth" ]; then - conflicted_string="$conflicted_string$filesBoth${prefix}B${suffix}" - fi - printf '%s' "$conflicted_string" -} +# Prompt elements -unstaged_status() { - gitStatus="${1:-"$(porcelain_status)"}" - prefix="${2:-""}" - suffix="${3:-""}" - unstaged_string="" - - filesModified="$(printf '%s' "$gitStatus" | grep -oE "[ACDRM ]M " | wc -l | grep -oEi '[1-9][0-9]*')" - filesDeleted="$(printf '%s' "$gitStatus" | grep -oE "[AMCR ]D " | wc -l | grep -oEi '[1-9][0-9]*')" - typeChanged="$(printf '%s' "$gitStatus" | grep -oE "[AMDR ]T " | wc -l | grep -oEi '[1-9][0-9]*')" - - if [ -n "$filesDeleted" ]; then - unstaged_string="$unstaged_string$filesDeleted${prefix}D${suffix}" - fi - if [ -n "$filesModified" ]; then - unstaged_string="$unstaged_string$filesModified${prefix}M${suffix}" - fi - if [ -n "$typeChanged" ]; then - unstaged_string="$unstaged_string$typeChanged${prefix}TC${suffix}" - fi - printf '%s' "$unstaged_string" +element_alert() { + configured_upstream="$(git config --local "branch.${branch_name}.merge")" + if { [ -n "$configured_upstream" ] && [ -z "$upstream_name" ]; } \ + || [ "$precheck_status" -ge 3 ]; then + printf '%b%b' "$ALERT_ICON" "$COLOR_DEFAULT" + fi } -untracked_status() { - gitStatus="${1:-"$(porcelain_status)"}" - prefix="${2:-""}" - suffix="${3:-""}" - untracked_string="" - - filesUntracked="$(printf '%s' "$gitStatus" | grep "?? " | wc -l | grep -oEi '[1-9][0-9]*')" - - if [ -n "$filesUntracked" ]; then - untracked_string="$untracked_string$filesUntracked${prefix}?${suffix}" - fi - printf '%s' "$untracked_string" +element_branch() { + [ -n "$branch_name" ] && branch="$branch_name" || branch="detached@${commit_hash}" + printf '%b%s%b' "$BRANCH_COLOR" "$branch" "$COLOR_DEFAULT" } -color_changes_status() { - separator="${1:- }" - porcelain="$(porcelain_status)" - changes="" - - if [ -n "$porcelain" ]; then - staged_changes="$(staged_status "$porcelain" "$COLOR_CHANGES_STAGED" "$RESET_COLOR_CHANGES")" - unstaged_changes="$(unstaged_status "$porcelain" "$COLOR_CHANGES_UNSTAGED" "$RESET_COLOR_CHANGES")" - untracked_changes="$(untracked_status "$porcelain" "$COLOR_CHANGES_UNTRACKED" "$RESET_COLOR_CHANGES")" - conflicted_changes="$(conflicted_status "$porcelain" "$COLOR_CHANGES_CONFLICTED" "$RESET_COLOR_CHANGES")" - if [ -n "$staged_changes" ]; then - staged_changes="$separator$staged_changes" +element_remote() { + if [ -n "$opt_remote" ]; then + default_branch="$(determine_default_branch)" + print_commit_range "$default_branch" "$upstream_name" fi - - if [ -n "$unstaged_changes" ]; then - unstaged_changes="$separator$unstaged_changes" - fi - - if [ -n "$conflicted_changes" ]; then - conflicted_changes="$separator$conflicted_changes" - fi - - if [ -n "$untracked_changes" ]; then - untracked_changes="$separator$untracked_changes" - fi - - changes="$staged_changes$conflicted_changes$unstaged_changes$untracked_changes" - fi - printf $PRINT_F_OPTION "${changes#"$separator"}" } -color_local_commits() { - green_ahead_arrow="${COLOR_LOCAL_AHEAD}↑$RESET_COLOR_LOCAL" - red_behind_arrow="${COLOR_LOCAL_BEHIND}↓$RESET_COLOR_LOCAL" - yellow_diverged_arrow="${COLOR_LOCAL_DIVERGED}⇵$RESET_COLOR_LOCAL" - local_commits="" - - if remote_branch="$(remote_branch_name)"; then - local_ahead="$(commits_ahead_of_remote "$remote_branch")" - local_behind="$(commits_behind_of_remote "$remote_branch")" - - if [ "$local_behind" -gt 0 ] && [ "$local_ahead" -gt 0 ]; then - local_commits="$local_behind$yellow_diverged_arrow$local_ahead" - elif [ "$local_behind" -gt 0 ]; then - local_commits="$local_behind$red_behind_arrow" - elif [ "$local_ahead" -gt 0 ]; then - local_commits="$green_ahead_arrow$local_ahead" +element_local() { + if [ -n "$opt_local" ]; then + print_commit_range "$upstream_name" "HEAD" fi - fi - printf $PRINT_F_OPTION "$local_commits" } -color_remote_commits() { - green_ahead_arrow="${COLOR_REMOTE_AHEAD}↑$RESET_COLOR_REMOTE" # "←" - red_behind_arrow="${COLOR_REMOTE_BEHIND}↓$RESET_COLOR_REMOTE" # "→" - yellow_diverged_arrow="${COLOR_REMOTE_DIVERGED}⇵$RESET_COLOR_REMOTE" # "⇄" - remote="" - - if remote_branch="$(remote_branch_name)"; then - remote_ahead="$(remote_ahead_of_master "$remote_branch")" - remote_behind="$(remote_behind_of_master "$remote_branch")" - - if [ "$remote_behind" -gt 0 ] && [ "$remote_ahead" -gt 0 ]; then - remote="$MASTER_SYMBOL$remote_behind$yellow_diverged_arrow$remote_ahead" - elif [ "$remote_ahead" -gt 0 ]; then - remote="$MASTER_SYMBOL$green_ahead_arrow$remote_ahead" - elif [ "$remote_behind" -gt 0 ]; then - remote="$MASTER_SYMBOL$remote_behind$red_behind_arrow" +element_stash() { + if [ -n "$opt_stash" ]; then + if cnt="$(git stash list | line_count)"; then + printf '%s%b%b' "$cnt" "$STASH_ICON" "$COLOR_DEFAULT" + fi fi - fi - - printf $PRINT_F_OPTION "$remote" -} - -color_alert() { - if { remote_branch="$(remote_branch_name)" \ - && ! git rev-parse "$remote_branch" -- >/dev/null 2>&1; } \ - || [ "$precheck_status" -ge 3 ]; then - printf '%b' "${COLOR_ALERT}⚡${RESET_COLOR}" - fi } -readable_branch_name() { - printf $PRINT_F_OPTION "$COLOR_BRANCH$(branch_name || printf '%s' "detached@$(commit_short_sha)")$RESET_COLOR_BRANCH" +element_staged() { + if [ -n "$opt_status" ]; then + for x in M T A D R C; do + if cnt="$(status_count "^${x}[MTDRC ] ")"; then + printf '%s%b%s%b' "$cnt" "$STATUS_COLOR_STAGED" "$x" "$COLOR_DEFAULT" + fi + done + fi } -stashed_status() { - printf '%s' "$(git stash list | wc -l 2>/dev/null | grep -oEi '[0-9][0-9]*')" +element_unstaged() { + if [ -n "$opt_status" ]; then + for x in M T D R C; do + if cnt="$(status_count "^[MTADRC ]${x} ")"; then + printf '%s%b%s%b' "$cnt" "$STATUS_COLOR_UNSTAGED" "$x" "$COLOR_DEFAULT" + fi + done + fi } -stash_status() { - number_stashes="$(stashed_status)" - if [ "$number_stashes" -gt 0 ]; then - printf $PRINT_F_OPTION "${number_stashes}${COLOR_STASH}≡${RESET_COLOR_STASH}" - fi +element_unmerged() { + if [ -n "$opt_status" ]; then + for x in A D U; do + if cnt="$(status_count "^(${x}${x}|U${x}|${x}U) ")"; then + printf '%s%b%s%b' "$cnt" "$STATUS_COLOR_UNMERGED" "$x" "$COLOR_DEFAULT" + fi + done + fi } -render_prompt() { - branch_sed="" - remote_sed="" - local_sed="" - changes_sed="" - stash_sed="" - - - if_pre="%\{([^%{}]{1,}:){0,1}" - if_post="(:[^%{}]{1,}){0,1}\}" - sed_pre="%{\(\([^%^{^}]*\)\:\)\{0,1\}" - sed_post="\(\:\([^%^{^}]*\)\)\{0,1\}}" - - if echo "$PROMPT_FORMAT" | grep -qE "${if_pre}alert${if_post}"; then - alert_result="$(color_alert)" - if [ -n "$alert_result" ]; then - alert_sed="s/${sed_pre}alert${sed_post}/\2${alert_result}\4/" - else - alert_sed="s/${sed_pre}alert${sed_post}//" +element_untracked() { + if [ -n "$opt_status" ]; then + if cnt="$(status_count "^\?\? ")"; then + printf '%s%b?%b' "$cnt" "$STATUS_COLOR_UNTRACKED" "$COLOR_DEFAULT" + fi fi - fi - if echo "$PROMPT_FORMAT" | grep -qE "${if_pre}remote${if_post}"; then - remote_result="$(color_remote_commits)" - if [ -n "$remote_result" ]; then - remote_sed="s/${sed_pre}remote${sed_post}/\2${remote_result}\4/" - else - remote_sed="s/${sed_pre}remote${sed_post}//" - fi - fi - if echo "$PROMPT_FORMAT" | grep -qE "${if_pre}branch${if_post}"; then - branch_result="$(readable_branch_name | sed -e 's/\//\\\//g')" - if [ -n "$branch_result" ]; then - branch_sed="s/${sed_pre}branch${sed_post}/\2${branch_result}\4/" - else - branch_sed="s/${sed_pre}branch${sed_post}//" - fi - fi - if echo "$PROMPT_FORMAT" | grep -qE "${if_pre}local${if_post}"; then - local_result="$(color_local_commits)" - if [ -n "$local_result" ]; then - local_sed="s/${sed_pre}local${sed_post}/\2$local_result\4/" - else - local_sed="s/${sed_pre}local${sed_post}//" - fi - fi - if echo "$PROMPT_FORMAT" | grep -qE "${if_pre}changes${if_post}"; then - changes_result="$(color_changes_status)" - if [ -n "$changes_result" ]; then - changes_sed="s/${sed_pre}changes${sed_post}/\2${changes_result}\4/" - else - changes_sed="s/${sed_pre}changes${sed_post}//" - fi - fi - if echo "$PROMPT_FORMAT" | grep -qE "${if_pre}stash${if_post}"; then - stash_result="$(stash_status)" - if [ -n "$stash_result" ]; then - stash_sed="s/${sed_pre}stash${sed_post}/\2${stash_result}\4/" - else - stash_sed="s/${sed_pre}stash${sed_post}//" - fi - fi - - printf '%b' "$PROMPT_FORMAT" | sed \ - -e "$alert_sed" \ - -e "$remote_sed" \ - -e "$branch_sed" \ - -e "$changes_sed" \ - -e "$local_sed" \ - -e "$stash_sed" } -usage() { - _git="\033[1;30mgit:(\033[0m" - _master="\033[0;37mmaster\033[0m" - _my_branch="\033[0;37mmy-branch\033[0m" - _endgit="\033[1;30m)\033[0m" - _untracked="\033[1;37mA\033[0m" - _added_staged="\033[1;32mA\033[0m" - _modified_unstaged="\033[1;31mM\033[0m" - _local_up="\033[1;32m↑\033[0m" - _2_from_master="\360\235\230\256 2 \033[1;31m→\033[0m " - _diverged_from_master="\360\235\230\256 2 \033[1;33m⇄\033[0m 3 " - _not_upstream="upstream \033[1;31m⚡\033[0m " - _detached="\033[0;37mdetached@94eac67\033[0m" - _conflicted_us="\033[1;33mU\033[0m" - _conflicted_them="\033[1;33mT\033[0m" - _ahead_master="\360\235\230\256 \033[1;32m←\033[0m" - _local_diverged="\033[1;33m⇵\033[0m" - _stash="\033[1;33m≡\033[0m" - echo "git-sonar - a heads up display for git" - echo " $GIT_SONAR_VERSION" - echo "" - echo "examples:" - printf '%b' " $_git$_master$_endgit" - echo " # You are on the master branch and everything is clean" - printf '%b' " $_git$_not_upstream$_my_branch$_endgit" - echo " # Fresh branch that we haven't pushed upstream" - printf '%b' " $_git$_my_branch$_endgit 2$_untracked" - echo " # Two files created that aren't tracked by git" - printf '%b' " $_git$_my_branch$_endgit 1$_added_staged 3$_modified_unstaged" - echo " # 1 new file staged to commit and 3 modifications that we still need to \`git add\`" - printf '%b' " $_git$_2_from_master$_my_branch 3$_local_up$_endgit" - echo " # 3 commits made locally ready to push up while master is ahead of us by 2" - printf '%b' " $_git$_diverged_from_master$_my_branch$_endgit" - echo " # our commits pushed up, master and my-branch have diverged" - printf '%b' " $_git$_detached$_endgit 2${_conflicted_them}3${_conflicted_us}" - echo " # mid rebase, we are detached and have 3 conflicts caused by US and 2 caused by THEM" - printf '%b' " $_git$_diverged_from_master$_my_branch 3${_local_diverged}5$_endgit" - echo " # rebase complete, our rewritten commits now need pushed up" - printf '%b' " $_git$_ahead_master 3 $_my_branch$_endgit" - echo " # origin/my-branch is up to date with master and has our 3 commits waiting merge" - printf '%b' " $_git$_master$_endgit 3$_stash" - echo " # You have 3 stashes stored" - - echo "" - echo "usage:" - echo " git-sonar [-h|--help] [-f|--fetch] [--zsh|--bash|--fish]" - echo "" - echo " -h --help # Display this text" - echo " -f --fetch # Periodically fetch your repo asynchronously in the background" - echo " --zsh # (does nothing - for compatibility with git-radar)" - echo " --bash # (does nothing - for compatibility with git-radar)" - echo " --fish # (does nothing - for compatibility with git-radar)" - exit +# Main functions + +IF_PRE="%\{([^%{}]{1,}:){0,1}" +IF_POST="(:[^%{}]{1,}){0,1}\}" +SED_PRE="%{\(\([^%^{^}]*\)\:\)\{0,1\}" +SED_POST="\(\:\([^%^{^}]*\)\)\{0,1\}}" + +prepare_element() { + if echo "$PROMPT_FORMAT" | grep -qE "${IF_PRE}${1}${IF_POST}"; then + result="$($2 | sed -e 's/\//\\\//g')" + if [ -n "$result" ]; then + printf '%b' "s/${SED_PRE}${1}${SED_POST}/\\\\2${result}\\\\4/" + else + printf '%b' "s/${SED_PRE}${1}${SED_POST}//" + fi + fi } -do_fetch="" - -while true; do - case "$1" in - --help) usage ;; - -h) usage ;; - --fetch) do_fetch="true" ;; - -f) do_fetch="true" ;; - --zsh) ;; - --bash) ;; - --fish) ;; - *) break - esac - shift -done +# If fetch was requested, manage timestamp and run fetch if necessary +if [ -n "$opt_fetch" ]; then + tsfile="$(git rev-parse --git-path lastupdatetime 2>/dev/null)" + now="$(date '+%s')" + timestamp="$(cat "$tsfile" 2>/dev/null)" + [ "$timestamp" -eq "$timestamp" ] >/dev/null 2>&1 || timestamp="0" -if [ $# -ne 0 ]; then - printf 'sonar: Unrecognized option given: %s\n' "$1" - exit 1 + if [ "$((now - timestamp))" -ge "$FETCH_TIME" ]; then + nohup git fetch --quiet >/dev/null 2>&1 & + echo "$now" >"$tsfile" + fi fi -git-precheck --quiet --ignore-dirty --ignore-untracked >/dev/null 2>&1 -precheck_status=$? - -# Guard all active operations by this "is in repo" check -if [ "$precheck_status" -lt 4 ]; then - # Merge configuration from accepted RC files - [ -f "$HOME/.gitradarrc" ] && . "$HOME/.gitradarrc" - [ -f "$HOME/.gitradarrc.bash" ] && . "$HOME/.gitradarrc.bash" - [ -f "$HOME/.gitradarrc.zsh" ] && . "$HOME/.gitradarrc.zsh" - [ -f "$HOME/.gitsonarrc" ] && . "$HOME/.gitsonarrc" - - [ -n "$do_fetch" ] && fetch >/dev/null 2>&1 - prepare_colors - render_prompt -fi +# Render prompt elements from format string +printf '%b' "$PROMPT_FORMAT" | sed \ + -e "$(prepare_element alert element_alert)" \ + -e "$(prepare_element branch element_branch)" \ + -e "$(prepare_element remote element_remote)" \ + -e "$(prepare_element local element_local)" \ + -e "$(prepare_element stash element_stash)" \ + -e "$(prepare_element staged element_staged)" \ + -e "$(prepare_element unstaged element_unstaged)" \ + -e "$(prepare_element unmerged element_unmerged)" \ + -e "$(prepare_element untracked element_untracked)" -- cgit v1.2.3