]> git.r.bdr.sh - rbdr/dotfiles/blob - zsh/functions/git-info.zsh
Use nvim as git editor
[rbdr/dotfiles] / zsh / functions / git-info.zsh
1 # Exposes Git repository information via the $git_info associative array.
2 #
3 # Authors:
4 # Sorin Ionescu <sorin.ionescu@gmail.com>
5 #
6
7 # Gets the Git special action (am, bisect, cherry, merge, rebase).
8 # Borrowed from vcs_info and edited.
9 function _git-action {
10 local action_dir
11 local git_dir="$(git-dir)"
12 local apply_formatted
13 local bisect_formatted
14 local cherry_pick_formatted
15 local cherry_pick_sequence_formatted
16 local merge_formatted
17 local rebase_formatted
18 local rebase_interactive_formatted
19 local rebase_merge_formatted
20
21 for action_dir in \
22 "${git_dir}/rebase-apply" \
23 "${git_dir}/rebase" \
24 "${git_dir}/../.dotest"
25 do
26 if [[ -d "$action_dir" ]] ; then
27 zstyle -s ':git:info:action:apply' format 'apply_formatted' || apply_formatted='apply'
28 zstyle -s ':git:info:action:rebase' format 'rebase_formatted' || rebase_formatted='rebase'
29
30 if [[ -f "${action_dir}/rebasing" ]] ; then
31 print "$rebase_formatted"
32 elif [[ -f "${action_dir}/applying" ]] ; then
33 print "$apply_formatted"
34 else
35 print "${rebase_formatted}/${apply_formatted}"
36 fi
37
38 return 0
39 fi
40 done
41
42 for action_dir in \
43 "${git_dir}/rebase-merge/interactive" \
44 "${git_dir}/.dotest-merge/interactive"
45 do
46 if [[ -f "$action_dir" ]]; then
47 zstyle -s ':git:info:action:rebase-interactive' format 'rebase_interactive_formatted' || rebase_interactive_formatted='rebase-interactive'
48 print "$rebase_interactive_formatted"
49 return 0
50 fi
51 done
52
53 for action_dir in \
54 "${git_dir}/rebase-merge" \
55 "${git_dir}/.dotest-merge"
56 do
57 if [[ -d "$action_dir" ]]; then
58 zstyle -s ':git:info:action:rebase-merge' format 'rebase_merge_formatted' || rebase_merge_formatted='rebase-merge'
59 print "$rebase_merge_formatted"
60 return 0
61 fi
62 done
63
64 if [[ -f "${git_dir}/MERGE_HEAD" ]]; then
65 zstyle -s ':git:info:action:merge' format 'merge_formatted' || merge_formatted='merge'
66 print "$merge_formatted"
67 return 0
68 fi
69
70 if [[ -f "${git_dir}/CHERRY_PICK_HEAD" ]]; then
71 if [[ -d "${git_dir}/sequencer" ]] ; then
72 zstyle -s ':git:info:action:cherry-pick-sequence' format 'cherry_pick_sequence_formatted' || cherry_pick_sequence_formatted='cherry-pick-sequence'
73 print "$cherry_pick_sequence_formatted"
74 else
75 zstyle -s ':git:info:action:cherry-pick' format 'cherry_pick_formatted' || cherry_pick_formatted='cherry-pick'
76 print "$cherry_pick_formatted"
77 fi
78
79 return 0
80 fi
81
82 if [[ -f "${git_dir}/BISECT_LOG" ]]; then
83 zstyle -s ':git:info:action:bisect' format 'bisect_formatted' || bisect_formatted='bisect'
84 print "$bisect_formatted"
85 return 0
86 fi
87
88 return 1
89 }
90
91 # Gets the Git status information.
92 function git-info {
93 # Extended globbing is needed to parse repository status.
94 setopt LOCAL_OPTIONS
95 setopt EXTENDED_GLOB
96
97 local action
98 local action_format
99 local action_formatted
100 local added=0
101 local added_format
102 local added_formatted
103 local ahead=0
104 local ahead_and_behind
105 local ahead_and_behind_cmd
106 local ahead_format
107 local ahead_formatted
108 local ahead_or_behind
109 local behind=0
110 local behind_format
111 local behind_formatted
112 local branch
113 local branch_format
114 local branch_formatted
115 local branch_info
116 local clean
117 local clean_formatted
118 local commit
119 local commit_format
120 local commit_formatted
121 local deleted=0
122 local deleted_format
123 local deleted_formatted
124 local dirty=0
125 local dirty_format
126 local dirty_formatted
127 local ignore_submodules
128 local indexed=0
129 local indexed_format
130 local indexed_formatted
131 local -A info_formats
132 local info_format
133 local modified=0
134 local modified_format
135 local modified_formatted
136 local position
137 local position_format
138 local position_formatted
139 local remote
140 local remote_cmd
141 local remote_format
142 local remote_formatted
143 local renamed=0
144 local renamed_format
145 local renamed_formatted
146 local stashed=0
147 local stashed_format
148 local stashed_formatted
149 local status_cmd
150 local status_mode
151 local unindexed=0
152 local unindexed_format
153 local unindexed_formatted
154 local unmerged=0
155 local unmerged_format
156 local unmerged_formatted
157 local untracked=0
158 local untracked_format
159 local untracked_formatted
160
161 # Clean up previous $git_info.
162 unset git_info
163 typeset -gA git_info
164
165 # Return if not inside a Git repository work tree.
166 if [ "$(git rev-parse --is-inside-work-tree 2> /dev/null)" != 'true' ]; then
167 return 1
168 fi
169
170 if (( $# > 0 )); then
171 if [[ "$1" == [Oo][Nn] ]]; then
172 git config --bool prompt.showinfo true
173 elif [[ "$1" == [Oo][Ff][Ff] ]]; then
174 git config --bool prompt.showinfo false
175 else
176 print "usage: $0 [ on | off ]" >&2
177 fi
178 return 0
179 fi
180
181 # Ignore submodule status.
182 zstyle -s ':git:status:ignore' submodules 'ignore_submodules'
183
184 # Format commit.
185 zstyle -s ':git:info:commit' format 'commit_format'
186 if [[ -n "$commit_format" ]]; then
187 commit="$(git rev-parse HEAD 2> /dev/null)"
188 if [[ -n "$commit" ]]; then
189 zformat -f commit_formatted "$commit_format" "c:$commit"
190 fi
191 fi
192
193 # Format stashed.
194 zstyle -s ':git:info:stashed' format 'stashed_format'
195 if [[ -n "$stashed_format" && -f "$(git-dir)/refs/stash" ]]; then
196 stashed="$(git stash list 2> /dev/null | wc -l | awk '{print $1}')"
197 if [[ -n "$stashed" ]]; then
198 zformat -f stashed_formatted "$stashed_format" "S:$stashed"
199 fi
200 fi
201
202 # Format action.
203 zstyle -s ':git:info:action' format 'action_format'
204 if [[ -n "$action_format" ]]; then
205 action="$(_git-action)"
206 if [[ -n "$action" ]]; then
207 zformat -f action_formatted "$action_format" "s:$action"
208 fi
209 fi
210
211 # Get the branch.
212 branch="${$(git symbolic-ref HEAD 2> /dev/null)#refs/heads/}"
213
214 # Format branch.
215 zstyle -s ':git:info:branch' format 'branch_format'
216 if [[ -n "$branch" && -n "$branch_format" ]]; then
217 zformat -f branch_formatted "$branch_format" "b:$branch"
218 fi
219
220 # Format position.
221 zstyle -s ':git:info:position' format 'position_format'
222 if [[ -z "$branch" && -n "$position_format" ]]; then
223 position="$(git describe --contains --all HEAD 2> /dev/null)"
224 if [[ -n "$position" ]]; then
225 zformat -f position_formatted "$position_format" "p:$position"
226 fi
227 fi
228
229 # Format remote.
230 zstyle -s ':git:info:remote' format 'remote_format'
231 if [[ -n "$branch" && -n "$remote_format" ]]; then
232 # Gets the remote name.
233 remote_cmd='git rev-parse --symbolic-full-name --verify HEAD@{upstream}'
234 remote="${$(${(z)remote_cmd} 2> /dev/null)##refs/remotes/}"
235 if [[ -n "$remote" ]]; then
236 zformat -f remote_formatted "$remote_format" "R:$remote"
237 fi
238 fi
239
240 zstyle -s ':git:info:ahead' format 'ahead_format'
241 zstyle -s ':git:info:behind' format 'behind_format'
242 if [[ -n "$branch" && ( -n "$ahead_format" || -n "$behind_format" ) ]]; then
243 # Gets the commit difference counts between local and remote.
244 ahead_and_behind_cmd='git rev-list --count --left-right HEAD...@{upstream}'
245
246 # Get ahead and behind counts.
247 ahead_and_behind="$(${(z)ahead_and_behind_cmd} 2> /dev/null)"
248
249 # Format ahead.
250 if [[ -n "$ahead_format" ]]; then
251 ahead="$ahead_and_behind[(w)1]"
252 if (( ahead > 0 )); then
253 zformat -f ahead_formatted "$ahead_format" "A:$ahead"
254 fi
255 fi
256
257 # Format behind.
258 if [[ -n "$behind_format" ]]; then
259 behind="$ahead_and_behind[(w)2]"
260 if (( behind > 0 )); then
261 zformat -f behind_formatted "$behind_format" "B:$behind"
262 fi
263 fi
264 fi
265
266 # Get status type.
267 if ! zstyle -t ':git:info' verbose; then
268 # Format indexed.
269 zstyle -s ':git:info:indexed' format 'indexed_format'
270 if [[ -n "$indexed_format" ]]; then
271 ((
272 indexed+=$(
273 git diff-index \
274 --no-ext-diff \
275 --name-only \
276 --cached \
277 --ignore-submodules=${ignore_submodules:-none} \
278 HEAD \
279 2> /dev/null \
280 | wc -l
281 )
282 ))
283 if (( indexed > 0 )); then
284 zformat -f indexed_formatted "$indexed_format" "i:$indexed"
285 fi
286 fi
287
288 # Format unindexed.
289 zstyle -s ':git:info:unindexed' format 'unindexed_format'
290 if [[ -n "$unindexed_format" ]]; then
291 ((
292 unindexed+=$(
293 git diff-files \
294 --no-ext-diff \
295 --name-only \
296 --ignore-submodules=${ignore_submodules:-none} \
297 2> /dev/null \
298 | wc -l
299 )
300 ))
301 if (( unindexed > 0 )); then
302 zformat -f unindexed_formatted "$unindexed_format" "I:$unindexed"
303 fi
304 fi
305
306 # Format untracked.
307 zstyle -s ':git:info:untracked' format 'untracked_format'
308 if [[ -n "$untracked_format" ]]; then
309 ((
310 untracked+=$(
311 git ls-files \
312 --other \
313 --exclude-standard \
314 2> /dev/null \
315 | wc -l
316 )
317 ))
318 if (( untracked > 0 )); then
319 zformat -f untracked_formatted "$untracked_format" "u:$untracked"
320 fi
321 fi
322
323 (( dirty = indexed + unindexed + untracked ))
324 else
325 # Use porcelain status for easy parsing.
326 status_cmd="git status --porcelain --ignore-submodules=${ignore_submodules:-none}"
327
328 # Get current status.
329 while IFS=$'\n' read line; do
330 # Count added, deleted, modified, renamed, unmerged, untracked, dirty.
331 # T (type change) is undocumented, see http://git.io/FnpMGw.
332 # For a table of scenarii, see http://i.imgur.com/2YLu1.png.
333 [[ "$line" == ([ACDMT][\ MT]|[ACMT]D)\ * ]] && (( added++ ))
334 [[ "$line" == [\ ACMRT]D\ * ]] && (( deleted++ ))
335 [[ "$line" == ?[MT]\ * ]] && (( modified++ ))
336 [[ "$line" == R?\ * ]] && (( renamed++ ))
337 [[ "$line" == (AA|DD|U?|?U)\ * ]] && (( unmerged++ ))
338 [[ "$line" == \?\?\ * ]] && (( untracked++ ))
339 (( dirty++ ))
340 done < <(${(z)status_cmd} 2> /dev/null)
341
342 # Format added.
343 if (( added > 0 )); then
344 zstyle -s ':git:info:added' format 'added_format'
345 zformat -f added_formatted "$added_format" "a:$added"
346 fi
347
348 # Format deleted.
349 if (( deleted > 0 )); then
350 zstyle -s ':git:info:deleted' format 'deleted_format'
351 zformat -f deleted_formatted "$deleted_format" "d:$deleted"
352 fi
353
354 # Format modified.
355 if (( modified > 0 )); then
356 zstyle -s ':git:info:modified' format 'modified_format'
357 zformat -f modified_formatted "$modified_format" "m:$modified"
358 fi
359
360 # Format renamed.
361 if (( renamed > 0 )); then
362 zstyle -s ':git:info:renamed' format 'renamed_format'
363 zformat -f renamed_formatted "$renamed_format" "r:$renamed"
364 fi
365
366 # Format unmerged.
367 if (( unmerged > 0 )); then
368 zstyle -s ':git:info:unmerged' format 'unmerged_format'
369 zformat -f unmerged_formatted "$unmerged_format" "U:$unmerged"
370 fi
371
372 # Format untracked.
373 if (( untracked > 0 )); then
374 zstyle -s ':git:info:untracked' format 'untracked_format'
375 zformat -f untracked_formatted "$untracked_format" "u:$untracked"
376 fi
377 fi
378
379 # Format dirty and clean.
380 if (( dirty > 0 )); then
381 zstyle -s ':git:info:dirty' format 'dirty_format'
382 zformat -f dirty_formatted "$dirty_format" "D:$dirty"
383 else
384 zstyle -s ':git:info:clean' format 'clean_formatted'
385 fi
386
387 # Format info.
388 zstyle -a ':git:info:keys' format 'info_formats'
389 for info_format in ${(k)info_formats}; do
390 zformat -f REPLY "$info_formats[$info_format]" \
391 "a:$added_formatted" \
392 "A:$ahead_formatted" \
393 "B:$behind_formatted" \
394 "b:$branch_formatted" \
395 "C:$clean_formatted" \
396 "c:$commit_formatted" \
397 "d:$deleted_formatted" \
398 "D:$dirty_formatted" \
399 "i:$indexed_formatted" \
400 "I:$unindexed_formatted" \
401 "m:$modified_formatted" \
402 "p:$position_formatted" \
403 "R:$remote_formatted" \
404 "r:$renamed_formatted" \
405 "s:$action_formatted" \
406 "S:$stashed_formatted" \
407 "U:$unmerged_formatted" \
408 "u:$untracked_formatted"
409 git_info[$info_format]="$REPLY"
410 done
411
412 unset REPLY
413
414 return 0
415 }
416
417 git-info "$@"