blob: 4da43f8e210080c2f82305a31cdb35337c6eba2c (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
#!/bin/sh
GIT_SONAR_VERSION="v0.9.0-dev"
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
}
opt_fetch=""
opt_remote="true"
opt_local="true"
opt_status="true"
opt_stash="true"
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
if [ $# -ne 0 ]; then
printf 'git-sonar: Unrecognized option given: %s\n' "$1"
exit 1
fi
# 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_DEF="\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_DEF"}"
STATUS_SEP="${GIT_SONAR_STATUS_SEPARATOR:-" "}"
AHEAD_ICON="${GIT_SONAR_AHEAD_ICON:-"${COLOR_GREEN}↑"}" # "←"
BEHIND_ICON="${GIT_SONAR_BEHIND_ICON:-"${COLOR_RED}↓"}" # "→"
DIVERGED_ICON="${GIT_SONAR_DIVERGED_ICON:-"${COLOR_YELLOW}⇵"}" # "⇄"
STAGED_COLOR="${GIT_SONAR_STAGED_COLOR:-"$COLOR_GREEN"}"
UNSTAGED_COLOR="${GIT_SONAR_UNSTAGED_COLOR:-"$COLOR_RED"}"
UNMERGED_COLOR="${GIT_SONAR_UNMERGED_COLOR:-"$COLOR_YELLOW"}"
UNTRACKED_COLOR="${GIT_SONAR_UNTRACKED_COLOR:-"$COLOR_WHITE"}"
PROMPT_COLOR="${GIT_SONAR_PROMPT_COLOR:-"$COLOR_GRAY"}"
PROMPT_FORMAT="${GIT_SONAR_PROMPT_FORMAT:-" ${PROMPT_COLOR}git:(${COLOR_DEF}%{alert}%{remote: }%{branch}%{ :local}${PROMPT_COLOR})${COLOR_DEF}%{ :stash}%{ :status}"}"
# Gather information about the current git branch.
upstream_name="$(git rev-parse --abbrev-ref '@{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
}
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" "$DIVERGED_ICON" "$COLOR_DEF" "$ahead"
elif [ "$behind" -ne 0 ]; then
printf '%s%b%b' "$behind" "$BEHIND_ICON" "$COLOR_DEF"
elif [ "$ahead" -ne 0 ]; then
printf '%b%b%s' "$AHEAD_ICON" "$COLOR_DEF" "$ahead"
fi
fi
}
# Prompt elements
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_DEF"
fi
}
element_branch() {
[ -n "$branch_name" ] && branch="$branch_name" || branch="detached@${commit_hash}"
printf '%b%s%b' "$BRANCH_COLOR" "$branch" "$COLOR_DEF"
}
element_remote() {
if [ -n "$opt_remote" ]; then
default_branch="$(determine_default_branch)"
print_commit_range "$default_branch" "$upstream_name"
fi
}
element_local() {
if [ -n "$opt_local" ]; then
print_commit_range "$upstream_name" "HEAD"
fi
}
element_stash() {
if [ -n "$opt_stash" ]; then
cnt="$(git rev-list --walk-reflogs --ignore-missing --count refs/stash)"
if [ "$cnt" -ne 0 ]; then
printf '%s%b%b' "$cnt" "$STASH_ICON" "$COLOR_DEF"
fi
fi
}
element_status() {
if [ -n "$opt_status" ]; then
# git status is the most expensive subprocess we call, so place it here
# to bypass it if $opt_status is "false". The output is filtered
# through sed to prevent certain unmerged paths from being double
# counted as staged/unstaged ("AA"/"DD").
git_status="$(\
git status --porcelain 2>/dev/null \
| sed 's/^AA /AU /;s/^DD /DU /' \
)"
gs=""
st_sep="$STATUS_SEP"
um_sep="$STATUS_SEP"
us_sep="$STATUS_SEP"
ut_sep="$STATUS_SEP"
# See "man 1 git-status" sections "Short Format" and "Porcelain Format
# Version 1" for an explanation of these status indicators.
# Staged
for x in A M R C D T; do
if cnt="$(echo "$git_status" | grep -cE "^${x}[^U] ")"; then
gs="$(printf '%b%b%s%b%s%b' \
"$gs" "$st_sep" "$cnt" "$STAGED_COLOR" "$x" "$COLOR_DEF" \
)"
st_sep=""
fi
done
# Unmerged, conflicted
for x in A U D; do
if cnt="$(echo "$git_status" | grep -cE "^(U${x}|${x}U) ")"; then
gs="$(printf '%b%b%s%b%s%b' \
"$gs" "$um_sep" "$cnt" "$UNMERGED_COLOR" "$x" "$COLOR_DEF" \
)"
um_sep=""
fi
done
# Unstaged
for x in M D T; do # R C omitted
if cnt="$(echo "$git_status" | grep -cE "^[^U]${x} ")"; then
gs="$(printf '%b%b%s%b%s%b' \
"$gs" "$us_sep" "$cnt" "$UNSTAGED_COLOR" "$x" "$COLOR_DEF" \
)"
us_sep=""
fi
done
# Untracked
if cnt="$(echo "$git_status" | grep -cE "^\?\? ")"; then
gs="$(printf '%b%b%s%b?%b' \
"$gs" "$ut_sep" "$cnt" "$UNTRACKED_COLOR" "$COLOR_DEF" \
)"
ut_sep=""
fi
printf '%b' "${gs#"$STATUS_SEP"}"
fi
}
# Main functions
SED_PRE="%{\(\([^%^{^}]*\)\:\)\{0,1\}"
SED_POST="\(\:\([^%^{^}]*\)\)\{0,1\}}"
prepare_element() {
result="$($2 | sed '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
}
# 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 [ "$((now - timestamp))" -ge "$FETCH_TIME" ]; then
nohup git fetch --quiet >/dev/null 2>&1 &
echo "$now" >"$tsfile"
fi
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 status element_status)"
|