-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-context-graph
executable file
·374 lines (290 loc) · 12.3 KB
/
git-context-graph
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
#!/usr/bin/env bash
usage() {
local b; b="$(printf '\e[1m')"
local u; u="$(printf '\e[4m')"
local r; r="$(printf '\e[0m')"
cat <<EOF
${b}NAME${r}
${u}git-context-graph${r} - Show graph log of branch, its remote counterparts and default branch.
${b}SYNOPSIS${r}
${u}git${r} ${u}context-graph${r} [--local] [--no-default] [-a|--add] [<branch>...]
${u}git${r} ${u}context-graph${r} [<git-log options>...] [<options>...] [<branch>...] [-- <paths>...]
${u}git${r} ${u}context-graph${r} (-l|--list) [-s|--short] [<branch>...]
${u}git${r} ${u}context-graph${r} [<branch>...] (-A|--config-add) <additional_branch>...
${u}git${r} ${u}context-graph${r} [<branch>...] (-C|--config-clear) [<additional_branch>...]
${u}git${r} ${u}context-graph${r} (-h|--usage)
${b}DESCRIPTION${r}
This command is a shortcut to:
git log --color --graph --abbrev-commit --decorate --pretty=oneline \\
my-branch origin/my-branch main origin/main ...
${u}git${r} ${u}context-graph${r} [--local] [--no-default] [-a|--add] [<branch>...]
Show graph log of branch, its remote counterparts and default branch.
${u}git${r} ${u}context-graph${r} [<git-log options>...] [<options>...] [<branch>...] [-- <paths>...]
git-log options can be used to refine or customize the output
(see git-log documentation: https://git-scm.com/docs/git-log)
Ex:
git context-graph --pretty=medium -- some/path
${u}git${r} ${u}context-graph${r} (-l|--list) [-s|--short] [<branch>...]
List branches that would be shown in the graph (does not display graph).
${u}git${r} ${u}context-graph${r} [<branch>...] (-A|--config-add) <additional_branch>...
${u}git${r} ${u}context-graph${r} [<branch>...] (-C|--config-clear) [<additional_branch>...]
For a given branch, persist additional context branches to git configuration.
${u}git${r} ${u}context-graph${r} (-h|--usage)
Show this help.
${b}ARGUMENTS${r}
<branch>...
Branches to show graph for. If omitted, current branch will be used.
${b}OPTIONS${r}
-a|--add
Consider <branch> arguments as additional branches (added to current branch).
--local
Show only local branches (ignore remotes).
--no-default
Show only related branches (local and remote), without default branch ('main' / 'master').
-l|--list
List branches that would be shown in the graph (does not display graph).
-s|--short
Use short branch names when listing branches (without 'refs/heads/' or 'refs/remotes/').
Implies --list.
-A|--config-add) <additional_branch>...
For a given branch, persist additional context branches to git configuration.
-C|--config-clear) [<additional_branch>...]
For a given branch, remove additional context branches from git configuration.
If no additional branch is passed, all configured additional branches will be removed.
-h|--usage
Show this help.
EOF
}
################################################################################
# Git utility functions
git_current_branch() {
# `git branch --show-current` is available starting from Git 2.22
# fallback to `git rev-parse --abbrev-ref HEAD` if first command fails
(git branch --show-current 2>/dev/null) ||
git rev-parse --abbrev-ref HEAD
}
branch_with_remotes() {
local branch=${1:-$(git_current_branch)}
# Convert 'refs/heads/my-branch' to 'my-branch'
local short_branch=${branch#"refs/heads/"}
if [[ $short_branch == "HEAD" ]]; then
echo "$branch"
return
fi
local ref_pattern
[[ -z $GIT_CG_SHOW_LOCAL_ONLY ]] &&
ref_pattern="refs/**/$short_branch" ||
ref_pattern="refs/heads/$short_branch"
# Branches with same name on all remotes
local same_name_refs;
same_name_refs=$(git for-each-ref --format="%(refname)" "$ref_pattern")
echo "$same_name_refs"
[[ -n $GIT_CG_SHOW_LOCAL_ONLY ]] && return
# Remote tracking branch (in case name is different)
local tracking_remote;
tracking_remote=$(git for-each-ref --format='%(upstream)' "$(git rev-parse --symbolic-full-name "$branch" 2> /dev/null)")
[[ ! $same_name_refs =~ $tracking_remote ]] && echo "$tracking_remote"
}
################################################################################
# Branch listing functions
related_branches() {
local branches=("$@")
local current_branch;
current_branch=$(git_current_branch)
[[ -z $current_branch ]] && current_branch="HEAD"
# Default to current branch
[[ ${#branches[@]} -eq 0 ]] && branches[0]="$current_branch"
# Add current branch to list when considering arguments are additional branches
[[ -n $GIT_CG_ADD_TO_CURRENT ]] && branches+=("$current_branch")
local branch
local related_branches
for branch in "${branches[@]}"; do
related_branches="${related_branches}\n$(branch_with_remotes "$branch")"
done
# de-duplicate and remove blank lines
echo -e "${related_branches}" | sort -u | grep "\S"
}
additional_branches() {
local branch="$1"
local short_branch=${branch#"refs/heads/"}
git config --local --get-all "branch.${short_branch}.context"
}
default_branches() {
local default_branches
# default remote branches
local remote
for remote in $(git remote); do
local remote_default
remote_default=$(git symbolic-ref "refs/remotes/${remote}/HEAD" -- 2>/dev/null)
if [[ -n $remote_default ]]; then
local short_remote_default=${remote_default#"refs/remotes/"}
local short_tracking_default
short_tracking_default=$(git branch -vv | grep "$short_remote_default" | sed 's/^[* ]*//' | cut -d' ' -f1)
local short_default
[[ -n $short_tracking_default ]] &&
short_default="$short_tracking_default" ||
short_default=${remote_default#"refs/remotes/${remote}/"}
default_branches="${default_branches}\n${short_default}"
fi
done
# standard default branches
if [[ -z $default_branches ]]; then
local std_default
for std_default in 'main' 'master'; do
git show-ref --verify --quiet "refs/heads/$std_default" &&
default_branches="${default_branches}\n${std_default}"
done
fi
# de-duplicate and remove blank lines
echo -e "${default_branches}" | sort -u | grep "\S"
}
context_branches() {
local context_branches
context_branches="$(related_branches "$@")"
local context_branch
for context_branch in $(echo "$context_branches" | grep "^refs/heads/" ); do
local additional_branch
for additional_branch in $(additional_branches "$context_branch"); do
context_branches="${context_branches}\n$(branch_with_remotes "$additional_branch")"
done
done
if [[ -z $GIT_CG_SHOW_RELATED_ONLY ]]; then
local default_branch
for default_branch in $(default_branches); do
context_branches="${context_branches}\n$(branch_with_remotes "$default_branch")"
done
fi
# de-duplicate and remove blank lines
context_branches=$(echo -e "${context_branches}" | sort -u | grep "\S")
# short branch listing
[[ -n $GIT_CG_LIST_SHORT ]] && context_branches=$(echo "$context_branches" |
sed -e "s/^refs\/heads\///" |
sed -e "s/^refs\/remotes\///")
echo "$context_branches"
}
################################################################################
# Additional branch configuration functions
configure_additional_branches() {
# Default to current branch, fail if unable
if [[ ${#GIT_CG_BRANCHES[@]} -eq 0 ]]; then
GIT_CG_BRANCHES[0]=$(git_current_branch)
if [[ -z ${GIT_CG_BRANCHES[0]} ]]; then
echo "Unable to configure additional branch to detached HEAD." >&2
echo "Please checkout a branch or add source branch argument." >&2
exit 1
fi
fi
local branch
for branch in "${GIT_CG_BRANCHES[@]}"; do
if [[ $GIT_CG_CONFIG_CLEAR -eq 1 ]]; then
clear_additional_branches "$branch" "${GIT_CG_CLEAR_BRANCHES[@]}"
fi
if [[ $GIT_CG_CONFIG_ADD -eq 1 ]]; then
add_additional_branches "$branch" "${GIT_CG_ADD_BRANCHES[@]}"
fi
local additional_branches
additional_branches=$(additional_branches "$branch")
if [[ -n $additional_branches ]]; then
echo "Additional context branches for $branch:"
# shellcheck disable=SC2001 # We need to replace all lines
echo "$additional_branches" | sed 's/^/ /'
else
[[ $GIT_CG_CONFIG_CLEAR -eq 1 ]] &&
echo "Cleared additional context branches for $branch." ||
echo "No additional context branches for $branch."
fi
done
}
add_additional_branches() {
local source_branch=$1
local additional_branches=("${@:2}")
if [[ -z $source_branch ]]; then
echo "No branch to configure." >&2
exit 1
fi
if [[ ${#additional_branches[@]} -eq 0 ]]; then
echo "No additional context branch." >&2
exit 1
fi
local branch
for branch in "${additional_branches[@]}"; do
local add=${branch#"refs/heads/"}
[[ $source_branch = "$add" ]] &&
continue # same branch
[[ -z $(git for-each-ref --format="%(refname)" "refs/**/$add") ]] &&
continue # unknown branch
[[ ! $(additional_branches "$source_branch") =~ $add ]] &&
git config --local --add -- "branch.${source_branch}.context" "$add"
done
}
clear_additional_branches() {
local source_branch=$1
local additional_branches=("${@:2}")
if [[ -z $source_branch ]]; then
echo "No branch to configure." >&2
exit 1
fi
if [[ ${#additional_branches[@]} -eq 0 ]]; then
git config --local --unset-all "branch.${source_branch}.context"
else
local branch
for branch in "${additional_branches[@]}"; do
git config --local --unset "branch.${source_branch}.context" "$branch"
done
fi
}
################################################################################
# Main
# Check current directory is a Git repository
if ! git rev-parse 2> /dev/null; then
echo "Not a git repository"
exit 1
fi
# Parse options & arguments
GIT_CG_BRANCHES=()
GIT_CG_ADD_BRANCHES=()
GIT_CG_CLEAR_BRANCHES=()
GIT_LOG_OPTIONS=()
GIT_LOG_PATHS=()
while [[ $# -gt 0 ]]; do
case $1 in
-a|--add) GIT_CG_ADD_TO_CURRENT=1; shift ;;
--local) GIT_CG_SHOW_LOCAL_ONLY=1; shift ;;
--no-default) GIT_CG_SHOW_RELATED_ONLY=1; shift ;;
-l|--list) GIT_CG_LIST=1; shift ;;
-s|--short) GIT_CG_LIST=1; GIT_CG_LIST_SHORT=1; shift ;;
-A|--config-add) GIT_CG_CONFIGURE="add"; GIT_CG_CONFIG_ADD=1; shift ;;
-C|--config-clear) GIT_CG_CONFIGURE="clear"; GIT_CG_CONFIG_CLEAR=1; shift ;;
-h|--usage) GIT_CONTEXT_GRAPH_HELP=1; shift ;;
--) GIT_LOG_PATHS+=("$1"); shift ;;
-*) GIT_LOG_OPTIONS+=("$1"); shift ;;
*)
if [[ ${#GIT_LOG_PATHS[@]} -gt 0 ]]; then GIT_LOG_PATHS+=("$1") # git-log path argument (after '--')
elif [[ $GIT_CG_CONFIGURE == "add" ]]; then GIT_CG_ADD_BRANCHES+=("$1") # additional branch to add to config
elif [[ $GIT_CG_CONFIGURE == "clear" ]]; then GIT_CG_CLEAR_BRANCHES+=("$1") # additional branch to remove from config
else GIT_CG_BRANCHES+=("$1") # git-context-graph branch argument
fi; shift ;;
esac
done
# Context branch configuration
if [[ -n $GIT_CG_CONFIGURE ]]; then
configure_additional_branches
exit 0
fi
# Alternative outputs
if [[ -n $GIT_CG_LIST ]]; then
context_branches "${GIT_CG_BRANCHES[@]}"
exit 0
fi
if [[ -n $GIT_CONTEXT_GRAPH_HELP ]]; then
usage
exit 0
fi
# Show graph
# shellcheck disable=SC2046 # use word splitting for GIT_CG_BRANCHES
git \
-c log.excludeDecoration='refs/remotes/*/HEAD' \
log --color --graph --abbrev-commit --decorate --pretty=oneline \
"${GIT_LOG_OPTIONS[@]}" \
$(context_branches "${GIT_CG_BRANCHES[@]}") \
"${GIT_LOG_PATHS[@]}"