• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rycus86 / githooks / 11207477189

07 Oct 2024 02:13AM UTC coverage: 79.868% (-0.08%) from 79.95%
11207477189

push

github

rycus86
Fix minor formatting and wording :yellow_heart:

19 of 22 new or added lines in 3 files covered. (86.36%)

2 existing lines in 1 file now uncovered.

2539 of 3179 relevant lines covered (79.87%)

84.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.13
/cli.sh
1
#!/bin/sh
2
#
3
# Command line helper for https://github.com/rycus86/githooks
4
#
5
# This tool provides a convenience utility to manage
6
#   Githooks configuration, hook files and other
7
#   related functionality.
8
# This script should be an alias for `git hooks`, done by
9
#   git config --global alias.hooks "!${SCRIPT_DIR}/githooks"
10
#
11
# See the documentation in the project README for more information,
12
#   or run the `git hooks help` command for available options.
13
#
14
# Legacy version number. Not used anymore, but old installs read it.
15
# Version: 9912.310000-000000
16

17
#####################################################
18
# Prints the command line help for usage and
19
#   available commands.
20
#####################################################
21
print_help() {
22
    print_help_header
14✔
23

24
    echo "
13✔
25
Available commands:
13✔
26

13✔
27
    disable     Disables a hook in the current repository
13✔
28
    enable      Enables a previously disabled hook in the current repository
13✔
29
    accept      Accepts the pending changes of a new or modified hook
13✔
30
    exec        Executes a hook script on demand
13✔
31
    trust       Manages settings related to trusted repositories
13✔
32
    list        Lists the active hooks in the current repository
13✔
33
    shared      Manages the shared hook repositories
13✔
34
    install     Installs the latest Githooks hooks
13✔
35
    uninstall   Uninstalls the Githooks hooks
13✔
36
    update      Performs an update check
13✔
37
    readme      Manages the Githooks README in the current repository
13✔
38
    ignore      Manages Githooks ignore files in the current repository
13✔
39
    config      Manages various Githooks configuration
13✔
40
    tools       Manages script folders for tools
13✔
41
    version     Prints the version number of this script
13✔
42
    help        Prints this help message
13✔
43

13✔
44
You can also execute \`git hooks <cmd> help\` for more information on the individual commands.
13✔
45
"
13✔
46
}
47

48
#####################################################
49
# Prints a general header to be included
50
#   as the first few lines of every help message.
51
#####################################################
52
print_help_header() {
53
    echo
58✔
54
    echo "Githooks - https://github.com/rycus86/githooks"
57✔
55
    echo "----------------------------------------------"
57✔
56
}
57

58
#####################################################
59
# Sets the ${INSTALL_DIR} variable.
60
#
61
# Returns: None
62
#####################################################
63
load_install_dir() {
64
    INSTALL_DIR=$(git config --global --get githooks.installDir)
868✔
65

66
    if [ -z "${INSTALL_DIR}" ]; then
434✔
67
        # install dir not defined, use default
68
        INSTALL_DIR=~/".githooks"
3✔
69
    elif [ ! -d "$INSTALL_DIR" ]; then
432✔
70
        echo "! Githooks installation is corrupt! " >&2
1✔
71
        echo "  Install directory at ${INSTALL_DIR} is missing." >&2
1✔
72
        INSTALL_DIR=~/".githooks"
1✔
73
        echo "  Falling back to default directory at ${INSTALL_DIR}" >&2
1✔
74
        echo "  Please run the Githooks install script again to fix it." >&2
1✔
75
    fi
76

77
    GITHOOKS_CLONE_DIR="$INSTALL_DIR/release"
434✔
78
}
79

80
#####################################################
81
# Set up the main variables that
82
#   we will throughout the hook.
83
#
84
# Sets the ${CURRENT_GIT_DIR} variable
85
#
86
# Returns: None
87
#####################################################
88
set_main_variables() {
89
    CURRENT_GIT_DIR=$(git rev-parse --git-common-dir 2>/dev/null)
869✔
90
    if [ "${CURRENT_GIT_DIR}" = "--git-common-dir" ]; then
434✔
91
        CURRENT_GIT_DIR=".git"
×
92
    fi
93

94
    load_install_dir
434✔
95

96
    # Global IFS for loops
97
    IFS_NEWLINE="
98
"
434✔
99
}
100

101
#####################################################
102
# Checks if the current directory is
103
#   a Git repository or not.
104

105
# Returns:
106
#   0 if it is likely a Git repository,
107
#   1 otherwise
108
#####################################################
109
is_running_in_git_repo_root() {
110
    git rev-parse >/dev/null 2>&1 || return 1
258✔
111
    [ -d "${CURRENT_GIT_DIR}" ] || return 1
216✔
112
}
113

114
#####################################################
115
# Echo if the current repository is non-bare.
116
#
117
# Returns: 0
118
#####################################################
119
echo_if_non_bare_repo() {
120
    if [ "$(git rev-parse --is-bare-repository 2>/dev/null)" = "false" ]; then
54✔
121
        echo "$@"
26✔
122
    fi
123
    return 0
27✔
124
}
125

126
#####################################################
127
# Finds a hook file path based on trigger name,
128
#   file name, relative or absolute path, or
129
#   some combination of these, intended to
130
#   enable or disable exactly one hook found.
131
#
132
# Sets the ${HOOK_PATH} environment variable.
133
#
134
# Returns:
135
#   0 on success, 1 when no hooks found
136
#####################################################
137
find_hook_path_to_enable_or_disable() {
138
    if [ "$1" = "--shared" ]; then
24✔
139
        shift
6✔
140

141
        if [ -z "$1" ]; then
6✔
142
            echo "! For shared repositories, either the trigger type" >&2
×
143
            echo "  the hook name or both needs to be given" >&2
×
144
            return 1
×
145
        fi
146

147
        if [ ! -d "$INSTALL_DIR/shared" ]; then
6✔
148
            echo "! No shared repositories found" >&2
×
149
            return 1
×
150
        fi
151

152
        IFS="$IFS_NEWLINE"
6✔
153
        for SHARED_ROOT in "$INSTALL_DIR/shared/"*; do
6✔
154
            unset IFS
6✔
155

156
            if [ ! -d "$SHARED_ROOT" ]; then
6✔
157
                continue
×
158
            fi
159

160
            REMOTE_URL=$(git -C "$SHARED_ROOT" config --get remote.origin.url)
12✔
161

162
            if [ -f "$(pwd)/.githooks/.shared" ]; then
12✔
163
                SHARED_LOCAL_REPOS_LIST=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
18✔
164
                ACTIVE_LOCAL_REPO=$(echo "$SHARED_LOCAL_REPOS_LIST" | grep -F -o "$REMOTE_URL")
18✔
165
            else
166
                ACTIVE_LOCAL_REPO=""
×
167
            fi
168

169
            ACTIVE_GLOBAL_REPO=$(git config --global --get-all githooks.shared | grep -o "$REMOTE_URL")
18✔
170

171
            if [ "$ACTIVE_LOCAL_REPO" != "$REMOTE_URL" ] && [ "$ACTIVE_GLOBAL_REPO" != "$REMOTE_URL" ]; then
6✔
172
                continue
×
173
            fi
174

175
            if [ -n "$1" ] && [ -n "$2" ]; then
12✔
176
                if [ -f "$SHARED_ROOT/.githooks/$1/$2" ]; then
2✔
177
                    HOOK_PATH="$SHARED_ROOT/.githooks/$1/$2"
1✔
178
                    return
1✔
179
                elif [ -f "$SHARED_ROOT/$1/$2" ]; then
1✔
180
                    HOOK_PATH="$SHARED_ROOT/$1/$2"
1✔
181
                    return
1✔
182
                fi
183
            elif [ -d "$SHARED_ROOT/.githooks" ]; then
4✔
184
                HOOK_PATH=$(find "$SHARED_ROOT/.githooks" -name "$1" | head -1)
6✔
185
                [ -n "$HOOK_PATH" ] && return 0 || return 1
4✔
186
            else
187
                HOOK_PATH=$(find "$SHARED_ROOT" -name "$1" | head -1)
6✔
188
                [ -n "$HOOK_PATH" ] && return 0 || return 1
4✔
189
            fi
190

191
            IFS="$IFS_NEWLINE"
×
192
        done
193

194
        echo "! Sorry, cannot find any shared hooks that would match that" >&2
×
195
        return 1
×
196
    fi
197

198
    if [ -z "$1" ]; then
18✔
199
        HOOK_PATH=$(cd .githooks && pwd)
12✔
200

201
    elif [ -n "$1" ] && [ -n "$2" ]; then
28✔
202
        HOOK_TARGET="$(pwd)/.githooks/$1/$2"
4✔
203
        if [ -e "$HOOK_TARGET" ]; then
2✔
204
            HOOK_PATH="$HOOK_TARGET"
2✔
205
        fi
206

207
    elif [ -n "$1" ]; then
12✔
208
        if [ -e "$1" ]; then
12✔
209
            HOOK_DIR=$(dirname "$1")
4✔
210
            HOOK_NAME=$(basename "$1")
4✔
211

212
            if [ "$HOOK_NAME" = "." ]; then
2✔
213
                HOOK_PATH=$(cd "$HOOK_DIR" && pwd)
3✔
214
            else
215
                HOOK_PATH=$(cd "$HOOK_DIR" && pwd)/"$HOOK_NAME"
3✔
216
            fi
217

218
        elif [ -f ".githooks/$1" ]; then
10✔
219
            HOOK_PATH=$(cd .githooks && pwd)/"$1"
3✔
220

221
        else
222
            for HOOK_DIR in .githooks/*; do
9✔
223
                HOOK_ITEM=$(basename "$HOOK_DIR")
18✔
224
                if [ "$HOOK_ITEM" = "$1" ]; then
9✔
225
                    HOOK_PATH=$(cd "$HOOK_DIR" && pwd)
3✔
226
                fi
227

228
                if [ ! -d "$HOOK_DIR" ]; then
9✔
229
                    continue
3✔
230
                fi
231

232
                HOOK_DIR=$(cd "$HOOK_DIR" && pwd)
18✔
233

234
                IFS="$IFS_NEWLINE"
6✔
235
                for HOOK_FILE in "$HOOK_DIR"/*; do
10✔
236
                    unset IFS
10✔
237

238
                    HOOK_ITEM=$(basename "$HOOK_FILE")
20✔
239
                    if [ "$HOOK_ITEM" = "$1" ]; then
10✔
240
                        HOOK_PATH="$HOOK_FILE"
3✔
241
                    fi
242

243
                    IFS="$IFS_NEWLINE"
10✔
244
                done
245
            done
246
        fi
247
    fi
248

249
    if [ -z "$HOOK_PATH" ]; then
18✔
250
        echo "! Sorry, cannot find any hooks that would match that" >&2
5✔
251
        return 1
5✔
252
    elif echo "$HOOK_PATH" | grep -F -qv "/.githooks"; then
26✔
253
        if [ -d "$HOOK_PATH/.githooks" ]; then
1✔
254
            HOOK_PATH="$HOOK_PATH/.githooks"
1✔
255
        else
256
            echo "! Sorry, cannot find any hooks that would match that" >&2
×
257
            return 1
×
258
        fi
259
    fi
260
}
261

262
#####################################################
263
# Finds a hook file path based on trigger name,
264
#   file name, relative or absolute path, or
265
#   some combination of these, intended to
266
#   loosely match for executing the ones found.
267
#
268
# Sets the ${HOOK_PATHS} environment variable,
269
#   with the list of matching hook paths.
270
#
271
# Returns:
272
#   0 on success, 1 when no hooks found
273
#####################################################
274
find_hook_path_to_execute() {
275
    if [ -z "$1" ]; then
8✔
276
        echo "! Either the trigger type, the hook name or both needs to be given" >&2
×
277
        return 1
×
278

279
    elif [ -n "$1" ] && [ -n "$2" ]; then
16✔
280
        if [ -n "$EXACT_MATCH" ]; then
4✔
281
            SEARCH_PATTERN="$1/$2\$"
2✔
282
        else
283
            SEARCH_PATTERN="$1/.*$2.*"
2✔
284
        fi
285

286
    else
287
        TRIGGER_TYPES="(applypatch-msg|pre-applypatch|post-applypatch|pre-commit|prepare-commit-msg|commit-msg|post-commit"
4✔
288
        TRIGGER_TYPES="${TRIGGER_TYPES}|pre-rebase|post-checkout|post-merge|pre-push|pre-receive|update|post-receive"
4✔
289
        TRIGGER_TYPES="${TRIGGER_TYPES}|post-update|push-to-checkout|pre-auto-gc|post-rewrite|sendemail-validate)"
4✔
290

291
        if echo "$1" | grep -qE "^${TRIGGER_TYPES}$"; then
8✔
292
            SEARCH_PATTERN="$1/.*"
2✔
293
        else
294
            if [ -n "$EXACT_MATCH" ]; then
2✔
295
                SEARCH_PATTERN="${TRIGGER_TYPES}/$1\$"
×
296
            else
297
                SEARCH_PATTERN="${TRIGGER_TYPES}/.*$1.*"
2✔
298
            fi
299
        fi
300
    fi
301

302
    HOOK_PATHS=""
8✔
303
    IFS="$IFS_NEWLINE"
8✔
304
    for TARGET_HOOK in $(find "$(pwd)/.githooks" -type f | grep -E "/\.githooks/$SEARCH_PATTERN" | sort); do
37✔
305
        if [ -z "$HOOK_PATHS" ]; then
5✔
306
            HOOK_PATHS="$TARGET_HOOK"
4✔
307
        else
308
            HOOK_PATHS="$HOOK_PATHS
309
$TARGET_HOOK"
1✔
310
        fi
311
    done
312
    unset IFS
8✔
313

314
    if [ ! -d "$INSTALL_DIR/shared" ]; then
8✔
315
        # No shared repositories found
316
        return
4✔
317
    fi
318

319
    IFS="$IFS_NEWLINE"
4✔
320
    for SHARED_ROOT in "$INSTALL_DIR/shared/"*; do
4✔
321
        unset IFS
4✔
322

323
        if [ ! -d "$SHARED_ROOT" ]; then
4✔
324
            continue
×
325
        fi
326

327
        REMOTE_URL=$(git -C "$SHARED_ROOT" config --get remote.origin.url)
8✔
328

329
        if [ -f "$(pwd)/.githooks/.shared" ]; then
8✔
330
            SHARED_LOCAL_REPOS_LIST=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
12✔
331
            ACTIVE_LOCAL_REPO=$(echo "$SHARED_LOCAL_REPOS_LIST" | grep -F -o "$REMOTE_URL")
12✔
332
        else
333
            ACTIVE_LOCAL_REPO=""
×
334
        fi
335

336
        ACTIVE_GLOBAL_REPO=$(git config --global --get-all githooks.shared | grep -o "$REMOTE_URL")
12✔
337

338
        if [ "$ACTIVE_LOCAL_REPO" != "$REMOTE_URL" ] && [ "$ACTIVE_GLOBAL_REPO" != "$REMOTE_URL" ]; then
4✔
339
            continue
×
340
        fi
341

342
        if [ -d "$SHARED_ROOT/.githooks" ]; then
4✔
343
            SHARED_ROOT_SEARCH_START="$SHARED_ROOT/.githooks"
4✔
344
        else
345
            SHARED_ROOT_SEARCH_START="$SHARED_ROOT"
×
346
        fi
347

348
        IFS="$IFS_NEWLINE"
4✔
349
        for TARGET_HOOK in $(find "$SHARED_ROOT_SEARCH_START" -type f | grep -E "${SHARED_ROOT_SEARCH_START}/${SEARCH_PATTERN}" | sort); do
17✔
350
            if [ -z "$HOOK_PATHS" ]; then
5✔
351
                HOOK_PATHS="$TARGET_HOOK"
4✔
352
            else
353
                HOOK_PATHS="$HOOK_PATHS
354
$TARGET_HOOK"
1✔
355
            fi
356
        done
357
        unset IFS
4✔
358

359
        IFS="$IFS_NEWLINE"
4✔
360
    done
361
}
362

363
#####################################################
364
# Creates the Githooks checksum file
365
#   for the repository if it does not exist yet.
366
#####################################################
367
ensure_checksum_file_exists() {
368
    touch "${CURRENT_GIT_DIR}/.githooks.checksum"
19✔
369
}
370

371
#####################################################
372
# Disables one or more hook files
373
#   in the current repository.
374
#
375
# Returns:
376
#   1 if the current directory is not a Git repo,
377
#   0 otherwise
378
#####################################################
379
disable_hook() {
380
    if [ "$1" = "help" ]; then
17✔
381
        print_help_header
2✔
382
        echo "
2✔
383
git hooks disable [--shared] [trigger] [hook-script]
2✔
384
git hooks disable [--shared] [hook-script]
2✔
385
git hooks disable [--shared] [trigger]
2✔
386
git hooks disable [-a|--all]
2✔
387
git hooks disable [-r|--reset]
2✔
388

2✔
389
    Disables a hook in the current repository.
2✔
390
    The \`trigger\` parameter should be the name of the Git event if given.
2✔
391
    The \`hook-script\` can be the name of the file to disable, or its
2✔
392
    relative path, or an absolute path, we will try to find it.
2✔
393
    If the \`--shared\` parameter is given as the first argument,
2✔
394
    hooks in the shared repositories will be disabled,
2✔
395
    otherwise they are looked up in the current local repository.
2✔
396
    The \`--all\` parameter on its own will disable running any Githooks
2✔
397
    in the current repository, both existing ones and any future hooks.
2✔
398
    The \`--reset\` parameter is used to undo this, and let hooks run again.
2✔
399
"
2✔
400
        return
2✔
401
    fi
402

403
    if ! is_running_in_git_repo_root; then
15✔
404
        echo "! The current directory \`$(pwd)\` does not seem" >&2
4✔
405
        echo "  to be the root of a Git repository!" >&2
2✔
406
        exit 1
2✔
407
    fi
408

409
    if [ "$1" = "-a" ] || [ "$1" = "--all" ]; then
25✔
410
        git config githooks.disable true &&
2✔
411
            echo "All existing and future hooks are disabled in the current repository" &&
2✔
412
            return
2✔
413

414
        echo "! Failed to disable hooks in the current repository" >&2
×
415
        exit 1
×
416

417
    elif [ "$1" = "-r" ] || [ "$1" = "--reset" ]; then
21✔
418
        git config --unset githooks.disable
2✔
419

420
        if ! git config --get githooks.disable; then
2✔
421
            echo "Githooks hook files are not disabled anymore by default" && return
4✔
422
        else
423
            echo "! Failed to re-enable Githooks hook files" >&2
×
424
            exit 1
×
425
        fi
426
    fi
427

428
    if ! find_hook_path_to_enable_or_disable "$@"; then
9✔
429
        if [ "$1" = "update" ]; then
2✔
430
            echo "  Did you mean \`git hooks update disable\` ?"
1✔
431
        fi
432

433
        exit 1
2✔
434
    fi
435

436
    ensure_checksum_file_exists
7✔
437

438
    find "$HOOK_PATH" -type f -path "*/.githooks/*" | while IFS= read -r HOOK_FILE; do
41✔
439
        if grep -q "disabled> $HOOK_FILE" "${CURRENT_GIT_DIR}/.githooks.checksum" 2>/dev/null; then
10✔
440
            echo "Hook file is already disabled at $HOOK_FILE"
3✔
441
            continue
3✔
442
        fi
443

444
        echo "disabled> $HOOK_FILE" >>"${CURRENT_GIT_DIR}/.githooks.checksum"
7✔
445
        echo "Hook file disabled at $HOOK_FILE"
7✔
446
    done
447
}
448

449
#####################################################
450
# Enables one or more hook files
451
#   in the current repository.
452
#
453
# Returns:
454
#   1 if the current directory is not a Git repo,
455
#   0 otherwise
456
#####################################################
457
enable_hook() {
458
    if [ "$1" = "help" ]; then
11✔
459
        print_help_header
2✔
460
        echo "
2✔
461
git hooks enable [--shared] [trigger] [hook-script]
2✔
462
git hooks enable [--shared] [hook-script]
2✔
463
git hooks enable [--shared] [trigger]
2✔
464

2✔
465
    Enables a hook or hooks in the current repository.
2✔
466
    The \`trigger\` parameter should be the name of the Git event if given.
2✔
467
    The \`hook-script\` can be the name of the file to enable, or its
2✔
468
    relative path, or an absolute path, we will try to find it.
2✔
469
    If the \`--shared\` parameter is given as the first argument,
2✔
470
    hooks in the shared repositories will be enabled,
2✔
471
    otherwise they are looked up in the current local repository.
2✔
472
"
2✔
473
        return
2✔
474
    fi
475

476
    if ! is_running_in_git_repo_root; then
9✔
477
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
478
        exit 1
2✔
479
    fi
480

481
    if ! find_hook_path_to_enable_or_disable "$@"; then
7✔
482
        if [ "$1" = "update" ]; then
2✔
483
            echo "  Did you mean \`git hooks update enable\` ?"
1✔
484
        fi
485

486
        exit 1
2✔
487
    fi
488

489
    ensure_checksum_file_exists
5✔
490

491
    sed "\\|disabled> $HOOK_PATH|d" "${CURRENT_GIT_DIR}/.githooks.checksum" >"${CURRENT_GIT_DIR}/.githooks.checksum.tmp" &&
5✔
492
        mv "${CURRENT_GIT_DIR}/.githooks.checksum.tmp" "${CURRENT_GIT_DIR}/.githooks.checksum" &&
5✔
493
        echo "Hook file(s) enabled at $HOOK_PATH"
5✔
494
}
495

496
#####################################################
497
# Accept changes to a new or existing but changed
498
#   hook file by recording its checksum as accepted.
499
#
500
# Returns:
501
#   1 if the current directory is not a Git repo,
502
#   0 otherwise
503
#####################################################
504
accept_changes() {
505
    if [ "$1" = "help" ]; then
12✔
506
        print_help_header
2✔
507
        echo "
2✔
508
git hooks accept [--shared] [trigger] [hook-script]
2✔
509
git hooks accept [--shared] [hook-script]
2✔
510
git hooks accept [--shared] [trigger]
2✔
511

2✔
512
    Accepts a new hook or changes to an existing hook.
2✔
513
    The \`trigger\` parameter should be the name of the Git event if given.
2✔
514
    The \`hook-script\` can be the name of the file to enable, or its
2✔
515
    relative path, or an absolute path, we will try to find it.
2✔
516
    If the \`--shared\` parameter is given as the first argument,
2✔
517
    hooks in the shared repositories will be accepted,
2✔
518
    otherwise they are looked up in the current local repository.
2✔
519
"
2✔
520
        return
2✔
521
    fi
522

523
    if ! is_running_in_git_repo_root; then
10✔
524
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
525
        exit 1
2✔
526
    fi
527

528
    find_hook_path_to_enable_or_disable "$@" || exit 1
9✔
529

530
    ensure_checksum_file_exists
7✔
531

532
    find "$HOOK_PATH" -type f -path "*/.githooks/*" | while IFS= read -r HOOK_FILE; do
39✔
533
        if grep -q "disabled> $HOOK_FILE" "${CURRENT_GIT_DIR}/.githooks.checksum"; then
9✔
534
            echo "Hook file is currently disabled at $HOOK_FILE"
1✔
535
            continue
1✔
536
        fi
537

538
        CHECKSUM=$(get_hook_checksum "$HOOK_FILE")
16✔
539

540
        echo "$CHECKSUM $HOOK_FILE" >>"${CURRENT_GIT_DIR}/.githooks.checksum" &&
8✔
541
            echo "Changes accepted for $HOOK_FILE"
8✔
542
    done
543
}
544

545
#####################################################
546
# Execute a specific hook on demand.
547
#
548
# Returns:
549
#   1 if the current directory is not a Git repo,
550
#   0 otherwise
551
#####################################################
552
execute_hook() {
553
    if [ "$1" = "help" ]; then
10✔
554
        print_help_header
2✔
555
        echo "
2✔
556
git hooks exec [--exact] [trigger] [hook-script]
2✔
557
git hooks exec [--exact] [hook-script]
2✔
558
git hooks exec [--exact] [trigger]
2✔
559

2✔
560
    Executes a hook script on demand.
2✔
561
    During these executions, the \`GITHOOKS_ON_DEMAND_EXEC\` environment
2✔
562
    variable will be set, hook scripts can use that for conditional logic.
2✔
563
    The \`trigger\` parameter should be the name of the Git event if given.
2✔
564
    The \`hook-script\` can be the name of the file to execute, or its
2✔
565
    relative path, or an absolute path, we will try to find it.
2✔
566
    If the \`--exact\` flag is given, only hook scripts matching exactly
2✔
567
    will be returned, otherwise all hook scripts matching the substring.
2✔
568
"
2✔
569
        return
2✔
570
    fi
571

572
    if ! is_running_in_git_repo_root; then
8✔
573
        echo "! The current directory \`$(pwd)\` does not seem to be the root of a Git repository!" >&2
×
574
        exit 1
×
575
    fi
576

577
    if [ "$1" = "--exact" ]; then
8✔
578
        EXACT_MATCH=1
2✔
579
        shift
2✔
580
    fi
581

582
    if [ -z "$1" ]; then
8✔
583
        echo "! Missing [hook-script] or [trigger] argument, see git hooks exec help" >&2
×
584
        exit 1
×
585

586
    elif [ -n "$2" ]; then
8✔
587
        REQUESTED_HOOK_TYPE="$1"
4✔
588
        ACCEPTED_HOOK_TYPES="
589
        applypatch-msg pre-applypatch post-applypatch
590
        pre-commit prepare-commit-msg commit-msg post-commit
591
        pre-rebase post-checkout post-merge pre-push
592
        pre-receive update post-receive post-update
593
        push-to-checkout pre-auto-gc post-rewrite sendemail-validate"
4✔
594

595
        if ! echo "$ACCEPTED_HOOK_TYPES" | grep -qE "\b$REQUESTED_HOOK_TYPE\b"; then
8✔
596
            echo "! Unknown trigger type: $REQUESTED_HOOK_TYPE" >&2
×
597
            echo "  See git hooks exec help for usage" >&2
×
598
            exit 1
×
599
        fi
600

601
    elif [ -n "$3" ]; then
4✔
602
        echo "! Unexpected argument: $3" >&2
×
603
        echo "  See git hooks exec help for usage" >&2
×
604
        exit 1
×
605
    fi
606

607
    find_hook_path_to_execute "$@" || exit 1
8✔
608

609
    if [ -z "$HOOK_PATHS" ]; then
8✔
610
        echo "! Sorry, cannot find any hooks that would match that" >&2
×
611
        exit 1
×
612
    fi
613

614
    AGGREGATE_RESULT="0"
8✔
615

616
    IFS="$IFS_NEWLINE"
8✔
617
    for HOOK_FILE in ${HOOK_PATHS}; do
10✔
618
        unset IFS
10✔
619

620
        if [ -x "$HOOK_FILE" ]; then
10✔
621
            # Run as an executable file
622
            GITHOOKS_ON_DEMAND_EXEC=1 "$HOOK_FILE"
×
623
            RUN_RESULT="$?"
×
624

625
        elif [ -f "$HOOK_FILE" ]; then
10✔
626
            # Run as a Shell script
627
            GITHOOKS_ON_DEMAND_EXEC=1 sh "$HOOK_FILE"
20✔
628
            RUN_RESULT="$?"
10✔
629

630
        else
631
            echo "! Unable to run hook file: $HOOK_FILE}" >&2
×
632
            RUN_RESULT="1"
×
633
        fi
634

635
        [ "$RUN_RESULT" -gt "$AGGREGATE_RESULT" ] && AGGREGATE_RESULT="$RUN_RESULT"
10✔
636

637
        IFS="$IFS_NEWLINE"
10✔
638
    done
639
    unset IFS
8✔
640

641
    exit "$AGGREGATE_RESULT"
8✔
642
}
643

644
#####################################################
645
# Returns the SHA1 hash of the hook file
646
#   passed in as the first argument.
647
#####################################################
648
get_hook_checksum() {
649
    git hash-object "$1" 2>/dev/null
125✔
650
}
651

652
#####################################################
653
# Manage settings related to trusted repositories.
654
#   It allows setting up and clearing marker
655
#   files and Git configuration.
656
#
657
# Returns:
658
#   1 on failure, 0 otherwise
659
#####################################################
660
manage_trusted_repo() {
661
    if [ "$1" = "help" ]; then
17✔
662
        print_help_header
2✔
663
        echo "
2✔
664
git hooks trust [--local|--global]
2✔
665
git hooks trust [revoke]
2✔
666
git hooks trust [delete]
2✔
667
git hooks trust [forget] [--local|--global]
2✔
668

2✔
669
    Sets up, or reverts the trusted setting for the local/global repository.
2✔
670
    When called without arguments, it marks the local repository as trusted.
2✔
671
    The \`revoke\` argument resets the already accepted trust setting,
2✔
672
    and the \`delete\` argument also deletes the trusted marker.
2✔
673
    The \`forget\` option unsets the trust setting, asking for accepting
2✔
674
    it again next time, if the repository is marked as trusted.
2✔
675
"
2✔
676
        return
2✔
677
    fi
678

679
    if [ "$1" != "--global" ] && [ "$2" != "--global" ] && ! is_running_in_git_repo_root; then
41✔
680
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
681
        exit 1
2✔
682
    fi
683

684
    if [ "$1" = "--local" ] || [ -z "$1" ]; then
24✔
685
        mkdir -p .githooks &&
3✔
686
            touch .githooks/trust-all &&
3✔
687
            git config githooks.trust.all Y &&
3✔
688
            echo "The current repository is now trusted." &&
3✔
689
            echo_if_non_bare_repo "  Do not forget to commit and push the trust marker!" &&
3✔
690
            return
3✔
691

692
        echo "! Failed to mark the current repository as trusted" >&2
×
693
        exit 1
×
694

695
    elif [ "$1" = "--global" ]; then
10✔
696
        GLOBAL_TRUST=$(git config --global --get githooks.trust.all)
2✔
697
        if [ -z "${GLOBAL_TRUST}" ] || [ "${GLOBAL_TRUST}" = "N" ]; then
1✔
698
            git config --global githooks.trust.all Y
1✔
699
            echo "Global hook are now trusted when they contains a .githooks/trust-all file"
1✔
700
            return
1✔
701
        elif [ "${GLOBAL_TRUST}" = "Y" ]; then
×
702
            echo "Global hook are already trusted"
×
703
            return
×
704
        fi
705

706
    elif [ "$1" = "forget" ]; then
9✔
707
        if [ "$2" = "--local" ]; then
5✔
708
            if [ -z "$(git config --local --get githooks.trust.all)" ]; then
6✔
709
                echo "The current repository does not have trust settings."
1✔
710
                return
1✔
711
            elif git config --unset githooks.trust.all; then
2✔
712
                echo "The current repository is no longer trusted."
2✔
713
                return
2✔
714
            else
715
                echo "! Failed to revoke the trusted setting" >&2
×
716
                exit 1
×
717
            fi
718

719
        elif [ "$2" = "--global" ]; then
2✔
720
            if [ -z "$(git config --global --get githooks.trust.all)" ]; then
4✔
721
                echo "Global hooks do not have trust settings."
1✔
722
                return
1✔
723
            elif git config --global --unset githooks.trust.all; then
1✔
724
                echo "Global hooks are no longer trusted."
1✔
725
                return
1✔
726
            else
727
                echo "! Failed to revoke the trusted setting for global hooks" >&2
×
728
                exit 1
×
729
            fi
730

731
        else
732
            echo "! Usage: \`git hooks trust forget [--local|--global] \`" >&2
×
733
            exit 1
×
734

735
        fi
736

737
    elif [ "$1" = "revoke" ] || [ "$1" = "delete" ]; then
6✔
738
        if git config githooks.trust.all N; then
4✔
739
            echo "The current repository is no longer trusted."
4✔
740
        else
741
            echo "! Failed to revoke the trusted setting" >&2
×
742
            exit 1
×
743
        fi
744

745
        if [ "$1" = "revoke" ]; then
4✔
746
            return
2✔
747
        fi
748

749
    fi
750

751
    if [ "$1" = "delete" ] || [ -f .githooks/trust-all ]; then
2✔
752
        rm -rf .githooks/trust-all &&
2✔
753
            echo "The trust marker is removed from the repository." &&
2✔
754
            echo_if_non_bare_repo "  Do not forget to commit and push the change!" &&
2✔
755
            return
2✔
756

757
        echo "! Failed to delete the trust marker" >&2
×
758
        exit 1
×
759

760
    fi
761

762
    echo "! Unknown subcommand: $1" >&2
×
763
    echo "  Run \`git hooks trust help\` to see the available options." >&2
×
764
    exit 1
×
765
}
766

767
#####################################################
768
# Checks if Githhoks is set up correctly,
769
#   and that other git settings do not prevent it
770
#   from executing scripts.
771
#####################################################
772
check_git_hooks_setup_is_correct() {
773
    if [ -n "$(git config core.hooksPath)" ]; then
156✔
774
        if [ "true" != "$(git config githooks.useCoreHooksPath)" ]; then
6✔
775
            echo "! WARNING" >&2
2✔
776
            echo "  \`git config core.hooksPath\` is set to $(git config core.hooksPath)," >&2
4✔
777
            echo "  but Githooks is not configured to use that folder," >&2
2✔
778
            echo "  which could mean the hooks in this repository are not run by Githooks" >&2
2✔
779
            echo >&2
2✔
780
        fi
781
    else
782
        if [ "true" = "$(git config githooks.useCoreHooksPath)" ]; then
150✔
783
            echo "! WARNING" >&2
×
784
            echo "  Githooks is configured to consider \`git config core.hooksPath\`," >&2
×
785
            echo "  but that git setting is not currently set," >&2
×
786
            echo "  which could mean the hooks in this repository are not run by Githooks" >&2
×
787
            echo >&2
×
788
        fi
789
    fi
790
}
791

792
#####################################################
793
# Lists the hook files in the current
794
#   repository along with their current state.
795
#
796
# Returns:
797
#   1 if the current directory is not a Git repo,
798
#   0 otherwise
799
#####################################################
800
list_hooks() {
801
    if [ "$1" = "help" ]; then
82✔
802
        print_help_header
2✔
803
        echo "
2✔
804
git hooks list [type]
2✔
805

2✔
806
    Lists the active hooks in the current repository along with their state.
2✔
807
    If \`type\` is given, then it only lists the hooks for that trigger event.
2✔
808
    This command needs to be run at the root of a repository.
2✔
809
"
2✔
810
        return
2✔
811
    fi
812

813
    if ! is_running_in_git_repo_root; then
80✔
814
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
815
        exit 1
2✔
816
    fi
817

818
    check_git_hooks_setup_is_correct
78✔
819

820
    if [ -n "$*" ]; then
78✔
821
        LIST_TYPES="$*"
21✔
822
        WARN_NOT_FOUND="1"
21✔
823
    else
824
        LIST_TYPES="
825
        applypatch-msg pre-applypatch post-applypatch
826
        pre-commit prepare-commit-msg commit-msg post-commit
827
        pre-rebase post-checkout post-merge pre-push
828
        pre-receive update post-receive post-update
829
        push-to-checkout pre-auto-gc post-rewrite sendemail-validate"
57✔
830
    fi
831

832
    for LIST_TYPE in $LIST_TYPES; do
1,089✔
833
        LIST_OUTPUT=""
1,089✔
834

835
        # non-Githooks hook file
836
        if [ -x "${CURRENT_GIT_DIR}/hooks/${LIST_TYPE}.replaced.githook" ]; then
1,089✔
837
            ITEM_STATE=$(get_hook_state "${CURRENT_GIT_DIR}/hooks/${LIST_TYPE}.replaced.githook")
10✔
838
            LIST_OUTPUT="$LIST_OUTPUT
839
  - $LIST_TYPE (previous / file / ${ITEM_STATE})"
5✔
840
        fi
841

842
        # global shared hooks
843
        SHARED_REPOS_LIST=$(git config --global --get-all githooks.shared)
2,178✔
844
        IFS="$IFS_NEWLINE"
1,089✔
845
        for SHARED_ITEM in $(list_hooks_in_shared_repos "$LIST_TYPE"); do
1,103✔
846
            unset IFS
14✔
847
            if [ -d "$SHARED_ITEM" ]; then
14✔
848
                for LIST_ITEM in "$SHARED_ITEM"/*; do
12✔
849
                    ITEM_NAME=$(basename "$LIST_ITEM")
24✔
850
                    ITEM_STATE=$(get_hook_state "$LIST_ITEM" "$@")
24✔
851
                    LIST_OUTPUT="$LIST_OUTPUT
852
  - $ITEM_NAME (${ITEM_STATE} / shared:global)"
12✔
853
                    LIST_OUTPUT="$LIST_OUTPUT is from $LIST_ITEM"
12✔
854
                done
855

856
            elif [ -f "$SHARED_ITEM" ]; then
2✔
857
                ITEM_STATE=$(get_hook_state "$SHARED_ITEM")
4✔
858
                LIST_OUTPUT="$LIST_OUTPUT
859
  - $LIST_TYPE (file / ${ITEM_STATE} / shared:global)"
2✔
860
            fi
861

862
            IFS="$IFS_NEWLINE"
14✔
863
        done
864

865
        # local shared hooks
866
        if [ -f "$(pwd)/.githooks/.shared" ]; then
2,178✔
867
            SHARED_REPOS_LIST=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
885✔
868

869
            IFS="$IFS_NEWLINE"
295✔
870
            for SHARED_ITEM in $(list_hooks_in_shared_repos "$LIST_TYPE"); do
315✔
871
                unset IFS
20✔
872

873
                if [ -d "$SHARED_ITEM" ]; then
20✔
874
                    for LIST_ITEM in "$SHARED_ITEM"/*; do
17✔
875
                        ITEM_NAME=$(basename "$LIST_ITEM")
34✔
876
                        ITEM_STATE=$(get_hook_state "$LIST_ITEM")
34✔
877
                        LIST_OUTPUT="$LIST_OUTPUT
878
  - $ITEM_NAME (${ITEM_STATE} / shared:local)"
17✔
879
                    done
880

881
                elif [ -f "$SHARED_ITEM" ]; then
4✔
882
                    ITEM_STATE=$(get_hook_state "$SHARED_ITEM")
8✔
883
                    LIST_OUTPUT="$LIST_OUTPUT
884
  - $LIST_TYPE (file / ${ITEM_STATE} / shared:local)"
4✔
885
                fi
886

887
                IFS="$IFS_NEWLINE"
20✔
888
            done
889
        fi
890

891
        # in the current repository
892
        if [ -d ".githooks/$LIST_TYPE" ]; then
1,089✔
893

894
            IFS="$IFS_NEWLINE"
60✔
895
            for LIST_ITEM in .githooks/"$LIST_TYPE"/*; do
87✔
896
                unset IFS
87✔
897

898
                ITEM_NAME=$(basename "$LIST_ITEM")
174✔
899
                ITEM_STATE=$(get_hook_state "$(pwd)/.githooks/$LIST_TYPE/$ITEM_NAME")
261✔
900
                LIST_OUTPUT="$LIST_OUTPUT
901
  - $ITEM_NAME (${ITEM_STATE})"
87✔
902

903
                IFS="$IFS_NEWLINE"
87✔
904
            done
905

906
        elif [ -f ".githooks/$LIST_TYPE" ]; then
1,029✔
907
            ITEM_STATE=$(get_hook_state "$(pwd)/.githooks/$LIST_TYPE")
6✔
908
            LIST_OUTPUT="$LIST_OUTPUT
909
  - $LIST_TYPE (file / ${ITEM_STATE})"
2✔
910

911
        fi
912

913
        if [ -n "$LIST_OUTPUT" ]; then
1,089✔
914
            echo "> ${LIST_TYPE}${LIST_OUTPUT}"
83✔
915

916
        elif [ -n "$WARN_NOT_FOUND" ]; then
1,006✔
917
            echo "> $LIST_TYPE"
1✔
918
            echo "  No active hooks found"
1✔
919

920
        fi
921
    done
922
}
923

924
#####################################################
925
# Returns the state of hook file
926
#   in a human-readable format
927
#   on the standard output.
928
#####################################################
929
get_hook_state() {
930
    ITEM="$1"
129✔
931
    shift
129✔
932

933
    if is_repository_disabled; then
129✔
934
        echo "disabled"
×
935
    elif is_file_ignored "$ITEM"; then
129✔
936
        echo "ignored"
6✔
937
    elif is_trusted_repo "$ITEM"; then
123✔
938
        echo "active / trusted"
6✔
939
    else
940
        get_hook_enabled_or_disabled_state "$ITEM"
117✔
941
    fi
942
}
943

944
#####################################################
945
# Checks if Githooks is disabled in the
946
#   current local repository.
947
#
948
# Returns:
949
#   0 if disabled, 1 otherwise
950
#####################################################
951
is_repository_disabled() {
952
    GITHOOKS_CONFIG_DISABLE=$(git config --get githooks.disable)
266✔
953
    if [ "$GITHOOKS_CONFIG_DISABLE" = "true" ] ||
133✔
954
        [ "$GITHOOKS_CONFIG_DISABLE" = "y" ] ||    # Legacy
131✔
955
        [ "$GITHOOKS_CONFIG_DISABLE" = "Y" ]; then # Legacy
131✔
956
        return 0
2✔
957
    else
958
        return 1
131✔
959
    fi
960
}
961

962
#####################################################
963
# Checks if the hook file at ${HOOK_PATH}
964
#   is ignored and should not be executed.
965
#
966
# Returns:
967
#   0 if ignored, 1 otherwise
968
#####################################################
969
is_file_ignored() {
970
    HOOK_NAME=$(basename "$1")
258✔
971
    IS_IGNORED=""
129✔
972

973
    # If there are .ignore files, read the list of patterns to exclude.
974
    ALL_IGNORE_FILE=$(mktemp)
258✔
975
    if [ -f ".githooks/.ignore" ]; then
129✔
976
        cat ".githooks/.ignore" >"$ALL_IGNORE_FILE"
6✔
977
        echo >>"$ALL_IGNORE_FILE"
6✔
978
    fi
979
    if [ -f ".githooks/${LIST_TYPE}/.ignore" ]; then
129✔
980
        cat ".githooks/${LIST_TYPE}/.ignore" >>"$ALL_IGNORE_FILE"
6✔
981
        echo >>"$ALL_IGNORE_FILE"
6✔
982
    fi
983

984
    # Check if the filename matches any of the ignored patterns
985
    while IFS= read -r IGNORED; do
270✔
986
        if [ -z "$IGNORED" ] || [ "$IGNORED" != "${IGNORED#\#}" ]; then
21✔
987
            continue
3✔
988
        fi
989

990
        # shellcheck disable=SC2295
991
        if [ -z "${HOOK_NAME##$IGNORED}" ]; then
9✔
992
            IS_IGNORED="y"
6✔
993
            break
6✔
994
        fi
995
    done <"$ALL_IGNORE_FILE"
×
996

997
    # Remove the temporary file
998
    rm -f "$ALL_IGNORE_FILE"
129✔
999

1000
    if [ -n "$IS_IGNORED" ]; then
129✔
1001
        return 0
6✔
1002
    else
1003
        return 1
123✔
1004
    fi
1005
}
1006

1007
#####################################################
1008
# Checks whether the current repository
1009
#   is trusted, and that this is accepted.
1010
#
1011
# Returns:
1012
#   0 if the repo is trusted, 1 otherwise
1013
#####################################################
1014
is_trusted_repo() {
1015
    # Check if global hooks are trusted
1016
    if [ -f "$1/../trust-all" ] && [ "$(git config --global --get githooks.trust.all)" = "Y" ]; then
123✔
1017
        return 0
×
1018
    elif [ -f "$1/../../trust-all" ] && [ "$(git config --global --get githooks.trust.all)" = "Y" ]; then
123✔
1019
        return 0
×
1020
    fi
1021

1022
    # Check if local hook are trusted
1023
    if [ -f ".githooks/trust-all" ]; then
123✔
1024
        TRUST_ALL_CONFIG=$(git config --local --get githooks.trust.all)
14✔
1025
        TRUST_ALL_RESULT=$?
7✔
1026

1027
        # shellcheck disable=SC2181
1028
        if [ $TRUST_ALL_RESULT -ne 0 ]; then
7✔
1029
            return 1
1✔
1030
        elif [ $TRUST_ALL_RESULT -eq 0 ] && [ "$TRUST_ALL_CONFIG" = "Y" ]; then
12✔
1031
            return 0
6✔
1032
        fi
1033
    fi
1034

1035
    return 1
116✔
1036
}
1037

1038
#####################################################
1039
# Returns the enabled or disabled state
1040
#   in human-readable format for a hook file
1041
#   passed in as the first argument.
1042
#####################################################
1043
get_hook_enabled_or_disabled_state() {
1044
    HOOK_PATH="$1"
117✔
1045

1046
    SHA_HASH=$(get_hook_checksum "$HOOK_PATH")
234✔
1047
    CURRENT_HASHES=$(grep "$HOOK_PATH" "${CURRENT_GIT_DIR}/.githooks.checksum" 2>/dev/null)
234✔
1048

1049
    # check against the previous hash
1050
    if echo "$CURRENT_HASHES" | grep -q "disabled> $HOOK_PATH" >/dev/null 2>&1; then
234✔
1051
        echo "disabled"
18✔
1052
    elif ! echo "$CURRENT_HASHES" | grep -F -q "$SHA_HASH $HOOK_PATH" >/dev/null 2>&1; then
198✔
1053
        if [ -z "$CURRENT_HASHES" ]; then
86✔
1054
            echo "pending / new"
85✔
1055
        else
1056
            echo "pending / changed"
1✔
1057
        fi
1058
    else
1059
        echo "active"
13✔
1060
    fi
1061
}
1062

1063
#####################################################
1064
# List the shared hooks from the
1065
#  $INSTALL_DIR/shared directory.
1066
#
1067
# Returns the list of paths to the hook files
1068
#   in the shared hook repositories found locally.
1069
#####################################################
1070
list_hooks_in_shared_repos() {
1071
    SHARED_LIST_TYPE="$1"
1,384✔
1072
    ALREADY_LISTED=""
1,384✔
1073

1074
    IFS="$IFS_NEWLINE"
1,384✔
1075
    for SHARED_REPO_ITEM in $SHARED_REPOS_LIST; do
495✔
1076
        unset IFS
495✔
1077

1078
        set_shared_root "$SHARED_REPO_ITEM"
495✔
1079

1080
        if [ -e "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}" ]; then
495✔
1081
            echo "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}"
16✔
1082
        elif [ -e "${SHARED_ROOT}/${SHARED_LIST_TYPE}" ]; then
479✔
1083
            echo "${SHARED_ROOT}/${SHARED_LIST_TYPE}"
5✔
1084
        fi
1085

1086
        ALREADY_LISTED="$ALREADY_LISTED
1087
        ${SHARED_ROOT}"
495✔
1088
    done
1089

1090
    if [ ! -d "$INSTALL_DIR/shared" ]; then
1,384✔
1091
        return
851✔
1092
    fi
1093

1094
    IFS="$IFS_NEWLINE"
533✔
1095
    for SHARED_ROOT in "$INSTALL_DIR/shared/"*; do
591✔
1096
        unset IFS
591✔
1097
        if [ ! -d "$SHARED_ROOT" ]; then
591✔
1098
            continue
×
1099
        fi
1100

1101
        if echo "$ALREADY_LISTED" | grep -F -q "$SHARED_ROOT"; then
1,182✔
1102
            continue
247✔
1103
        fi
1104

1105
        REMOTE_URL=$(git -C "$SHARED_ROOT" config --get remote.origin.url)
688✔
1106
        ACTIVE_REPO=$(echo "$SHARED_REPOS_LIST" | grep -F -o "$REMOTE_URL")
1,032✔
1107
        if [ "$ACTIVE_REPO" != "$REMOTE_URL" ]; then
344✔
1108
            continue
286✔
1109
        fi
1110

1111
        if [ -e "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}" ]; then
58✔
1112
            echo "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}"
7✔
1113
        elif [ -e "${SHARED_ROOT}/${LIST_TYPE}" ]; then
51✔
1114
            echo "${SHARED_ROOT}/${LIST_TYPE}"
6✔
1115
        fi
1116
        IFS="$IFS_NEWLINE"
58✔
1117
    done
1118
}
1119

1120
#####################################################
1121
# Manages the shared hook repositories set either
1122
#   globally, or locally within the repository.
1123
# Changes the \`githooks.shared\` global Git
1124
#   configuration, or the contents of the
1125
#   \`.githooks/.shared\` file in the local
1126
#   Git repository.
1127
#
1128
# Returns:
1129
#   0 on success, 1 on failure (exit code)
1130
#####################################################
1131
manage_shared_hook_repos() {
1132
    if [ "$1" = "help" ]; then
121✔
1133
        print_help_header
2✔
1134
        echo "
2✔
1135
git hooks shared [add|remove] [--shared|--local|--global] <git-url>
2✔
1136
git hooks shared clear [--shared|--local|--global|--all]
2✔
1137
git hooks shared purge
2✔
1138
git hooks shared list [--shared|--local|--global|--all]
2✔
1139
git hooks shared [update|pull]
2✔
1140

2✔
1141
    Manages the shared hook repositories set either in the \`.githooks.shared\` file locally in the repository or
2✔
1142
    in the local or global Git configuration \`githooks.shared\`.
2✔
1143
    The \`add\` or \`remove\` subcommands adds or removes an item, given as \`git-url\` from the list.
2✔
1144
    If \`--local|--global\` is given, then the \`githooks.shared\` local/global Git configuration
2✔
1145
    is modified, or if the \`--shared\` option (default) is set, the \`.githooks/.shared\`
2✔
1146
    file is modified in the local repository.
2✔
1147
    The \`clear\` subcommand deletes every item on either the global or the local list,
2✔
1148
    or both when the \`--all\` option is given.
2✔
1149
    The \`purge\` subcommand deletes the shared hook repositories already pulled locally.
2✔
1150
    The \`list\` subcommand list the shared, local, global or all (default) shared hooks repositories.
2✔
1151
    The \`update\` or \`pull\` subcommands update all the shared repositories, either by
2✔
1152
    running \`git pull\` on existing ones or \`git clone\` on new ones.
2✔
1153
"
2✔
1154
        return
2✔
1155
    fi
1156

1157
    if [ "$1" = "update" ] || [ "$1" = "pull" ]; then
227✔
1158
        update_shared_hook_repos
21✔
1159
        return
21✔
1160
    fi
1161

1162
    if [ "$1" = "clear" ]; then
98✔
1163
        shift
7✔
1164
        clear_shared_hook_repos "$@"
7✔
1165
        return
3✔
1166
    fi
1167

1168
    if [ "$1" = "purge" ]; then
91✔
1169
        [ -w "$INSTALL_DIR/shared" ] &&
7✔
1170
            rm -rf "$INSTALL_DIR/shared" &&
5✔
1171
            echo "All existing shared hook repositories have been deleted locally" &&
5✔
1172
            return
5✔
1173

1174
        echo "! Cannot delete existing shared hook repositories locally (maybe there is none)" >&2
2✔
1175
        exit 1
2✔
1176
    fi
1177

1178
    if [ "$1" = "list" ]; then
84✔
1179
        shift
36✔
1180
        list_shared_hook_repos "$@"
36✔
1181
        return
32✔
1182
    fi
1183

1184
    if [ "$1" = "add" ]; then
48✔
1185
        shift
29✔
1186
        add_shared_hook_repo "$@"
29✔
1187
        return
23✔
1188
    fi
1189

1190
    if [ "$1" = "remove" ]; then
19✔
1191
        shift
17✔
1192
        remove_shared_hook_repo "$@"
17✔
1193
        return
13✔
1194
    fi
1195

1196
    echo "! Unknown subcommand: \`$1\`" >&2
2✔
1197
    exit 1
2✔
1198
}
1199

1200
#####################################################
1201
# Adds the URL of a new shared hook repository to
1202
#   the global or local list.
1203
#####################################################
1204
add_shared_hook_repo() {
1205
    SET_SHARED_TYPE="--shared"
29✔
1206
    SHARED_REPO_URL=
29✔
1207

1208
    if echo "$1" | grep -qE "\-\-(shared|local|global)"; then
58✔
1209
        SET_SHARED_TYPE="$1"
24✔
1210
        SHARED_REPO_URL="$2"
24✔
1211
    else
1212
        SHARED_REPO_URL="$1"
5✔
1213
    fi
1214

1215
    if [ -z "$SHARED_REPO_URL" ]; then
29✔
1216
        echo "! Usage: \`git hooks shared add [--shared|--local|--global] <git-url>\`" >&2
2✔
1217
        exit 1
2✔
1218
    fi
1219

1220
    if [ "$SET_SHARED_TYPE" != "--shared" ]; then
27✔
1221

1222
        if [ "$SET_SHARED_TYPE" = "--local" ] && ! is_running_in_git_repo_root; then
15✔
1223
            echo "! The current directory \`$(pwd)\` does not" >&2
×
1224
            echo "  seem to be the root of a Git repository!" >&2
×
1225
            exit 1
×
1226
        fi
1227

1228
        git config "$SET_SHARED_TYPE" --add githooks.shared "$SHARED_REPO_URL" &&
13✔
1229
            echo "The new shared hook repository is successfully added" &&
13✔
1230
            if [ "$SET_SHARED_TYPE" = "--global" ]; then choose_whether_to_trust_shared_repo "$@"; fi &&
24✔
1231
            return
13✔
1232

1233
        echo "! Failed to add the new shared hook repository" >&2
×
1234
        exit 1
×
1235

1236
    else
1237
        if ! is_running_in_git_repo_root; then
14✔
1238
            echo "! The current directory \`$(pwd)\` does not" >&2
4✔
1239
            echo "  seem to be the root of a Git repository!" >&2
2✔
1240
            exit 1
2✔
1241
        fi
1242

1243
        if is_local_path "$SHARED_REPO_URL" ||
12✔
1244
            is_local_url "$SHARED_REPO_URL"; then
10✔
1245
            echo "! Adding a local path:" >&2
2✔
1246
            echo "  \`$SHARED_REPO_URL\`" >&2
2✔
1247
            echo "  to the local shared hooks is forbidden." >&2
2✔
1248
            exit 1
2✔
1249
        fi
1250

1251
        mkdir -p "$(pwd)/.githooks"
20✔
1252

1253
        [ -f "$(pwd)/.githooks/.shared" ] &&
20✔
1254
            echo "" >>"$(pwd)/.githooks/.shared"
8✔
1255

1256
        echo "# Added on $(date)" >>"$(pwd)/.githooks/.shared" &&
30✔
1257
            echo "$SHARED_REPO_URL" >>"$(pwd)/.githooks/.shared" &&
20✔
1258
            echo "The new shared hook repository is successfully added" &&
10✔
1259
            echo_if_non_bare_repo "  Do not forget to commit the change!" &&
10✔
1260
            return
10✔
1261

1262
        echo "! Failed to add the new shared hook repository" >&2
×
1263
        exit 1
×
1264

1265
    fi
1266
}
1267

1268
#####################################################
1269
# Function is called when a shared hook has been
1270
#   added, then ask the user whether they want to
1271
#   trust the repository if the trust marker exists
1272
#####################################################
1273
choose_whether_to_trust_shared_repo() {
1274
    GLOBAL_TRUST=$(git config --global --get githooks.trust.all)
22✔
1275
    if [ -z "${GLOBAL_TRUST}" ]; then
11✔
1276

1277
        printf "If a trust marker is found in this repository, do you want to trust and accept future hooks without prompting ? [y/N]"
2✔
1278
        read -r TRUST_ALL_HOOKS </dev/tty
2✔
1279
        if [ "$TRUST_ALL_HOOKS" = "y" ] || [ "$TRUST_ALL_HOOKS" = "Y" ]; then
4✔
1280
            git config --global githooks.trust.all Y
×
1281
        else
1282
            git config --global githooks.trust.all N
2✔
1283
        fi
1284
    fi
1285
}
1286

1287
#####################################################
1288
# Removes the URL of a new shared hook repository to
1289
#   the global or local list.
1290
#####################################################
1291
remove_shared_hook_repo() {
1292
    SET_SHARED_TYPE="--shared"
17✔
1293
    SHARED_REPO_URL=
17✔
1294

1295
    if echo "$1" | grep -qE "\-\-(shared|local|global)"; then
34✔
1296
        SET_SHARED_TYPE="$1"
13✔
1297
        SHARED_REPO_URL="$2"
13✔
1298
    else
1299
        SHARED_REPO_URL="$1"
4✔
1300
    fi
1301

1302
    if [ -z "$SHARED_REPO_URL" ]; then
17✔
1303
        echo "! Usage: \`git hooks shared remove [--shared|--local|--global] <git-url>\`" >&2
2✔
1304
        exit 1
2✔
1305
    fi
1306

1307
    if [ "$SET_SHARED_TYPE" != "--shared" ]; then
15✔
1308

1309
        if [ "$SET_SHARED_TYPE" = "--local" ] && ! is_running_in_git_repo_root; then
7✔
1310
            echo "! The current directory \`$(pwd)\` does not" >&2
×
1311
            echo "  seem to be the root of a Git repository!" >&2
×
1312
            exit 1
×
1313
        fi
1314

1315
        CURRENT_LIST=$(git config "$SET_SHARED_TYPE" --get-all githooks.shared)
14✔
1316

1317
        # Unset all and add them back
1318
        git config "$SET_SHARED_TYPE" --unset-all githooks.shared
7✔
1319

1320
        IFS="$IFS_NEWLINE"
7✔
1321
        for SHARED_REPO_ITEM in $CURRENT_LIST; do
13✔
1322
            unset IFS
13✔
1323
            if [ "$SHARED_REPO_ITEM" = "$SHARED_REPO_URL" ]; then
13✔
1324
                continue
7✔
1325
            fi
1326

1327
            git config "$SET_SHARED_TYPE" --add githooks.shared "$SHARED_REPO_ITEM"
6✔
1328
            IFS="$IFS_NEWLINE"
6✔
1329
        done
1330
        unset IFS
7✔
1331

1332
        echo "The list of shared hook repositories is successfully changed"
7✔
1333
        return
7✔
1334

1335
    else
1336
        if ! is_running_in_git_repo_root; then
8✔
1337
            echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
1338
            exit 1
2✔
1339
        fi
1340

1341
        if [ ! -f "$(pwd)/.githooks/.shared" ]; then
12✔
1342
            echo "! No \`.githooks/.shared\` in current repository" >&2
×
1343
            return
×
1344
        fi
1345

1346
        NEW_LIST=""
6✔
1347
        ONLY_COMMENTS="true"
6✔
1348
        IFS="$IFS_NEWLINE"
6✔
1349
        while read -r LINE || [ -n "$LINE" ]; do
54✔
1350
            unset IFS
42✔
1351

1352
            if echo "$LINE" | grep -qE "^[^#\n\r ].*$"; then
84✔
1353
                if [ "$LINE" = "$SHARED_REPO_URL" ]; then
12✔
1354
                    continue
6✔
1355
                fi
1356
                ONLY_COMMENTS="false"
6✔
1357
            elif echo "$LINE" | grep -qE "^ *[#].*$"; then
60✔
1358
                : # nothing to do for comments ...
18✔
1359
            else
1360
                : # skip empty lines
12✔
1361
            fi
1362

1363
            if [ -z "$NEW_LIST" ]; then
36✔
1364
                NEW_LIST="$LINE"
6✔
1365
            else
1366
                NEW_LIST="${NEW_LIST}
1367
${LINE}"
30✔
1368
            fi
1369

1370
            IFS="$IFS_NEWLINE"
36✔
1371
        done <"$(pwd)/.githooks/.shared"
×
1372
        unset IFS
6✔
1373

1374
        if [ -z "$NEW_LIST" ] || [ "$ONLY_COMMENTS" = "true" ]; then
12✔
1375
            clear_shared_hook_repos "$SET_SHARED_TYPE" && return || exit 1
4✔
1376
        fi
1377

1378
        echo "$NEW_LIST" >"$(pwd)/.githooks/.shared" &&
8✔
1379
            echo "The list of shared hook repositories is successfully changed" &&
4✔
1380
            echo_if_non_bare_repo "  Do not forget to commit the change!" &&
4✔
1381
            return
4✔
1382

1383
        echo "! Failed to remove a shared hook repository" >&2
×
1384
        exit 1
×
1385

1386
    fi
1387
}
1388

1389
#####################################################
1390
# Clears the list of shared hook repositories
1391
#   from the global or local list, or both.
1392
#####################################################
1393
clear_shared_hook_repos() {
1394
    CLEAR_GLOBAL_REPOS=""
9✔
1395
    CLEAR_LOCAL_REPOS=""
9✔
1396
    CLEAR_SHARED_REPOS=""
9✔
1397
    CLEAR_REPOS_FAILED=""
9✔
1398

1399
    case "$1" in
9✔
1400
    "--shared")
1401
        CLEAR_SHARED_REPOS=1
2✔
1402
        ;;
1403
    "--local")
1404
        CLEAR_LOCAL_REPOS=1
×
1405
        ;;
1406
    "--global")
1407
        CLEAR_GLOBAL_REPOS=1
×
1408
        ;;
1409
    "--all")
1410
        CLEAR_SHARED_REPOS=1
3✔
1411
        CLEAR_LOCAL_REPOS=1
3✔
1412
        CLEAR_GLOBAL_REPOS=1
3✔
1413
        ;;
1414
    *)
1415
        echo "! Unknown clear option \`$1\`" >&2
4✔
1416
        echo "  Usage: \`git hooks shared clear [--shared|--local|--global|--all]\`" >&2
4✔
1417
        exit 1
4✔
1418
        ;;
1419
    esac
1420

1421
    if [ -n "$CLEAR_LOCAL_REPOS" ]; then
6✔
1422
        if ! is_running_in_git_repo_root; then
3✔
1423
            echo "! The current directory \`$(pwd)\` does not" >&2
×
1424
            echo "  seem to be the root of a Git repository!" >&2
×
1425
            CLEAR_REPOS_FAILED=1
×
1426
        else
1427
            git config --local --unset-all githooks.shared
3✔
1428
            echo "Shared hook repository list in local Git config cleared"
4✔
1429
        fi
1430
    fi
1431

1432
    if [ -n "$CLEAR_GLOBAL_REPOS" ]; then
5✔
1433
        git config --global --unset-all githooks.shared
3✔
1434
        echo "Shared hook repository list in global Git config cleared"
3✔
1435
    fi
1436

1437
    if [ -n "$CLEAR_SHARED_REPOS" ] && [ -f "$(pwd)/.githooks/.shared" ]; then
15✔
1438
        rm -f "$(pwd)/.githooks/.shared" &&
4✔
1439
            echo "Shared hook repository list in \".githooks/.shared\` file cleared" ||
2✔
1440
            CLEAR_REPOS_FAILED=1
1✔
1441
    fi
1442

1443
    if [ -n "$CLEAR_REPOS_FAILED" ]; then
5✔
1444
        echo "! There were some problems clearing the shared hook repository list" >&2
1✔
1445
        exit 1
×
1446
    fi
1447
}
1448

1449
#####################################################
1450
# Prints the list of shared hook repositories,
1451
#   along with their Git URLs optionally, from
1452
#   the global or local list, or both.
1453
#####################################################
1454
list_shared_hook_repos() {
1455
    LIST_SHARED=1
44✔
1456
    LIST_CONFIGS="global,local"
44✔
1457

1458
    for ARG in "$@"; do
24✔
1459
        case "$ARG" in
24✔
1460
        "--shared")
1461
            LIST_CONFIGS=""
6✔
1462
            ;;
1463
        "--local")
1464
            LIST_CONFIGS="local"
×
1465
            LIST_SHARED=""
×
1466
            ;;
1467
        "--global")
1468
            LIST_CONFIGS="global"
10✔
1469
            LIST_SHARED=""
10✔
1470
            ;;
1471
        "--all") ;;
×
1472
        *)
1473
            echo "! Unknown list option \`$ARG\`" >&2
4✔
1474
            echo "  Usage: \`git hooks shared list [--shared|--local|--global|--all]\`" >&2
2✔
1475
            exit 1
2✔
1476
            ;;
1477
        esac
1478
    done
1479

1480
    IFS=","
42✔
1481
    for LIST_CONFIG in $LIST_CONFIGS; do
62✔
1482
        unset IFS
62✔
1483

1484
        echo "Shared hook repositories in $LIST_CONFIG Git config:"
62✔
1485
        SHARED_REPOS_LIST=$(git config "--$LIST_CONFIG" --get-all githooks.shared)
124✔
1486

1487
        if [ -z "$SHARED_REPOS_LIST" ]; then
62✔
1488
            echo "  - None"
41✔
1489
        else
1490

1491
            IFS="$IFS_NEWLINE"
21✔
1492
            for LIST_ITEM in $SHARED_REPOS_LIST; do
41✔
1493
                unset IFS
41✔
1494

1495
                set_shared_root "$LIST_ITEM"
41✔
1496

1497
                LIST_ITEM_STATE="invalid"
41✔
1498

1499
                if [ "$SHARED_REPO_IS_CLONED" = "true" ]; then
41✔
1500
                    if [ -d "$SHARED_ROOT" ]; then
39✔
1501
                        if [ "$(git -C "$SHARED_ROOT" config --get remote.origin.url)" = "$SHARED_REPO_CLONE_URL" ]; then
18✔
1502
                            LIST_ITEM_STATE="active"
7✔
1503
                        fi
1504
                    else
1505
                        LIST_ITEM_STATE="pending"
30✔
1506
                    fi
1507
                else
1508
                    [ -d "$SHARED_ROOT" ] && LIST_ITEM_STATE="active"
4✔
1509
                fi
1510

1511
                echo "  - $LIST_ITEM ($LIST_ITEM_STATE)"
41✔
1512

1513
                IFS="$IFS_NEWLINE"
41✔
1514
            done
1515
            unset IFS
21✔
1516
        fi
1517

1518
        IFS=","
62✔
1519
    done
1520
    unset IFS
42✔
1521

1522
    if [ -n "$LIST_SHARED" ]; then
42✔
1523
        echo "Shared hook repositories in \`.githooks/.shared\`:"
32✔
1524

1525
        if ! is_running_in_git_repo_root; then
32✔
1526
            echo "  - Current folder does not seem to be a Git repository"
2✔
1527
            [ -z "$LIST_CONFIGS" ] && exit 1
4✔
1528
        elif [ ! -f "$(pwd)/.githooks/.shared" ]; then
60✔
1529
            echo "  - None"
15✔
1530
        else
1531
            SHARED_REPOS_LIST=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
45✔
1532

1533
            IFS="$IFS_NEWLINE"
15✔
1534
            for LIST_ITEM in $SHARED_REPOS_LIST; do
29✔
1535
                unset IFS
29✔
1536

1537
                set_shared_root "$LIST_ITEM"
29✔
1538

1539
                LIST_ITEM_STATE="invalid"
29✔
1540

1541
                if [ "$SHARED_REPO_IS_CLONED" != "true" ]; then
29✔
UNCOV
1542
                    [ -d "$SHARED_ROOT" ] && LIST_ITEM_STATE="active"
×
1543
                else
1544
                    if [ -d "$SHARED_ROOT" ]; then
29✔
1545
                        if [ "$(git -C "$SHARED_ROOT" config --get remote.origin.url)" = "$SHARED_REPO_CLONE_URL" ]; then
16✔
1546
                            LIST_ITEM_STATE="active"
6✔
1547
                        fi
1548
                    else
1549
                        LIST_ITEM_STATE="pending"
21✔
1550
                    fi
1551
                fi
1552

1553
                echo "  - $LIST_ITEM ($LIST_ITEM_STATE)"
31✔
1554

1555
                IFS="$IFS_NEWLINE"
29✔
1556
            done
1557
            unset IFS
15✔
1558
        fi
1559
    fi
1560

1561
}
1562

1563
#####################################################
1564
# Updates the configured shared hook repositories.
1565
#
1566
# Returns:
1567
#   None
1568
#####################################################
1569
update_shared_hook_repos() {
1570
    if [ "$1" = "help" ]; then
22✔
1571
        print_help_header
1✔
1572
        echo "
1✔
1573
git hooks shared pull
1✔
1574

1✔
1575
    Updates the shared repositories found either
1✔
1576
    in the global Git configuration, or in the
1✔
1577
    \`.githooks/.shared\` file in the local repository.
1✔
1578

1✔
1579
> Please use \`git hooks shared pull\` instead, this version is now deprecated.
1✔
1580
"
1✔
1581
        return
1✔
1582
    fi
1583

1584
    if [ -f "$(pwd)/.githooks/.shared" ]; then
42✔
1585
        SHARED_HOOKS=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
39✔
1586
        update_shared_hooks_in --shared "$SHARED_HOOKS"
13✔
1587
    fi
1588

1589
    SHARED_HOOKS=$(git config --local --get-all githooks.shared 2>/dev/null)
42✔
1590
    if [ -n "$SHARED_HOOKS" ]; then
21✔
1591
        update_shared_hooks_in --local "$SHARED_HOOKS"
2✔
1592
    fi
1593

1594
    SHARED_HOOKS=$(git config --global --get-all githooks.shared)
42✔
1595
    if [ -n "$SHARED_HOOKS" ]; then
21✔
1596
        update_shared_hooks_in --global "$SHARED_HOOKS"
11✔
1597
    fi
1598

1599
    echo "Finished"
21✔
1600
}
1601

1602
#####################################################
1603
# Check if `$1` is not a supported git clone url and
1604
#   is treated as a local path to a repository.
1605
#   See `https://tools.ietf.org/html/rfc3986#appendix-B`
1606

1607
# Returns: 0 if it is a local path, 1 otherwise
1608
#####################################################
1609
is_local_path() {
1610
    if echo "$1" | grep -Eq "^[^:/?#]+://" ||  # its a URL `<scheme>://...``
1,210✔
1611
        echo "$1" | grep -Eq "^.+@.+:.+"; then # or its a short scp syntax
334✔
1612
        return 1
438✔
1613
    fi
1614
    return 0
167✔
1615
}
1616

1617
#####################################################
1618
# Check if url `$1`is a local url, e.g `file://`.
1619
#
1620
# Returns: 0 if it is a local url, 1 otherwise
1621
#####################################################
1622
is_local_url() {
1623
    if echo "$1" | grep -iEq "^\s*file://"; then
1,308✔
1624
        return 0
139✔
1625
    fi
1626
    return 1
301✔
1627
}
1628

1629
#####################################################
1630
# Sets the `SHARED_ROOT` and `NORMALIZED_NAME`
1631
#   for the shared hook repo url `$1` and sets
1632
#   `SHARED_REPO_IS_CLONED` to `true` and its
1633
#   `SHARED_REPO_CLONE_URL` if is needs to get
1634
#    cloned and `SHARED_REPO_IS_LOCAL` to `true`
1635
#    if `$1` points to to a local path.
1636
#
1637
# Returns:
1638
#   none
1639
#####################################################
1640
set_shared_root() {
1641

1642
    SHARED_ROOT=""
593✔
1643
    SHARED_REPO_CLONE_URL=""
593✔
1644
    SHARED_REPO_CLONE_BRANCH=""
593✔
1645
    SHARED_REPO_IS_LOCAL="false"
594✔
1646
    SHARED_REPO_IS_CLONED="true"
593✔
1647
    DO_SPLIT="true"
593✔
1648

1649
    if is_local_path "$1"; then
593✔
1650
        SHARED_REPO_IS_LOCAL="true"
165✔
1651

1652
        if is_bare_repo "$1"; then
165✔
1653
            DO_SPLIT="false"
×
1654
        else
1655
            # We have a local path to a non-bare repo
1656
            SHARED_REPO_IS_CLONED="false"
167✔
1657
            SHARED_ROOT="$1"
165✔
1658
            return
165✔
1659
        fi
1660
    elif is_local_url "$1"; then
428✔
1661
        SHARED_REPO_IS_LOCAL="true"
139✔
1662
    fi
1663

1664
    if [ "$SHARED_REPO_IS_CLONED" = "true" ]; then
428✔
1665
        # Here we now have a supported Git URL or
1666
        # a local bare-repo `<localpath>`
1667

1668
        # Split "...@(.*)"
1669
        if [ "$DO_SPLIT" = "true" ] && echo "$1" | grep -q "@"; then
1,290✔
1670
            SHARED_REPO_CLONE_URL="$(echo "$1" | sed -E "s|^(.+)@.+$|\\1|")"
150✔
1671
            SHARED_REPO_CLONE_BRANCH="$(echo "$1" | sed -E "s|^.+@(.+)$|\\1|")"
150✔
1672
        else
1673
            SHARED_REPO_CLONE_URL="$1"
378✔
1674
            SHARED_REPO_CLONE_BRANCH=""
378✔
1675
        fi
1676

1677
        # Double-check what we did above
1678
        if echo "$SHARED_REPO_CLONE_BRANCH" | grep -q ":"; then
856✔
1679
            # the branch name had a ":" so it was probably not a branch name
1680
            SHARED_REPO_CLONE_URL="${SHARED_REPO_CLONE_URL}@${SHARED_REPO_CLONE_BRANCH}"
×
1681
            SHARED_REPO_CLONE_BRANCH=""
×
1682

1683
        elif echo "$SHARED_REPO_CLONE_URL" | grep -qE ".*://[^/]+$"; then
856✔
1684
            # the clone URL is something starting with a protocol then no path parts, then we probably split at the wrong place
1685
            SHARED_REPO_CLONE_URL="${SHARED_REPO_CLONE_URL}@${SHARED_REPO_CLONE_BRANCH}"
48✔
1686
            SHARED_REPO_CLONE_BRANCH=""
48✔
1687
        fi
1688

1689
        # Define the shared clone folder
1690
        SHA_HASH=$(echo "$1" | git hash-object --stdin 2>/dev/null)
1,284✔
1691
        NAME=$(echo "$1" | tail -c 48 | sed -E "s/[^a-zA-Z0-9]/-/g")
1,712✔
1692
        SHARED_ROOT="$INSTALL_DIR/shared/$SHA_HASH-$NAME"
428✔
1693
    fi
1694
}
1695

1696
#####################################################
1697
# Updates the shared hooks repositories
1698
#   on the list passed in on the first argument.
1699
#####################################################
1700
update_shared_hooks_in() {
1701
    SHARED_HOOKS_TYPE="$1"
26✔
1702
    SHARED_REPOS_LIST="$2"
26✔
1703

1704
    IFS="$IFS_NEWLINE"
26✔
1705
    for SHARED_REPO in $SHARED_REPOS_LIST; do
28✔
1706
        unset IFS
28✔
1707

1708
        set_shared_root "$SHARED_REPO"
28✔
1709

1710
        if [ "$SHARED_REPO_IS_CLONED" != "true" ]; then
28✔
1711
            # Non-cloned roots are ignored
1712
            continue
11✔
1713
        elif [ "$SHARED_HOOKS_TYPE" = "--shared" ] &&
17✔
1714
            [ "$SHARED_REPO_IS_LOCAL" = "true" ]; then
10✔
1715
            echo "! Warning: Shared hooks in \`.githooks/.shared\` contain a local path" >&2
×
1716
            echo "  \`$SHARED_REPO\`" >&2
×
1717
            echo "  which is forbidden. It will be skipped." >&2
×
1718
            echo ""
×
1719
            echo "  You can only have local paths for shared hooks defined" >&2
×
1720
            echo "  in the local or global Git configuration." >&2
×
1721
            echo ""
×
1722
            echo "  This can be achieved by running" >&2
×
1723
            echo "    \$ git hooks shared add [--local|--global] \"$SHARED_REPO\"" >&2
×
1724
            echo "  and deleting it from the \`.shared\` file by" >&2
×
1725
            echo "    \$ git hooks shared remove --shared \"$SHARED_REPO\"" >&2
×
1726
            continue
×
1727
        fi
1728

1729
        if [ -d "$SHARED_ROOT/.git" ]; then
17✔
1730
            echo "* Updating shared hooks from: $SHARED_REPO"
2✔
1731

1732
            # shellcheck disable=SC2086
1733
            PULL_OUTPUT="$(execute_git "$SHARED_ROOT" pull 2>&1)"
4✔
1734

1735
            # shellcheck disable=SC2181
1736
            if [ $? -ne 0 ]; then
2✔
1737
                echo "! Update failed, git pull output:" >&2
×
1738
                echo "$PULL_OUTPUT" >&2
×
1739
            fi
1740
        else
1741
            echo "* Retrieving shared hooks from: $SHARED_REPO_CLONE_URL"
15✔
1742

1743
            ADD_ARGS=""
15✔
1744
            [ "$SHARED_REPO_IS_LOCAL" != "true" ] && ADD_ARGS="--depth=1"
26✔
1745

1746
            [ -d "$SHARED_ROOT" ] &&
15✔
1747
                rm -rf "$SHARED_ROOT" &&
×
1748
                mkdir -p "$SHARED_ROOT"
×
1749

1750
            if [ -n "$SHARED_REPO_CLONE_BRANCH" ]; then
15✔
1751
                # shellcheck disable=SC2086
UNCOV
1752
                CLONE_OUTPUT=$(git clone \
×
1753
                    -c core.hooksPath=/dev/null \
1754
                    --template=/dev/null \
1755
                    --single-branch \
1756
                    --branch "$SHARED_REPO_CLONE_BRANCH" \
1757
                    $ADD_ARGS \
1758
                    "$SHARED_REPO_CLONE_URL" \
1759
                    "$SHARED_ROOT" 2>&1)
1760
            else
1761
                # shellcheck disable=SC2086
1762
                CLONE_OUTPUT=$(git clone \
×
1763
                    -c core.hooksPath=/dev/null \
1764
                    --template=/dev/null \
1765
                    --single-branch \
1766
                    $ADD_ARGS \
1767
                    "$SHARED_REPO_CLONE_URL" \
1768
                    "$SHARED_ROOT" 2>&1)
1769
            fi
1770

1771
            # shellcheck disable=SC2181
1772
            if [ $? -ne 0 ]; then
15✔
1773
                echo "! Clone failed, git clone output:" >&2
×
1774
                echo "$CLONE_OUTPUT" >&2
×
1775
            fi
1776
        fi
1777

1778
        IFS="$IFS_NEWLINE"
17✔
1779
    done
1780

1781
    unset IFS
26✔
1782
}
1783

1784
#####################################################
1785
# Executes an ondemand installation
1786
#   of the latest Githooks version.
1787
#
1788
# Returns:
1789
#   1 if the installation fails,
1790
#   0 otherwise
1791
#####################################################
1792
run_ondemand_installation() {
1793
    if [ "$1" = "help" ]; then
8✔
1794
        print_help_header
2✔
1795
        echo "
2✔
1796
git hooks install [--global]
2✔
1797

2✔
1798
    Installs the Githooks hooks into the current repository.
2✔
1799
    If the \`--global\` flag is given, it executes the installation
2✔
1800
    globally, including the hook templates for future repositories.
2✔
1801
"
2✔
1802
        return
2✔
1803
    fi
1804

1805
    INSTALL_FLAGS="--single"
6✔
1806
    if [ "$1" = "--global" ]; then
6✔
1807
        INSTALL_FLAGS=""
2✔
1808
    elif [ -n "$1" ]; then
4✔
1809
        echo "! Invalid argument: \`$1\`" >&2 && exit 1
×
1810
    fi
1811

1812
    if ! fetch_latest_updates; then
6✔
1813
        echo "! Failed to fetch the latest install script" >&2
×
1814
        echo "  You can retry manually using one of the alternative methods," >&2
×
1815
        echo "  see them here: https://github.com/rycus86/githooks#installation" >&2
×
1816
        exit 1
×
1817
    fi
1818

1819
    # shellcheck disable=SC2086
1820
    if ! execute_install_script $INSTALL_FLAGS; then
6✔
1821
        echo "! Failed to execute the installation" >&2
2✔
1822
        exit 1
2✔
1823
    fi
1824
}
1825

1826
#####################################################
1827
# Executes an ondemand uninstallation of Githooks.
1828
#
1829
# Returns:
1830
#   1 if the uninstallation fails,
1831
#   0 otherwise
1832
#####################################################
1833
run_ondemand_uninstallation() {
1834
    if [ "$1" = "help" ]; then
3✔
1835
        print_help_header
2✔
1836
        echo "
2✔
1837
git hooks uninstall [--global]
2✔
1838

2✔
1839
    Uninstalls the Githooks hooks from the current repository.
2✔
1840
    If the \`--global\` flag is given, it executes the uninstallation
2✔
1841
    globally, including the hook templates and all local repositories.
2✔
1842
"
2✔
1843
        return
2✔
1844
    fi
1845

1846
    UNINSTALL_ARGS="--single"
1✔
1847
    if [ "$1" = "--global" ]; then
1✔
1848
        UNINSTALL_ARGS="--global"
1✔
1849
    elif [ -n "$1" ]; then
×
1850
        echo "! Invalid argument: \`$1\`" >&2 && exit 1
×
1851
    fi
1852

1853
    if ! execute_uninstall_script $UNINSTALL_ARGS; then
1✔
1854
        echo "! Failed to execute the uninstallation" >&2
×
1855
        exit 1
×
1856
    fi
1857
}
1858

1859
#####################################################
1860
# Executes an update check, and potentially
1861
#   the installation of the latest version.
1862
#
1863
# Returns:
1864
#   1 if the latest version cannot be retrieved,
1865
#   0 otherwise
1866
#####################################################
1867
run_update_check() {
1868
    if [ "$1" = "help" ]; then
12✔
1869
        print_help_header
2✔
1870
        echo "
2✔
1871
git hooks update [force]
2✔
1872
git hooks update [enable|disable]
2✔
1873

2✔
1874
    Executes an update check for a newer Githooks version.
2✔
1875
    If it finds one, or if \`force\` was given, the downloaded
2✔
1876
    install script is executed for the latest version.
2✔
1877
    The \`enable\` and \`disable\` options enable or disable
2✔
1878
    the automatic checks that would normally run daily
2✔
1879
    after a successful commit event.
2✔
1880
"
2✔
1881
        return 0
2✔
1882
    fi
1883

1884
    if [ "$1" = "enable" ]; then
10✔
1885
        git config --global githooks.autoupdate.enabled true &&
2✔
1886
            echo "Automatic update checks have been enabled" &&
2✔
1887
            return 0
2✔
1888

1889
        echo "! Failed to enable automatic updates" >&2 && exit 1
×
1890

1891
    elif [ "$1" = "disable" ]; then
8✔
1892
        git config --global githooks.autoupdate.enabled false &&
2✔
1893
            echo "Automatic update checks have been disabled" &&
2✔
1894
            return 0
2✔
1895

1896
        echo "! Failed to disable automatic updates" >&2 && exit 1
×
1897

1898
    elif [ -n "$1" ] && [ "$1" != "force" ]; then
9✔
1899
        echo "! Invalid operation: \`$1\`" >&2 && exit 1
2✔
1900

1901
    fi
1902

1903
    record_update_time
5✔
1904

1905
    if ! fetch_latest_updates; then
5✔
1906
        echo "! Failed to check for updates: cannot fetch updates"
×
1907
        exit 1
×
1908
    fi
1909

1910
    if [ "$1" != "force" ]; then
5✔
1911
        if ! is_update_available; then
3✔
1912
            echo "  Githooks is already on the latest version"
1✔
1913
            return 0
1✔
1914
        fi
1915
    fi
1916

1917
    # shellcheck disable=SC2086
1918
    if ! execute_install_script $INSTALL_FLAGS; then
4✔
1919
        echo "! Failed to execute the installation"
×
1920
        print_update_disable_info
×
1921
        return 1
×
1922
    fi
1923
    return 0
4✔
1924
}
1925

1926
#####################################################
1927
# Saves the last update time into the
1928
#   githooks.autoupdate.lastrun global Git config.
1929
#
1930
# Returns:
1931
#   None
1932
#####################################################
1933
record_update_time() {
1934
    git config --global githooks.autoupdate.lastrun "$(date +%s)"
10✔
1935
}
1936

1937
#####################################################
1938
# Returns the script path e.g. `run` for the app
1939
#   `$1`
1940
#
1941
# Returns:
1942
#   0 and "$INSTALL_DIR/tools/$1/run"
1943
#   1 and "" otherwise
1944
#####################################################
1945
get_tool_script() {
1946
    if [ -f "$INSTALL_DIR/tools/$1/run" ]; then
×
1947
        echo "$INSTALL_DIR/tools/$1/run" && return 0
×
1948
    fi
1949
    return 1
×
1950
}
1951

1952
#####################################################
1953
# Call a script "$1". If it is not executable
1954
# call it as a shell script.
1955
#
1956
# Returns:
1957
#   Error code of the script.
1958
#####################################################
1959
call_script() {
1960
    SCRIPT="$1"
×
1961
    shift
×
1962

1963
    if [ -x "$SCRIPT" ]; then
×
1964
        "$SCRIPT" "$@"
×
1965
    else
1966
        sh "$SCRIPT" "$@"
×
1967

1968
    fi
1969
    return $?
×
1970
}
1971

1972
#####################################################
1973
# Does a update clone repository exist in the
1974
#  install folder
1975
#
1976
# Returns: 0 if `true`, 1 otherwise
1977
#####################################################
1978
is_release_clone_existing() {
1979
    if git -C "$INSTALL_DIR/release" rev-parse >/dev/null 2>&1; then
×
1980
        return 0
×
1981
    fi
1982
    return 1
×
1983
}
1984

1985
#####################################################
1986
# Checks if there is an update in the release clone
1987
#   waiting for a fast-forward merge.
1988
#
1989
# Returns:
1990
#   0 if an update needs to be applied, 1 otherwise
1991
#####################################################
1992
is_update_available() {
1993
    [ "$GITHOOKS_CLONE_UPDATE_AVAILABLE" = "true" ] || return 1
4✔
1994
}
1995

1996
#####################################################
1997
# Fetches updates in the release clone.
1998
#   If the release clone is newly created the variable
1999
#   `$GITHOOKS_CLONE_CREATED` is set to
2000
#   `true`
2001
#   If an update is available
2002
#   `GITHOOKS_CLONE_UPDATE_AVAILABLE` is set to `true`
2003
#
2004
# Returns:
2005
#   1 if failed, 0 otherwise
2006
#####################################################
2007
fetch_latest_updates() {
2008

2009
    echo "^ Checking for updates ..."
11✔
2010

2011
    GITHOOKS_CLONE_CREATED="false"
11✔
2012
    GITHOOKS_CLONE_UPDATE_AVAILABLE="false"
11✔
2013

2014
    GITHOOKS_CLONE_URL=$(git config --global githooks.cloneUrl)
22✔
2015
    GITHOOKS_CLONE_BRANCH=$(git config --global githooks.cloneBranch)
22✔
2016

2017
    # We do a fresh clone if there is not a repository
2018
    # or wrong url/branch configured and the user agrees.
2019
    if is_git_repo "$GITHOOKS_CLONE_DIR"; then
11✔
2020

2021
        URL=$(execute_git "$GITHOOKS_CLONE_DIR" config remote.origin.url 2>/dev/null)
20✔
2022
        BRANCH=$(execute_git "$GITHOOKS_CLONE_DIR" symbolic-ref -q --short HEAD 2>/dev/null)
20✔
2023

2024
        if [ "$URL" != "$GITHOOKS_CLONE_URL" ] ||
10✔
2025
            [ "$BRANCH" != "$GITHOOKS_CLONE_BRANCH" ]; then
10✔
2026

2027
            CREATE_NEW_CLONE="false"
×
2028

2029
            echo "! Cannot fetch updates because \`origin\` of update clone" >&2
×
2030
            echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
2031
            echo "  points to url:" >&2
×
2032
            echo "  \`$URL\`" >&2
×
2033
            echo "  on branch \`$BRANCH\`" >&2
×
2034
            echo "  which is not configured." >&2
×
2035

2036
            echo "Do you want to delete and reclone the existing update clone? [N/y]"
×
2037
            read -r ANSWER </dev/tty
×
2038

2039
            if [ "$ANSWER" = "y" ] || [ "$ANSWER" = "Y" ]; then
×
2040
                CREATE_NEW_CLONE="true"
×
2041
            fi
2042

2043
            if [ "$CREATE_NEW_CLONE" != "true" ]; then
×
2044
                echo "! See \`git hooks config [set|print] clone-url\` and" >&2
×
2045
                echo "      \`git hooks config [set|print] clone-branch\`" >&2
×
2046
                echo "  Either fix this or delete the clone" >&2
×
2047
                echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
2048
                echo "  to trigger a new checkout." >&2
×
2049
                return 1
×
2050
            fi
2051
        fi
2052

2053
        # Check if the update clone is dirty which it really should not.
2054
        if ! execute_git "$GITHOOKS_CLONE_DIR" diff-index --quiet HEAD >/dev/null 2>&1; then
10✔
2055
            echo "! Cannot pull updates because the update clone" >&2
×
2056
            echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
2057
            echo "  is dirty!" >&2
×
2058

2059
            echo "Do you want to delete and reclone the existing update clone? [N/y]"
×
2060
            read -r ANSWER </dev/tty
×
2061

2062
            if [ "$ANSWER" = "y" ] || [ "$ANSWER" = "Y" ]; then
×
2063
                CREATE_NEW_CLONE="true"
×
2064
            fi
2065

2066
            if [ "$CREATE_NEW_CLONE" != "true" ]; then
×
2067
                echo "! Either fix this or delete the clone" >&2
×
2068
                echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
2069
                echo "  to trigger a new checkout." >&2
×
2070
                return 1
×
2071
            fi
2072
        fi
2073
    else
2074
        CREATE_NEW_CLONE="true"
1✔
2075
    fi
2076

2077
    if [ "$CREATE_NEW_CLONE" = "true" ]; then
11✔
2078
        clone_release_repository || return 1
1✔
2079

2080
        # shellcheck disable=SC2034
2081
        GITHOOKS_CLONE_CREATED="true"
1✔
2082
        GITHOOKS_CLONE_UPDATE_AVAILABLE="true"
1✔
2083
    else
2084

2085
        FETCH_OUTPUT=$(
×
2086
            execute_git "$GITHOOKS_CLONE_DIR" fetch origin "$GITHOOKS_CLONE_BRANCH" 2>&1
×
2087
        )
2088

2089
        # shellcheck disable=SC2181
2090
        if [ $? -ne 0 ]; then
10✔
2091
            echo "! Fetching updates in  \`$GITHOOKS_CLONE_DIR\` failed with:" >&2
×
2092
            echo "$FETCH_OUTPUT" >&2
×
2093
            return 1
×
2094
        fi
2095

2096
        RELEASE_COMMIT=$(execute_git "$GITHOOKS_CLONE_DIR" rev-parse "$GITHOOKS_CLONE_BRANCH")
20✔
2097
        UPDATE_COMMIT=$(execute_git "$GITHOOKS_CLONE_DIR" rev-parse "origin/$GITHOOKS_CLONE_BRANCH")
20✔
2098

2099
        if [ "$RELEASE_COMMIT" != "$UPDATE_COMMIT" ]; then
10✔
2100
            # We have an update available
2101
            # install.sh deals with updating ...
2102
            GITHOOKS_CLONE_UPDATE_AVAILABLE="true"
5✔
2103
        fi
2104
    fi
2105

2106
    return 0
11✔
2107
}
2108

2109
############################################################
2110
# Checks whether the given directory
2111
#   is a Git repository (bare included) or not.
2112
#
2113
# Returns:
2114
#   1 if failed, 0 otherwise
2115
############################################################
2116
is_git_repo() {
2117
    git -C "$1" rev-parse >/dev/null 2>&1 || return 1
24✔
2118
}
2119

2120
############################################################
2121
# Checks whether the given directory
2122
#   is a Git bare repository.
2123
#
2124
# Returns:
2125
#   1 if failed, 0 otherwise
2126
############################################################
2127
is_bare_repo() {
2128
    [ "$(git -C "$1" rev-parse --is-bare-repository 2>/dev/null)" = "true" ] || return 1
495✔
2129
}
2130

2131
#####################################################
2132
# Safely execute a git command in the standard
2133
#   clone dir `$1`.
2134
#
2135
# Returns: Error code from `git`
2136
#####################################################
2137
execute_git() {
2138
    REPO="$1"
71✔
2139
    shift
71✔
2140

2141
    git -C "$REPO" \
71✔
2142
        --work-tree="$REPO" \
2143
        --git-dir="$REPO/.git" \
2144
        -c core.hooksPath=/dev/null \
2145
        "$@"
2146
}
2147

2148
#####################################################
2149
#  Creates the release clone if needed.
2150
# Returns:
2151
#   1 if failed, 0 otherwise
2152
#####################################################
2153
assert_release_clone() {
2154

2155
    # We do a fresh clone if there is no clone
2156
    if ! is_git_repo "$GITHOOKS_CLONE_DIR"; then
12✔
2157
        clone_release_repository || return 1
×
2158
    fi
2159

2160
    return 0
12✔
2161
}
2162

2163
############################################################
2164
# Clone the URL `$GITHOOKS_CLONE_URL` into the install
2165
# folder `$INSTALL_DIR/release` for further updates.
2166
#
2167
# Returns: 0 if succesful, 1 otherwise
2168
############################################################
2169
clone_release_repository() {
2170

2171
    GITHOOKS_CLONE_URL=$(git config --global githooks.cloneUrl)
2✔
2172
    GITHOOKS_CLONE_BRANCH=$(git config --global githooks.cloneBranch)
2✔
2173

2174
    if [ -z "$GITHOOKS_CLONE_URL" ]; then
1✔
2175
        GITHOOKS_CLONE_URL="https://github.com/rycus86/githooks.git"
1✔
2176
    fi
2177

2178
    if [ -z "$GITHOOKS_CLONE_BRANCH" ]; then
1✔
2179
        GITHOOKS_CLONE_BRANCH="master"
1✔
2180
    fi
2181

2182
    if [ -d "$GITHOOKS_CLONE_DIR" ]; then
1✔
2183
        if ! rm -rf "$GITHOOKS_CLONE_DIR" >/dev/null 2>&1; then
×
2184
            echo "! Failed to remove an existing githooks release repository" >&2
×
2185
            return 1
×
2186
        fi
2187
    fi
2188

2189
    echo "Cloning \`$GITHOOKS_CLONE_URL\` to \`$GITHOOKS_CLONE_DIR\` ..."
1✔
2190

2191
    CLONE_OUTPUT=$(git clone \
×
2192
        -c core.hooksPath=/dev/null \
2193
        --template=/dev/null \
2194
        --depth=1 \
2195
        --single-branch \
2196
        --branch "$GITHOOKS_CLONE_BRANCH" \
2197
        "$GITHOOKS_CLONE_URL" "$GITHOOKS_CLONE_DIR" 2>&1)
2198

2199
    # shellcheck disable=SC2181
2200
    if [ $? -ne 0 ]; then
1✔
2201
        echo "! Cloning \`$GITHOOKS_CLONE_URL\` to \`$GITHOOKS_CLONE_DIR\` failed with output: " >&2
×
2202
        echo "$CLONE_OUTPUT" >&2
×
2203
        return 1
×
2204
    fi
2205

2206
    git config --global githooks.cloneUrl "$GITHOOKS_CLONE_URL"
1✔
2207
    git config --global githooks.cloneBranch "$GITHOOKS_CLONE_BRANCH"
1✔
2208

2209
    return 0
1✔
2210
}
2211

2212
#####################################################
2213
# Checks if updates are enabled.
2214
#
2215
# Returns:
2216
#   0 if updates are enabled, 1 otherwise
2217
#####################################################
2218
is_autoupdate_enabled() {
2219
    [ "$(git config --global githooks.autoupdate.enabled)" = "Y" ] || return 1
×
2220
}
2221

2222
#####################################################
2223
# Performs the installation of the previously
2224
#   fetched install script.
2225
#
2226
# Returns:
2227
#   0 if the installation was successful, 1 otherwise
2228
#####################################################
2229
execute_install_script() {
2230

2231
    if ! assert_release_clone; then
10✔
2232
        echo "! Could not create a release clone in \`$GITHOOKS_CLONE_DIR\`" >&2
×
2233
        exit 1
×
2234
    fi
2235

2236
    # Set the install script
2237
    INSTALL_SCRIPT="$INSTALL_DIR/release/install.sh"
10✔
2238
    if [ ! -f "$INSTALL_SCRIPT" ]; then
10✔
2239
        echo "! Non-existing \`install.sh\` in  \`$INSTALL_DIR/release\`" >&2
×
2240
        return 1
×
2241
    fi
2242

2243
    sh -s -- "$@" <"$INSTALL_SCRIPT" || return 1
12✔
2244
    return 0
8✔
2245
}
2246

2247
#####################################################
2248
# Performs the uninstallation of the previously
2249
#   fetched uninstall script.
2250
#
2251
# Returns:
2252
#   0 if the uninstallation was successful,
2253
#   1 otherwise
2254
#####################################################
2255
execute_uninstall_script() {
2256

2257
    # Set the install script
2258
    UNINSTALL_SCRIPT="$INSTALL_DIR/release/uninstall.sh"
1✔
2259
    if [ ! -f "$UNINSTALL_SCRIPT" ]; then
1✔
2260
        echo "! Non-existing \`uninstall.sh\` in  \`$INSTALL_DIR/release\`" >&2
×
2261
        return 1
×
2262
    fi
2263

2264
    sh -s -- "$@" <"$UNINSTALL_SCRIPT" || return 1
1✔
2265

2266
    return 0
1✔
2267
}
2268

2269
#####################################################
2270
# Prints some information on how to disable
2271
#   automatic update checks.
2272
#
2273
# Returns:
2274
#   None
2275
#####################################################
2276
print_update_disable_info() {
2277
    echo "  If you would like to disable auto-updates, run:"
×
2278
    echo "    \$ git hooks update disable"
×
2279
}
2280

2281
#####################################################
2282
# Adds or updates the Githooks README in
2283
#   the current local repository.
2284
#
2285
# Returns:
2286
#   1 on failure, 0 otherwise
2287
#####################################################
2288
manage_readme_file() {
2289
    case "$1" in
6✔
2290
    "add")
2291
        FORCE_README=""
3✔
2292
        ;;
2293
    "update")
2294
        FORCE_README="y"
1✔
2295
        ;;
2296
    *)
2297
        print_help_header
2✔
2298
        echo "
2✔
2299
git hooks readme [add|update]
2✔
2300

2✔
2301
    Adds or updates the Githooks README in the \`.githooks\` folder.
2✔
2302
    If \`add\` is used, it checks first if there is a README file already.
2✔
2303
    With \`update\`, the file is always updated, creating it if necessary.
2✔
2304
    This command needs to be run at the root of a repository.
2✔
2305
"
2✔
2306
        if [ "$1" = "help" ]; then
2✔
2307
            exit 0
2✔
2308
        else
2309
            exit 1
×
2310
        fi
2311
        ;;
2312
    esac
2313

2314
    if ! is_running_in_git_repo_root; then
4✔
2315
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2316
        exit 1
1✔
2317
    fi
2318

2319
    if [ -f .githooks/README.md ] && [ "$FORCE_README" != "y" ]; then
4✔
2320
        echo "! This repository already seems to have a Githooks README." >&2
1✔
2321
        echo "  If you would like to replace it with the latest one, please run \`git hooks readme update\`" >&2
1✔
2322
        exit 1
1✔
2323
    fi
2324

2325
    if ! assert_release_clone; then
2✔
2326
        exit 1
×
2327
    fi
2328

2329
    README_FILE="$INSTALL_DIR/release/.githooks/README.md"
2✔
2330
    mkdir -p "$(pwd)/.githooks" &&
4✔
2331
        cat "$README_FILE" >"$(pwd)/.githooks/README.md" &&
4✔
2332
        echo "The README file is updated." &&
2✔
2333
        echo_if_non_bare_repo "  Do not forget to commit and push it!" ||
2✔
2334
        echo "! Failed to update the README file in the current repository" >&2
×
2335
}
2336

2337
#####################################################
2338
# Adds or updates Githooks ignore files in
2339
#   the current local repository.
2340
#
2341
# Returns:
2342
#   1 on failure, 0 otherwise
2343
#####################################################
2344
manage_ignore_files() {
2345
    if [ "$1" = "help" ]; then
14✔
2346
        print_help_header
4✔
2347
        echo "
4✔
2348
git hooks ignore [pattern...]
4✔
2349
git hooks ignore [trigger] [pattern...]
4✔
2350

4✔
2351
    Adds new file name patterns to the Githooks \`.ignore\` file, either
4✔
2352
    in the main \`.githooks\` folder, or in the Git event specific one.
4✔
2353
    Note, that it may be required to surround the individual pattern
4✔
2354
    parameters with single quotes to avoid expanding or splitting them.
4✔
2355
    The \`trigger\` parameter should be the name of the Git event if given.
4✔
2356
    This command needs to be run at the root of a repository.
4✔
2357
"
4✔
2358
        return
4✔
2359
    fi
2360

2361
    if ! is_running_in_git_repo_root; then
10✔
2362
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2363
        exit 1
1✔
2364
    fi
2365

2366
    TRIGGER_TYPES="
2367
        applypatch-msg pre-applypatch post-applypatch
2368
        pre-commit prepare-commit-msg commit-msg post-commit
2369
        pre-rebase post-checkout post-merge pre-push
2370
        pre-receive update post-receive post-update
2371
        push-to-checkout pre-auto-gc post-rewrite sendemail-validate"
9✔
2372

2373
    TARGET_DIR="$(pwd)/.githooks"
18✔
2374

2375
    for TRIGGER_TYPE in $TRIGGER_TYPES; do
114✔
2376
        if [ "$1" = "$TRIGGER_TYPE" ]; then
114✔
2377
            TARGET_DIR="$(pwd)/.githooks/$TRIGGER_TYPE"
8✔
2378
            shift
4✔
2379
            break
4✔
2380
        fi
2381
    done
2382

2383
    if [ -z "$1" ]; then
9✔
2384
        manage_ignore_files "help"
2✔
2385
        echo "! Missing pattern parameter" >&2
2✔
2386
        exit 1
2✔
2387
    fi
2388

2389
    if ! mkdir -p "$TARGET_DIR" && touch "$TARGET_DIR/.ignore"; then
7✔
2390
        echo "! Failed to prepare the ignore file at $TARGET_DIR/.ignore" >&2
×
2391
        exit 1
×
2392
    fi
2393

2394
    [ -f "$TARGET_DIR/.ignore" ] &&
7✔
2395
        echo "" >>"$TARGET_DIR/.ignore"
2✔
2396

2397
    for PATTERN in "$@"; do
7✔
2398
        if ! echo "$PATTERN" >>"$TARGET_DIR/.ignore"; then
7✔
2399
            echo "! Failed to update the ignore file at $TARGET_DIR/.ignore" >&2
1✔
2400
            exit 1
1✔
2401
        fi
2402
    done
2403

2404
    echo "The ignore file at $TARGET_DIR/.ignore is updated"
6✔
2405
    echo_if_non_bare_repo "  Do not forget to commit the changes!"
6✔
2406
}
2407

2408
#####################################################
2409
# Manages various Githooks settings,
2410
#   that is stored in Git configuration.
2411
#
2412
# Returns:
2413
#   1 on failure, 0 otherwise
2414
#####################################################
2415
manage_configuration() {
2416
    if [ "$1" = "help" ]; then
106✔
2417
        print_help_header
10✔
2418
        echo "
10✔
2419
git hooks config list [--local|--global]
10✔
2420

10✔
2421
    Lists the Githooks related settings of the Githooks configuration.
10✔
2422
    Can be either global or local configuration, or both by default.
10✔
2423

10✔
2424
git hooks config [set|reset|print] disable
10✔
2425

10✔
2426
    Disables running any Githooks files in the current repository,
10✔
2427
    when the \`set\` option is used.
10✔
2428
    The \`reset\` option clears this setting.
10✔
2429
    The \`print\` option outputs the current setting.
10✔
2430
    This command needs to be run at the root of a repository.
10✔
2431

10✔
2432
[deprecated] git hooks config [set|reset|print] single
10✔
2433

10✔
2434
    This command is deprecated and will be removed in the future.
10✔
2435
    Marks the current local repository to be managed as a single Githooks
10✔
2436
    installation, or clears the marker, with \`set\` and \`reset\` respectively.
10✔
2437
    The \`print\` option outputs the current setting of it.
10✔
2438
    This command needs to be run at the root of a repository.
10✔
2439

10✔
2440
git hooks config set search-dir <path>
10✔
2441
git hooks config [reset|print] search-dir
10✔
2442

10✔
2443
    Changes the previous search directory setting used during installation.
10✔
2444
    The \`set\` option changes the value, and the \`reset\` option clears it.
10✔
2445
    The \`print\` option outputs the current setting of it.
10✔
2446

10✔
2447
git hooks config set shared [--local] <git-url...>
10✔
2448
git hooks config [reset|print] shared [--local]
10✔
2449

10✔
2450
    Updates the list of global (or local) shared hook repositories when
10✔
2451
    the \`set\` option is used, which accepts multiple <git-url> arguments,
10✔
2452
    each containing a clone URL of a hook repository.
10✔
2453
    The \`reset\` option clears this setting.
10✔
2454
    The \`print\` option outputs the current setting.
10✔
2455

10✔
2456
git hooks config [accept|deny|reset|print] trusted
10✔
2457

10✔
2458
    Accepts changes to all existing and new hooks in the current repository
10✔
2459
    when the trust marker is present and the \`set\` option is used.
10✔
2460
    The \`deny\` option marks the repository as
10✔
2461
    it has refused to trust the changes, even if the trust marker is present.
10✔
2462
    The \`reset\` option clears this setting.
10✔
2463
    The \`print\` option outputs the current setting.
10✔
2464
    This command needs to be run at the root of a repository.
10✔
2465

10✔
2466
git hooks config [enable|disable|reset|print] update
10✔
2467

10✔
2468
    Enables or disables automatic update checks with
10✔
2469
    the \`enable\` and \`disable\` options respectively.
10✔
2470
    The \`reset\` option clears this setting.
10✔
2471
    The \`print\` option outputs the current setting.
10✔
2472

10✔
2473
git hooks config set clone-url <git-url>
10✔
2474
git hooks config [set|print] clone-url
10✔
2475

10✔
2476
    Sets or prints the configured githooks clone url used
10✔
2477
    for any update.
10✔
2478

10✔
2479
git hooks config set clone-branch <branch-name>
10✔
2480
git hooks config print clone-branch
10✔
2481

10✔
2482
    Sets or prints the configured branch of the update clone
10✔
2483
    used for any update.
10✔
2484

10✔
2485
git hooks config [reset|print] update-time
10✔
2486

10✔
2487
    Resets the last Githooks update time with the \`reset\` option,
10✔
2488
    causing the update check to run next time if it is enabled.
10✔
2489
    Use \`git hooks update [enable|disable]\` to change that setting.
10✔
2490
    The \`print\` option outputs the current value of it.
10✔
2491

10✔
2492
git hooks config [enable|disable|print] fail-on-non-existing-shared-hooks [--local|--global]
10✔
2493

10✔
2494
Enable or disable failing hooks with an error when any
10✔
2495
shared hooks configured in \`.shared\` are missing,
10✔
2496
which usually means \`git hooks update\` has not been called yet.
10✔
2497

10✔
2498
git hooks config [yes|no|reset|print] delete-detected-lfs-hooks
10✔
2499

10✔
2500
By default, detected LFS hooks during install are disabled and backed up.
10✔
2501
The \`yes\` option remembers to always delete these hooks.
10✔
2502
The \`no\` option remembers the default behavior.
10✔
2503
The decision is reset with \`reset\` to the default behavior.
10✔
2504
The \`print\` option outputs the current behavior.
10✔
2505
"
10✔
2506
        return
10✔
2507
    fi
2508

2509
    CONFIG_OPERATION="$1"
96✔
2510

2511
    if [ "$CONFIG_OPERATION" = "list" ]; then
96✔
2512
        if [ "$2" = "--local" ] && ! is_running_in_git_repo_root; then
6✔
2513
            echo "! Local configuration can only be printed from a Git repository" >&2
1✔
2514
            exit 1
1✔
2515
        fi
2516

2517
        if [ -z "$2" ]; then
4✔
2518
            git config --get-regexp "(^githooks|alias.hooks)" | sort
4✔
2519
        else
2520
            git config "$2" --get-regexp "(^githooks|alias.hooks)" | sort
4✔
2521
        fi
2522
        exit $?
4✔
2523
    fi
2524

2525
    CONFIG_ARGUMENT="$2"
91✔
2526

2527
    [ "$#" -ge 1 ] && shift
181✔
2528
    [ "$#" -ge 1 ] && shift
181✔
2529

2530
    case "$CONFIG_ARGUMENT" in
91✔
2531
    "disable")
2532
        config_disable "$CONFIG_OPERATION"
12✔
2533
        ;;
2534
    "search-dir")
2535
        config_search_dir "$CONFIG_OPERATION" "$@"
10✔
2536
        ;;
2537
    "shared")
2538
        config_shared_hook_repos "$CONFIG_OPERATION" "$@"
15✔
2539
        ;;
2540
    "trusted")
2541
        config_trust_all_hooks "$CONFIG_OPERATION"
17✔
2542
        ;;
2543
    "update")
2544
        config_update_state "$CONFIG_OPERATION" "$@"
15✔
2545
        ;;
2546
    "clone-url")
2547
        config_update_clone_url "$CONFIG_OPERATION" "$@"
×
2548
        ;;
2549
    "clone-branch")
2550
        config_update_clone_branch "$CONFIG_OPERATION" "$@"
×
2551
        ;;
2552
    "update-time")
2553
        config_update_last_run "$CONFIG_OPERATION"
9✔
2554
        ;;
2555
    "fail-on-non-existing-shared-hooks")
2556
        config_fail_on_not_existing_shared_hooks "$CONFIG_OPERATION" "$@"
5✔
2557
        ;;
2558
    "delete-detected-lfs-hooks")
2559
        config_delete_detected_lfs_hooks "$CONFIG_OPERATION" "$@"
3✔
2560
        ;;
2561
    *)
2562
        manage_configuration "help"
7✔
2563
        echo "! Invalid configuration option: \`$CONFIG_ARGUMENT\`" >&2
5✔
2564
        exit 1
6✔
2565
        ;;
2566
    esac
2567
}
2568

2569
#####################################################
2570
# Manages Githooks disable settings for
2571
#   the current repository.
2572
# Prints or modifies the \`githooks.disable\`
2573
#   local Git configuration.
2574
#####################################################
2575
config_disable() {
2576
    if ! is_running_in_git_repo_root; then
12✔
2577
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2578
        exit 1
1✔
2579
    fi
2580

2581
    if [ "$1" = "set" ]; then
11✔
2582
        git config githooks.disable true
4✔
2583
    elif [ "$1" = "reset" ]; then
7✔
2584
        git config --unset githooks.disable
2✔
2585
    elif [ "$1" = "print" ]; then
5✔
2586
        if is_repository_disabled; then
4✔
2587
            echo "Githooks is disabled in the current repository"
2✔
2588
        else
2589
            echo "Githooks is NOT disabled in the current repository"
2✔
2590
        fi
2591
    else
2592
        echo "! Invalid operation: \`$1\` (use \`set\`, \`reset\` or \`print\`)" >&2
1✔
2593
        exit 1
2✔
2594
    fi
2595
}
2596

2597
#####################################################
2598
# Manages previous search directory setting
2599
#   used during Githooks installation.
2600
# Prints or modifies the
2601
#   \`githooks.previousSearchDir\`
2602
#   global Git configuration.
2603
#####################################################
2604
config_search_dir() {
2605
    if [ "$1" = "set" ]; then
10✔
2606
        if [ -z "$2" ]; then
3✔
2607
            manage_configuration "help"
1✔
2608
            echo "! Missing <path> parameter" >&2
1✔
2609
            exit 1
1✔
2610
        fi
2611

2612
        git config --global githooks.previousSearchDir "$2"
2✔
2613
    elif [ "$1" = "reset" ]; then
7✔
2614
        git config --global --unset githooks.previousSearchDir
2✔
2615
    elif [ "$1" = "print" ]; then
5✔
2616
        CONFIG_SEARCH_DIR=$(git config --global --get githooks.previousSearchDir)
8✔
2617
        if [ -z "$CONFIG_SEARCH_DIR" ]; then
4✔
2618
            echo "No previous search directory is set"
2✔
2619
        else
2620
            echo "Search directory is set to: $CONFIG_SEARCH_DIR"
2✔
2621
        fi
2622
    else
2623
        echo "! Invalid operation: \`$1\` (use \`set\`, \`reset\` or \`print\`)" >&2
2✔
2624
        exit 1
1✔
2625
    fi
2626
}
2627

2628
#####################################################
2629
# Manages global shared hook repository list setting.
2630
# Prints or modifies the \`githooks.shared\`
2631
#   global Git configuration.
2632
#####################################################
2633
config_shared_hook_repos() {
2634

2635
    if [ "$1" = "set" ]; then
15✔
2636

2637
        SHARED_TYPE="--global"
4✔
2638
        [ "$2" = "--local" ] && SHARED_TYPE="$2" && shift
4✔
2639

2640
        if [ -z "$2" ]; then
4✔
2641
            manage_configuration "help"
1✔
2642
            echo "! Missing <git-url> parameter" >&2
1✔
2643
            exit 1
1✔
2644
        fi
2645

2646
        shift
3✔
2647

2648
        for SHARED_REPO_ITEM in "$@"; do
5✔
2649
            git config "$SHARED_TYPE" --add githooks.shared "$SHARED_REPO_ITEM"
5✔
2650
        done
2651

2652
    elif [ "$1" = "reset" ]; then
11✔
2653
        SHARED_TYPE="--global"
2✔
2654
        if echo "$2" | grep -qE "\-\-(local)"; then
4✔
2655
            SHARED_TYPE="$2"
×
2656
        elif [ -n "$2" ]; then
2✔
2657
            manage_configuration "help"
×
2658
            echo "! Wrong argument \`$2\`" >&2
×
2659
        fi
2660

2661
        git config "$SHARED_TYPE" --unset-all githooks.shared
2✔
2662

2663
    elif [ "$1" = "print" ]; then
9✔
2664
        SHARED_TYPE="--global"
8✔
2665
        if echo "$2" | grep -qE "\-\-(local)"; then
16✔
2666
            SHARED_TYPE="$2"
×
2667
        elif [ -n "$2" ]; then
8✔
2668
            manage_configuration "help"
×
2669
            echo "! Wrong argument $($2)" >&2
×
2670
        fi
2671
        list_shared_hook_repos "$SHARED_TYPE"
8✔
2672
    else
2673
        manage_configuration "help"
1✔
2674
        echo "! Invalid operation: \`$1\` (use \`set\`, \`reset\` or \`print\`)" >&2
1✔
2675
        exit 1
1✔
2676
    fi
2677
}
2678

2679
#####################################################
2680
# Manages the trust-all-hooks setting
2681
#   for the current repository.
2682
# Prints or modifies the \`githooks.trust.all\`
2683
#   local Git configuration.
2684
#####################################################
2685
config_trust_all_hooks() {
2686
    if ! is_running_in_git_repo_root; then
17✔
2687
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2688
        exit 1
1✔
2689
    fi
2690

2691
    if [ "$1" = "accept" ]; then
16✔
2692
        git config githooks.trust.all Y
4✔
2693
    elif [ "$1" = "deny" ]; then
12✔
2694
        git config githooks.trust.all N
3✔
2695
    elif [ "$1" = "reset" ]; then
9✔
2696
        git config --unset githooks.trust.all
2✔
2697
    elif [ "$1" = "print" ]; then
7✔
2698
        CONFIG_TRUST_ALL=$(git config --local --get githooks.trust.all)
12✔
2699
        if [ "$CONFIG_TRUST_ALL" = "Y" ]; then
6✔
2700
            echo "The current repository trusts all hooks automatically"
2✔
2701
        elif [ -z "$CONFIG_TRUST_ALL" ]; then
4✔
2702
            echo "The current repository does NOT have trust settings"
2✔
2703
        else
2704
            echo "The current repository does NOT trust hooks automatically"
2✔
2705
        fi
2706
    else
2707
        echo "! Invalid operation: \`$1\` (use \`accept\`, \`deny\`, \`reset\` or \`print\`)" >&2
1✔
2708
        exit 1
1✔
2709
    fi
2710
}
2711

2712
#####################################################
2713
# Manages the automatic update check setting.
2714
# Prints or modifies the
2715
#   \`githooks.autoupdate.enabled\`
2716
#   global Git configuration.
2717
#####################################################
2718
config_update_state() {
2719
    if [ "$1" = "enable" ]; then
15✔
2720
        git config --global githooks.autoupdate.enabled true
4✔
2721
    elif [ "$1" = "disable" ]; then
11✔
2722
        git config --global githooks.autoupdate.enabled false
2✔
2723
    elif [ "$1" = "reset" ]; then
9✔
2724
        git config --global --unset githooks.autoupdate.enabled
2✔
2725
    elif [ "$1" = "print" ]; then
7✔
2726
        CONFIG_UPDATE_ENABLED=$(git config --get githooks.autoupdate.enabled)
12✔
2727
        if [ "$CONFIG_UPDATE_ENABLED" = "true" ] ||
6✔
2728
            [ "$CONFIG_UPDATE_ENABLED" = "Y" ]; then
4✔
2729
            echo "Automatic update checks are enabled"
2✔
2730
        else
2731
            echo "Automatic update checks are NOT enabled"
4✔
2732
        fi
2733
    else
2734
        echo "! Invalid operation: \`$1\` (use \`enable\`, \`disable\`, \`reset\` or \`print\`)" >&2
1✔
2735
        exit 1
1✔
2736
    fi
2737
}
2738

2739
#####################################################
2740
# Manages the automatic update clone url.
2741
# Prints or modifies the
2742
#   \`githooks.cloneUrl\`
2743
#   global Git configuration.
2744
#####################################################
2745
config_update_clone_url() {
2746
    if [ "$1" = "print" ]; then
×
2747
        echo "Update clone url set to: $(git config --global githooks.cloneUrl)"
×
2748
    elif [ "$1" = "set" ]; then
×
2749
        if [ -z "$2" ]; then
×
2750
            echo "! No valid url given" >&2
×
2751
            exit 1
×
2752
        fi
2753
        git config --global githooks.cloneUrl "$2"
×
2754
        config_update_clone_url "print"
×
2755
    else
2756
        echo "! Invalid operation: \`$1\` (use \`set\`, or \`print\`)" >&2
×
2757
        exit 1
×
2758
    fi
2759
}
2760

2761
#####################################################
2762
# Manages the automatic update clone branch.
2763
# Prints or modifies the
2764
#   \`githooks.cloneUrl\`
2765
#   global Git configuration.
2766
#####################################################
2767
config_update_clone_branch() {
2768
    if [ "$1" = "print" ]; then
×
2769
        echo "Update clone branch set to: $(git config --global githooks.cloneBranch)"
×
2770
    elif [ "$1" = "set" ]; then
×
2771
        if [ -z "$2" ]; then
×
2772
            echo "! No valid branch name given" >&2
×
2773
            exit 1
×
2774
        fi
2775
        git config --global githooks.cloneBranch "$2"
×
2776
        config_update_clone_branch "print"
×
2777
    else
2778
        echo "! Invalid operation: \`$1\` (use \`set\`, or \`print\`)" >&2
×
2779
        exit 1
×
2780
    fi
2781
}
2782

2783
#####################################################
2784
# Manages the timestamp for the last update check.
2785
# Prints or modifies the
2786
#   \`githooks.autoupdate.lastrun\`
2787
#   global Git configuration.
2788
#####################################################
2789
config_update_last_run() {
2790
    if [ "$1" = "reset" ]; then
9✔
2791
        git config --global --unset githooks.autoupdate.lastrun
2✔
2792
    elif [ "$1" = "print" ]; then
7✔
2793
        LAST_UPDATE=$(git config --global --get githooks.autoupdate.lastrun)
12✔
2794
        if [ -z "$LAST_UPDATE" ]; then
6✔
2795
            echo "The update has never run"
4✔
2796
        else
2797
            if ! date --date="@${LAST_UPDATE}" 2>/dev/null; then
2✔
2798
                if ! date -j -f "%s" "$LAST_UPDATE" 2>/dev/null; then
×
2799
                    echo "Last update timestamp: $LAST_UPDATE"
×
2800
                fi
2801
            fi
2802
        fi
2803
    else
2804
        echo "! Invalid operation: \`$1\` (use \`reset\` or \`print\`)" >&2
1✔
2805
        exit 1
1✔
2806
    fi
2807
}
2808

2809
#####################################################
2810
# Manages the failOnNonExistingSharedHook switch.
2811
# Prints or modifies the
2812
#   `githooks.failOnNonExistingSharedHooks`
2813
#   local or global Git configuration.
2814
#####################################################
2815
config_fail_on_not_existing_shared_hooks() {
2816
    CONFIG="--local"
5✔
2817
    if [ -n "$2" ]; then
5✔
2818
        if [ "$2" = "--local" ] || [ "$2" = "--global" ]; then
7✔
2819
            CONFIG="$2"
4✔
2820
        else
2821
            echo "! Invalid option: \`$2\` (use \`--local\` or \`--global\`)" >&2
×
2822
            exit 1
×
2823
        fi
2824
    fi
2825

2826
    if [ "$1" = "enable" ]; then
5✔
2827
        if ! git config "$CONFIG" githooks.failOnNonExistingSharedHooks "true"; then
2✔
2828
            echo "! Failed to enable \`fail-on-non-existing-shared-hooks\`" >&2
×
2829
            exit 1
×
2830
        fi
2831

2832
        echo "Failing on not existing shared hooks is enabled"
2✔
2833

2834
    elif [ "$1" = "disable" ]; then
3✔
2835
        if ! git config "$CONFIG" githooks.failOnNonExistingSharedHooks "false"; then
×
2836
            echo "! Failed to disable \`fail-on-non-existing-shared-hooks\`" >&2
×
2837
            exit 1
×
2838
        fi
2839

2840
        echo "Failing on not existing shared hooks is disabled"
×
2841

2842
    elif [ "$1" = "print" ]; then
3✔
2843
        FAIL_ON_NOT_EXISTING=$(git config "$CONFIG" --get githooks.failOnNonExistingSharedHooks)
6✔
2844
        if [ "$FAIL_ON_NOT_EXISTING" = "true" ]; then
3✔
2845
            echo "Failing on not existing shared hooks is enabled"
2✔
2846
        else
2847
            # default also if it does not exist
2848
            echo "Failing on not existing shared hooks is disabled"
1✔
2849
        fi
2850

2851
    else
2852
        echo "! Invalid operation: \`$1\` (use \`enable\`, \`disable\` or \`print\`)" >&2
×
2853
        exit 1
×
2854
    fi
2855
}
2856

2857
#####################################################
2858
# Manages the deleteDetectedLFSHooks default bahavior.
2859
# Modifies or prints
2860
#   `githooks.deleteDetectedLFSHooks`
2861
#   global Git configuration.
2862
#####################################################
2863
config_delete_detected_lfs_hooks() {
2864
    if [ "$1" = "yes" ]; then
4✔
2865
        git config --global githooks.deleteDetectedLFSHooks "a"
×
2866
        config_delete_detected_lfs_hooks "print"
×
2867
    elif [ "$1" = "no" ]; then
4✔
2868
        git config --global githooks.deleteDetectedLFSHooks "n"
×
2869
        config_delete_detected_lfs_hooks "print"
×
2870
    elif [ "$1" = "reset" ]; then
4✔
2871
        git config --global --unset githooks.deleteDetectedLFSHooks
1✔
2872
        config_delete_detected_lfs_hooks "print"
1✔
2873
    elif [ "$1" = "print" ]; then
3✔
2874
        VALUE=$(git config --global githooks.deleteDetectedLFSHooks)
6✔
2875
        if [ "$VALUE" = "Y" ]; then
3✔
2876
            echo "Detected LFS hooks are by default deleted"
×
2877
        else
2878
            echo "Detected LFS hooks are by default disabled and backed up"
3✔
2879
        fi
2880
    else
2881
        echo "! Invalid operation: \`$1\` (use \`yes\`, \`no\` or \`reset\`)" >&2
×
2882
        exit 1
×
2883
    fi
2884
}
2885

2886
#####################################################
2887
# Manages the app script folders.
2888
#
2889
# Returns:
2890
#   1 on failure, 0 otherwise
2891
#####################################################
2892
manage_tools() {
2893
    if [ "$1" = "help" ]; then
5✔
2894
        print_help_header
2✔
2895
        echo "
2✔
2896
git hooks tools register <toolName> <scriptFolder>
2✔
2897

2✔
2898
    Install the script folder \`<scriptFolder>\` in
2✔
2899
    the installation directory under \`tools/<toolName>\`.
2✔
2900

2✔
2901
    Currently the following tools are supported:
2✔
2902

2✔
2903
    >> Dialog Tool (<toolName> = \"dialog\")
2✔
2904

2✔
2905
    The interface of the dialog tool is as follows.
2✔
2906

2✔
2907
    # if \`run\` is executable
2✔
2908
    \$ run <title> <text> <options> <long-options>
2✔
2909
    # otherwise, assuming \`run\` is a shell script
2✔
2910
    \$ sh run <title> <text> <options> <long-options>
2✔
2911

2✔
2912
    The arguments of the dialog tool are:
2✔
2913
    - \`<title>\` the title for the GUI dialog
2✔
2914
    - \`<text>\` the text for the GUI dialog
2✔
2915
    - \`<short-options>\` the button return values, slash-delimited,
2✔
2916
        e.g. \`Y/n/d\`.
2✔
2917
        The default button is the first capital character found.
2✔
2918
    - \`<long-options>\` the button texts in the GUI,
2✔
2919
        e.g. \`Yes/no/disable\`
2✔
2920

2✔
2921
    The script needs to return one of the short-options on \`stdout\`.
2✔
2922
    Non-zero exit code triggers the fallback of reading from \`stdin\`.
2✔
2923

2✔
2924
git hooks tools unregister <toolName>
2✔
2925

2✔
2926
    Uninstall the script folder in the installation
2✔
2927
    directory under \`tools/<toolName>\`.
2✔
2928
"
2✔
2929
        return
2✔
2930
    fi
2931

2932
    TOOLS_OPERATION="$1"
3✔
2933

2934
    shift
3✔
2935

2936
    case "$TOOLS_OPERATION" in
3✔
2937
    "register")
2938
        tools_register "$@"
2✔
2939
        ;;
2940
    "unregister")
2941
        tools_unregister "$@"
1✔
2942
        ;;
2943
    *)
2944
        manage_tools "help"
×
2945
        echo "! Invalid tools option: \`$TOOLS_OPERATION\`" >&2
×
2946
        exit 1
×
2947
        ;;
2948
    esac
2949
}
2950

2951
#####################################################
2952
# Installs a script folder of a tool.
2953
#
2954
# Returns:
2955
#   1 on failure, 0 otherwise
2956
#####################################################
2957
tools_register() {
2958
    if [ "$1" = "dialog" ]; then
2✔
2959
        SCRIPT_FOLDER="$2"
2✔
2960

2961
        if [ -d "$SCRIPT_FOLDER" ]; then
2✔
2962
            SCRIPT_FOLDER=$(cd "$SCRIPT_FOLDER" && pwd)
6✔
2963

2964
            if [ ! -f "$SCRIPT_FOLDER/run" ]; then
2✔
2965
                echo "! File \`run\` does not exist in \`$SCRIPT_FOLDER\`" >&2
×
2966
                exit 1
×
2967
            fi
2968

2969
            if ! tools_unregister "$1" --quiet; then
2✔
2970
                echo "! Unregister failed!" >&2
×
2971
                exit 1
×
2972
            fi
2973

2974
            TARGET_FOLDER="$INSTALL_DIR/tools/$1"
2✔
2975

2976
            mkdir -p "$TARGET_FOLDER" >/dev/null 2>&1 # Install new
2✔
2977
            if ! cp -r "$SCRIPT_FOLDER"/* "$TARGET_FOLDER"/; then
2✔
2978
                echo "! Registration failed" >&2
×
2979
                exit 1
×
2980
            fi
2981
            echo "Registered \`$SCRIPT_FOLDER\` as \`$1\` tool"
2✔
2982
        else
2983
            echo "! The \`$SCRIPT_FOLDER\` directory does not exist!" >&2
×
2984
            exit 1
×
2985
        fi
2986
    else
2987
        echo "! Invalid operation: \`$1\` (use \`dialog\`)" >&2
×
2988
        exit 1
×
2989
    fi
2990
}
2991

2992
#####################################################
2993
# Uninstalls a script folder of a tool.
2994
#
2995
# Returns:
2996
#   1 on failure, 0 otherwise
2997
#####################################################
2998
tools_unregister() {
2999
    [ "$2" = "--quiet" ] && QUIET="Y"
5✔
3000

3001
    if [ "$1" = "dialog" ]; then
3✔
3002
        if [ -d "$INSTALL_DIR/tools/$1" ]; then
3✔
3003
            rm -r "$INSTALL_DIR/tools/$1"
2✔
3004
            [ -n "$QUIET" ] || echo "Uninstalled the \`$1\` tool"
3✔
3005
        else
3006
            [ -n "$QUIET" ] || echo "! The \`$1\` tool is not installed" >&2
1✔
3007
        fi
3008
    else
3009
        [ -n "$QUIET" ] || echo "! Invalid tool: \`$1\` (use \`dialog\`)" >&2
×
3010
        exit 1
×
3011
    fi
3012
}
3013

3014
#####################################################
3015
# Prints the version number of this script,
3016
#   that would match the latest installed version
3017
#   of Githooks in most cases.
3018
#####################################################
3019
print_current_version_number() {
3020
    if [ "$1" = "help" ]; then
5✔
3021
        print_help_header
2✔
3022
        echo "
2✔
3023
git hooks version
2✔
3024

2✔
3025
    Prints the version number of the \`git hooks\` helper and exits.
2✔
3026
"
2✔
3027
        return
2✔
3028
    fi
3029

3030
    CURRENT_VERSION=$(execute_git "$GITHOOKS_CLONE_DIR" rev-parse --short=6 HEAD)
6✔
3031
    CURRENT_COMMIT_DATE=$(execute_git "$GITHOOKS_CLONE_DIR" log -1 "--date=format:%y%m.%d%H%M" --format="%cd" HEAD)
6✔
3032
    CURRENT_COMMIT_LOG=$(execute_git "$GITHOOKS_CLONE_DIR" log --pretty="format:%h (%s, %ad)" --date=short -1)
6✔
3033
    print_help_header
3✔
3034

3035
    echo
3✔
3036
    echo "Version: $CURRENT_COMMIT_DATE-$CURRENT_VERSION"
3✔
3037
    echo "Commit: $CURRENT_COMMIT_LOG"
3✔
3038
    echo
3✔
3039
}
3040

3041
#####################################################
3042
# Dispatches the command to the
3043
#   appropriate helper function to process it.
3044
#
3045
# Returns:
3046
#   1 if an unknown command was given,
3047
#   the exit code of the command otherwise
3048
#####################################################
3049
choose_command() {
3050
    CMD="$1"
434✔
3051
    [ -n "$CMD" ] && shift
866✔
3052

3053
    case "$CMD" in
434✔
3054
    "disable")
3055
        disable_hook "$@"
17✔
3056
        ;;
3057
    "enable")
3058
        enable_hook "$@"
11✔
3059
        ;;
3060
    "accept")
3061
        accept_changes "$@"
12✔
3062
        ;;
3063
    "exec")
3064
        execute_hook "$@"
10✔
3065
        ;;
3066
    "trust")
3067
        manage_trusted_repo "$@"
17✔
3068
        ;;
3069
    "list")
3070
        list_hooks "$@"
82✔
3071
        ;;
3072
    "shared")
3073
        manage_shared_hook_repos "$@"
121✔
3074
        ;;
3075
    "pull")
3076
        update_shared_hook_repos "$@"
1✔
3077
        ;;
3078
    "install")
3079
        run_ondemand_installation "$@"
8✔
3080
        ;;
3081
    "uninstall")
3082
        run_ondemand_uninstallation "$@"
3✔
3083
        ;;
3084
    "update")
3085
        run_update_check "$@"
12✔
3086
        ;;
3087
    "readme")
3088
        manage_readme_file "$@"
6✔
3089
        ;;
3090
    "ignore")
3091
        manage_ignore_files "$@"
12✔
3092
        ;;
3093
    "config")
3094
        manage_configuration "$@"
98✔
3095
        ;;
3096
    "tools")
3097
        manage_tools "$@"
5✔
3098
        ;;
3099
    "version")
3100
        print_current_version_number "$@"
5✔
3101
        ;;
3102
    "help")
3103
        print_help
6✔
3104
        ;;
3105
    *)
3106
        print_help
8✔
3107
        [ -n "$CMD" ] && echo "! Unknown command: $CMD" >&2
14✔
3108
        exit 1
8✔
3109
        ;;
3110
    esac
3111
}
3112

3113
set_main_variables
434✔
3114
# Choose and execute the command
3115
choose_command "$@"
434✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc