This repository was archived by the owner on Sep 7, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgit-hooks-multiplexer
More file actions
executable file
·226 lines (192 loc) · 7.78 KB
/
git-hooks-multiplexer
File metadata and controls
executable file
·226 lines (192 loc) · 7.78 KB
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
#!/usr/bin/env bash
set -eu -o pipefail
# Determine which git hook is being run and make sure we're not accidentally
# being called from another multiplexer script.
# shellcheck disable=2155
[[ -z ${HOOK:-} ]] && export HOOK=$(basename "$0") || exit
# Determine if we're being run explicitly with "git-hooks run ...""
export GITHOOKS_RUN=${GITHOOKS_RUN:-false}
# Get our installed location
unset CDPATH
bash_source="${BASH_SOURCE[0]}"
while [[ -h "$bash_source" ]]; do
bash_source_dir="$(cd -P "$(dirname "$bash_source")" >/dev/null && pwd)"
bash_source="$(readlink "$bash_source")"
[[ "$bash_source" != /* ]] && bash_source="${bash_source_dir}/${bash_source}"
done
bash_source_dir="$(cd -P "$(dirname "$bash_source")" >/dev/null && pwd)"
bash_source="${bash_source_dir}/${bash_source##*/}"
# Export our source lib directory so hook scripts can include lib functionality
export GIT_HOOKS_LIB="${bash_source_dir}/included/lib"
# Get functions defined in the main git-hooks script
# shellcheck disable=1090
. "${bash_source_dir}/git-hooks" "${bash_source_dir}"
export c_action
export c_prompt
export c_value
export c_error
export c_warning
export c_missing
export c_reset
# This script will run all configured hooks found in <repository>/.githooks/$HOOK.
#
# It will fail if any of those return non-zero exit codes. You can disable
# individual hooks with the 'git hooks disable' command:
#
# # Disable all custom hooks for this git hook
# git hooks disable <hook>
#
# # Disable an individual custom hook
# git hooks disable <custom-hook>
#
# The hook scripts will be run sequentially by default for safety.
# If you determine that your hooks can be run in parallel, enable it with:
#
# git config --int hooks.<hook>.parallel <num>
#
# where <num> is the number of jobs you wish to start. If 0, <num> will
# be interpretted as the number of CPUs on the machine. When running in
# parallel, each hook's output will be buffered until complete. If you
# choose to not run your hooks in parallel, the output will not be buffered.
#
# For safety, you can check for parallel execution in your hook by calling
# the "prevent_parallel" function. It checks the value of
# "git config hooks.<hook>.parallel" and exits with a non-zero exit code
# if it is set to anything but 1.
#
# Example:
# #!/usr/bin/env bash
# prevent_parallel # Will fail the hook unless it is being run
# # sequentially.
#
CAPTURE=/tmp/capture.$$
cat <<"EOF" >$CAPTURE
#!/usr/bin/env bash
function prevent_parallel {
if is_parallel; then
printf "${c_value} ${c_error}%s${c_reset}\\n" "${0##*/}" "cannot be run as a parallel job: git config hooks.$HOOK.parallel is set"
exit ${1:-1}
fi
}
function capture_on_exit {
# Store the exit code
local RESULT=$?
if [[ $RESULT -ne 0 ]]; then
printf "${c_error}%s ${c_value}%s ${c_error}%s${c_reset}\\n" "[failed" "$hook_name" "(exit code ${RESULT})]"
fi
trap - EXIT SIGHUP SIGINT SIGTERM
return $RESULT
}
# Create the temporary output buffer
read -r capture_outfile infile hook_name hook_path <<<"$1"
shift
# Redirect $infile to stdin if our hook expects inbound input
case "$HOOK" in
post-rewrite|pre-push) exec 10<&0 0<$infile ;;
*) exec 10>&0 0</dev/tty ;;
esac
# Report which hook is running
printf "${c_action}%s${c_reset}\\n" "[running ${HOOK}/${hook_name#*${HOOK}}]"
# Store stdout and stderr, then redirect them to the buffer
is_parallel && exec 11>&1 12>&2 &>$capture_outfile
# Provide the prevent_parallel function to the hook scripts
export -f prevent_parallel
# Display input if requested
if git config --get-regexp "hooks\.$HOOK\.showinput" true &>/dev/null; then
echo "[$# args to ${hook_name}] ${*:1}"
echo "[input to ${hook_name}]"
cat "$infile" # | sed "s|^|$(basename "${hook_name}")(in) \||g"
fi
# Set our exit trap to display any errors and do our clean up
trap "capture_on_exit" EXIT SIGHUP SIGINT SIGTERM
# Call the wrapped script
if is_parallel || git config --get-regexp "hooks\.$HOOK\.showinput" true &>/dev/null; then
echo "[output for ${hook_name}]"
fi
"$hook_path" "$@" 2>&1 # | sed "s|^|${hook_name}(out) \||g"
exit ${PIPESTATUS[0]}
EOF
# Collect the various hooks to run
hooks=
missing=
disabled=
nonexecutable=
tmpfile=$(mktemp -t git_hooks.XXXX)
count=0
# Set a trap to clean up our temp file
# shellcheck disable=2064
trap "rm -f ${tmpfile}*" EXIT SIGHUP SIGINT SIGTERM
# Collate our .githook scripts
while read -r _ hook_name hook hook_path; do
if { ! $GITHOOKS_RUN; } && { git config --get-regexp "hooks\\.$HOOK\\.enabled" false &>/dev/null \
|| git config --get-regexp "hooks\\.${hook_name}-${hook}\\.enabled" false &>/dev/null; }; then
disabled="${disabled:-}[${hook}]\\n"
elif [[ -f "$hook_path" ]]; then
if [[ -x "$hook_path" ]]; then
hooks="${hooks:-}$tmpfile.$((count++)) $tmpfile.stdin $hook ${hook_path}\\n"
else
nonexecutable="${nonexecutable:-}[${hook}]\\n"
fi
else
missing="${missing:-}[${hook}]\\n"
fi
done < <(git_hooks__get_hooks "$HOOK")
# Display disabled hooks
if [[ -n "$disabled" ]]; then
printf "${c_action}%s${c_reset}\\n" "[skipping disabled hooks]"
echo -en "${c_missing}${disabled}${c_reset}" | xargs -L1 | sed "s/^\\(.*\\)/ \\1/"
fi
# Might be done already
if [[ -z "$hooks" ]] && [[ -z "$missing" ]] && [[ -z "$nonexecutable" ]]; then
exit
fi
function is_parallel {
local jobs
jobs=$(git config "hooks.$HOOK.parallel")
[[ -n $jobs && ($jobs == max || $jobs -gt 1) ]]
}
export -f is_parallel
# Set a trap to display any buffered output and clean up our temp files
# shellcheck disable=2064
trap "rm -f ${tmpfile}.stdin; cat ${tmpfile}* 2>/dev/null; rm -f ${tmpfile}*" EXIT SIGHUP SIGINT SIGTERM
# Save our stdin so we can feed it to multiple scripts
# shellcheck disable=2015
[[ -t 0 ]] && touch "${tmpfile}.stdin" || cat >"${tmpfile}.stdin"
# Decide whether to run in parallel or sequentially
hooks_parallel="-P $(git config "hooks.${HOOK}.parallel")" || hooks_parallel=
if [[ max == $(git config "hooks.{$HOOK}.parallel") ]]; then
hooks_parallel="-P $(grep -c processor /proc/cpuinfo)"
fi
if [[ -n "$hooks_parallel" ]]; then
# Run the hooks through xargs. If any of them fail, xargs will fail and the script
# will exit with an error code due to the -e at the top of this script.
# shellcheck disable=2086
echo -en "$hooks" | xargs -I {} $hooks_parallel bash -e "$CAPTURE" {} "$@"
elif [[ -n "$hooks" ]]; then
# Use a loop instead of xargs since we want to stop on the first failure
# and we can't do that with xargs while both preserving the exit and not
# spamming up the output with xargs error messages.
while read -r args; do
bash -e "$CAPTURE" "$args" "$@"
done < <(echo -en "$hooks")
fi
# All hooks succeeded, reset our trap for cleanup and manually display the results
# shellcheck disable=2064
trap "rm -f ${tmpfile}* /tmp/capture.$$" EXIT SIGHUP SIGINT SIGTERM
rm "${tmpfile}.stdin"
cat "${tmpfile}"* 2>/dev/null || :
if [[ -n "$missing" || -n "$nonexecutable" ]]; then
# Fail the hook if we couldn't find one of the hook scripts (even though we ran
# the ones we found and encountered no errors)
if [[ -n "$missing" ]]; then
printf "${c_action}%s${c_reset}\\n" "[failed $HOOK checks due to missing hooks]"
echo -en "${c_missing}${missing}${c_reset}" | xargs -L1 | sed "s/^\\(.*\\)/ \\1/"
fi
# Fail the hook if we couldn't execute one of the hook scripts (even though we
# ran the ones we found and encountered no errors)
if [[ -n "$nonexecutable" ]]; then
printf "${c_action}%s${c_reset}\\n" "[failed $HOOK checks due to non-executable hooks]"
echo -en "${c_missing}${nonexecutable}${c_reset}" | xargs -L1 | sed "s/^\\(.*\\)/ \\1/"
fi
exit 1
fi