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

rycus86 / githooks / 4543718405

28 Mar 2023 01:40PM UTC coverage: 80.077% (+0.2%) from 79.878%
4543718405

push

github

Viktor Adam
Only print skipping disabled hooks once a day :sparkles:

22 of 22 new or added lines in 1 file covered. (100.0%)

2508 of 3132 relevant lines covered (80.08%)

83.88 hits per line

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

83.58
/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.
12✔
45
"
12✔
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)
856✔
65

66
    if [ -z "${INSTALL_DIR}" ]; then
428✔
67
        # install dir not defined, use default
68
        INSTALL_DIR=~/".githooks"
3✔
69
    elif [ ! -d "$INSTALL_DIR" ]; then
426✔
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"
428✔
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)
857✔
90
    if [ "${CURRENT_GIT_DIR}" = "--git-common-dir" ]; then
428✔
91
        CURRENT_GIT_DIR=".git"
×
92
    fi
93

94
    load_install_dir
428✔
95

96
    # Global IFS for loops
97
    IFS_NEWLINE="
98
"
428✔
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
256✔
111
    [ -d "${CURRENT_GIT_DIR}" ] || return 1
214✔
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
52✔
121
        echo "$@"
25✔
122
    fi
123
    return 0
26✔
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
123✔
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
14✔
662
        print_help_header
2✔
663
        echo "
2✔
664
git hooks trust
2✔
665
git hooks trust [revoke]
2✔
666
git hooks trust [delete]
2✔
667
git hooks trust [forget]
2✔
668

2✔
669
    Sets up, or reverts the trusted setting for the local 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 ! is_running_in_git_repo_root; then
12✔
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 [ -z "$1" ]; then
10✔
685
        mkdir -p .githooks &&
2✔
686
            touch .githooks/trust-all &&
2✔
687
            git config githooks.trust.all Y &&
2✔
688
            echo "The current repository is now trusted." &&
2✔
689
            echo_if_non_bare_repo "  Do not forget to commit and push the trust marker!" &&
2✔
690
            return
2✔
691

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

696
    if [ "$1" = "forget" ]; then
8✔
697
        if [ -z "$(git config --local --get githooks.trust.all)" ]; then
6✔
698
            echo "The current repository does not have trust settings."
1✔
699
            return
1✔
700
        elif git config --unset githooks.trust.all; then
2✔
701
            echo "The current repository is no longer trusted."
2✔
702
            return
2✔
703
        else
704
            echo "! Failed to revoke the trusted setting" >&2
×
705
            exit 1
×
706
        fi
707

708
    elif [ "$1" = "revoke" ] || [ "$1" = "delete" ]; then
8✔
709
        if git config githooks.trust.all N; then
4✔
710
            echo "The current repository is no longer trusted."
4✔
711
        else
712
            echo "! Failed to revoke the trusted setting" >&2
×
713
            exit 1
×
714
        fi
715

716
        if [ "$1" = "revoke" ]; then
4✔
717
            return
2✔
718
        fi
719
    fi
720

721
    if [ "$1" = "delete" ] || [ -f .githooks/trust-all ]; then
4✔
722
        rm -rf .githooks/trust-all &&
2✔
723
            echo "The trust marker is removed from the repository." &&
2✔
724
            echo_if_non_bare_repo "  Do not forget to commit and push the change!" &&
2✔
725
            return
2✔
726

727
        echo "! Failed to delete the trust marker" >&2
×
728
        exit 1
×
729
    fi
730

731
    echo "! Unknown subcommand: $1" >&2
1✔
732
    echo "  Run \`git hooks trust help\` to see the available options." >&2
1✔
733
    exit 1
1✔
734
}
735

736
#####################################################
737
# Checks if Githhoks is set up correctly,
738
#   and that other git settings do not prevent it
739
#   from executing scripts.
740
#####################################################
741
check_git_hooks_setup_is_correct() {
742
    if [ -n "$(git config core.hooksPath)" ]; then
154✔
743
        if [ "true" != "$(git config githooks.useCoreHooksPath)" ]; then
6✔
744
            echo "! WARNING" >&2
2✔
745
            echo "  \`git config core.hooksPath\` is set to $(git config core.hooksPath)," >&2
4✔
746
            echo "  but Githooks is not configured to use that folder," >&2
2✔
747
            echo "  which could mean the hooks in this repository are not run by Githooks" >&2
2✔
748
            echo >&2
2✔
749
        fi
750
    else
751
        if [ "true" = "$(git config githooks.useCoreHooksPath)" ]; then
148✔
752
            echo "! WARNING" >&2
×
753
            echo "  Githooks is configured to consider \`git config core.hooksPath\`," >&2
×
754
            echo "  but that git setting is not currently set," >&2
×
755
            echo "  which could mean the hooks in this repository are not run by Githooks" >&2
×
756
            echo >&2
×
757
        fi
758
    fi
759
}
760

761
#####################################################
762
# Lists the hook files in the current
763
#   repository along with their current state.
764
#
765
# Returns:
766
#   1 if the current directory is not a Git repo,
767
#   0 otherwise
768
#####################################################
769
list_hooks() {
770
    if [ "$1" = "help" ]; then
81✔
771
        print_help_header
2✔
772
        echo "
2✔
773
git hooks list [type]
2✔
774

2✔
775
    Lists the active hooks in the current repository along with their state.
2✔
776
    If \`type\` is given, then it only lists the hooks for that trigger event.
2✔
777
    This command needs to be run at the root of a repository.
2✔
778
"
2✔
779
        return
2✔
780
    fi
781

782
    if ! is_running_in_git_repo_root; then
79✔
783
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
784
        exit 1
2✔
785
    fi
786

787
    check_git_hooks_setup_is_correct
77✔
788

789
    if [ -n "$*" ]; then
76✔
790
        LIST_TYPES="$*"
21✔
791
        WARN_NOT_FOUND="1"
21✔
792
    else
793
        LIST_TYPES="
794
        applypatch-msg pre-applypatch post-applypatch
795
        pre-commit prepare-commit-msg commit-msg post-commit
796
        pre-rebase post-checkout post-merge pre-push
797
        pre-receive update post-receive post-update
798
        push-to-checkout pre-auto-gc post-rewrite sendemail-validate"
55✔
799
    fi
800

801
    for LIST_TYPE in $LIST_TYPES; do
1,066✔
802
        LIST_OUTPUT=""
1,066✔
803

804
        # non-Githooks hook file
805
        if [ -x "${CURRENT_GIT_DIR}/hooks/${LIST_TYPE}.replaced.githook" ]; then
1,066✔
806
            ITEM_STATE=$(get_hook_state "${CURRENT_GIT_DIR}/hooks/${LIST_TYPE}.replaced.githook")
10✔
807
            LIST_OUTPUT="$LIST_OUTPUT
808
  - $LIST_TYPE (previous / file / ${ITEM_STATE})"
5✔
809
        fi
810

811
        # global shared hooks
812
        SHARED_REPOS_LIST=$(git config --global --get-all githooks.shared)
2,132✔
813
        IFS="$IFS_NEWLINE"
1,066✔
814
        for SHARED_ITEM in $(list_hooks_in_shared_repos "$LIST_TYPE"); do
1,079✔
815
            unset IFS
13✔
816
            if [ -d "$SHARED_ITEM" ]; then
13✔
817
                for LIST_ITEM in "$SHARED_ITEM"/*; do
11✔
818
                    ITEM_NAME=$(basename "$LIST_ITEM")
22✔
819
                    ITEM_STATE=$(get_hook_state "$LIST_ITEM")
22✔
820
                    LIST_OUTPUT="$LIST_OUTPUT
821
  - $ITEM_NAME (${ITEM_STATE} / shared:global)"
11✔
822
                done
823

824
            elif [ -f "$SHARED_ITEM" ]; then
2✔
825
                ITEM_STATE=$(get_hook_state "$SHARED_ITEM")
4✔
826
                LIST_OUTPUT="$LIST_OUTPUT
827
  - $LIST_TYPE (file / ${ITEM_STATE} / shared:global)"
2✔
828
            fi
829

830
            IFS="$IFS_NEWLINE"
13✔
831
        done
832

833
        # local shared hooks
834
        if [ -f "$(pwd)/.githooks/.shared" ]; then
2,132✔
835
            SHARED_REPOS_LIST=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
885✔
836

837
            IFS="$IFS_NEWLINE"
295✔
838
            for SHARED_ITEM in $(list_hooks_in_shared_repos "$LIST_TYPE"); do
315✔
839
                unset IFS
20✔
840

841
                if [ -d "$SHARED_ITEM" ]; then
20✔
842
                    for LIST_ITEM in "$SHARED_ITEM"/*; do
17✔
843
                        ITEM_NAME=$(basename "$LIST_ITEM")
34✔
844
                        ITEM_STATE=$(get_hook_state "$LIST_ITEM")
34✔
845
                        LIST_OUTPUT="$LIST_OUTPUT
846
  - $ITEM_NAME (${ITEM_STATE} / shared:local)"
17✔
847
                    done
848

849
                elif [ -f "$SHARED_ITEM" ]; then
4✔
850
                    ITEM_STATE=$(get_hook_state "$SHARED_ITEM")
8✔
851
                    LIST_OUTPUT="$LIST_OUTPUT
852
  - $LIST_TYPE (file / ${ITEM_STATE} / shared:local)"
4✔
853
                fi
854

855
                IFS="$IFS_NEWLINE"
20✔
856
            done
857
        fi
858

859
        # in the current repository
860
        if [ -d ".githooks/$LIST_TYPE" ]; then
1,066✔
861

862
            IFS="$IFS_NEWLINE"
59✔
863
            for LIST_ITEM in .githooks/"$LIST_TYPE"/*; do
86✔
864
                unset IFS
86✔
865

866
                ITEM_NAME=$(basename "$LIST_ITEM")
172✔
867
                ITEM_STATE=$(get_hook_state "$(pwd)/.githooks/$LIST_TYPE/$ITEM_NAME")
258✔
868
                LIST_OUTPUT="$LIST_OUTPUT
869
  - $ITEM_NAME (${ITEM_STATE})"
86✔
870

871
                IFS="$IFS_NEWLINE"
86✔
872
            done
873

874
        elif [ -f ".githooks/$LIST_TYPE" ]; then
1,007✔
875
            ITEM_STATE=$(get_hook_state "$(pwd)/.githooks/$LIST_TYPE")
6✔
876
            LIST_OUTPUT="$LIST_OUTPUT
877
  - $LIST_TYPE (file / ${ITEM_STATE})"
2✔
878

879
        fi
880

881
        if [ -n "$LIST_OUTPUT" ]; then
1,066✔
882
            echo "> ${LIST_TYPE}${LIST_OUTPUT}"
81✔
883

884
        elif [ -n "$WARN_NOT_FOUND" ]; then
985✔
885
            echo "> $LIST_TYPE"
1✔
886
            echo "  No active hooks found"
1✔
887

888
        fi
889
    done
890
}
891

892
#####################################################
893
# Returns the state of hook file
894
#   in a human-readable format
895
#   on the standard output.
896
#####################################################
897
get_hook_state() {
898
    if is_repository_disabled; then
127✔
899
        echo "disabled"
×
900
    elif is_file_ignored "$1"; then
127✔
901
        echo "ignored"
6✔
902
    elif is_trusted_repo; then
121✔
903
        echo "active / trusted"
6✔
904
    else
905
        get_hook_enabled_or_disabled_state "$1"
115✔
906
    fi
907
}
908

909
#####################################################
910
# Checks if Githooks is disabled in the
911
#   current local repository.
912
#
913
# Returns:
914
#   0 if disabled, 1 otherwise
915
#####################################################
916
is_repository_disabled() {
917
    GITHOOKS_CONFIG_DISABLE=$(git config --get githooks.disable)
262✔
918
    if [ "$GITHOOKS_CONFIG_DISABLE" = "true" ] ||
131✔
919
        [ "$GITHOOKS_CONFIG_DISABLE" = "y" ] ||    # Legacy
129✔
920
        [ "$GITHOOKS_CONFIG_DISABLE" = "Y" ]; then # Legacy
129✔
921
        return 0
2✔
922
    else
923
        return 1
129✔
924
    fi
925
}
926

927
#####################################################
928
# Checks if the hook file at ${HOOK_PATH}
929
#   is ignored and should not be executed.
930
#
931
# Returns:
932
#   0 if ignored, 1 otherwise
933
#####################################################
934
is_file_ignored() {
935
    HOOK_NAME=$(basename "$1")
254✔
936
    IS_IGNORED=""
127✔
937

938
    # If there are .ignore files, read the list of patterns to exclude.
939
    ALL_IGNORE_FILE=$(mktemp)
254✔
940
    if [ -f ".githooks/.ignore" ]; then
127✔
941
        cat ".githooks/.ignore" >"$ALL_IGNORE_FILE"
6✔
942
        echo >>"$ALL_IGNORE_FILE"
6✔
943
    fi
944
    if [ -f ".githooks/${LIST_TYPE}/.ignore" ]; then
127✔
945
        cat ".githooks/${LIST_TYPE}/.ignore" >>"$ALL_IGNORE_FILE"
6✔
946
        echo >>"$ALL_IGNORE_FILE"
6✔
947
    fi
948

949
    # Check if the filename matches any of the ignored patterns
950
    while IFS= read -r IGNORED; do
266✔
951
        if [ -z "$IGNORED" ] || [ "$IGNORED" != "${IGNORED#\#}" ]; then
21✔
952
            continue
3✔
953
        fi
954

955
        # shellcheck disable=SC2295
956
        if [ -z "${HOOK_NAME##$IGNORED}" ]; then
9✔
957
            IS_IGNORED="y"
6✔
958
            break
6✔
959
        fi
960
    done <"$ALL_IGNORE_FILE"
×
961

962
    # Remove the temporary file
963
    rm -f "$ALL_IGNORE_FILE"
127✔
964

965
    if [ -n "$IS_IGNORED" ]; then
127✔
966
        return 0
6✔
967
    else
968
        return 1
121✔
969
    fi
970
}
971

972
#####################################################
973
# Checks whether the current repository
974
#   is trusted, and that this is accepted.
975
#
976
# Returns:
977
#   0 if the repo is trusted, 1 otherwise
978
#####################################################
979
is_trusted_repo() {
980
    if [ -f ".githooks/trust-all" ]; then
121✔
981
        TRUST_ALL_CONFIG=$(git config --local --get githooks.trust.all)
14✔
982
        TRUST_ALL_RESULT=$?
7✔
983

984
        # shellcheck disable=SC2181
985
        if [ $TRUST_ALL_RESULT -ne 0 ]; then
7✔
986
            return 1
1✔
987
        elif [ $TRUST_ALL_RESULT -eq 0 ] && [ "$TRUST_ALL_CONFIG" = "Y" ]; then
12✔
988
            return 0
6✔
989
        fi
990
    fi
991

992
    return 1
114✔
993
}
994

995
#####################################################
996
# Returns the enabled or disabled state
997
#   in human-readable format for a hook file
998
#   passed in as the first argument.
999
#####################################################
1000
get_hook_enabled_or_disabled_state() {
1001
    HOOK_PATH="$1"
115✔
1002

1003
    SHA_HASH=$(get_hook_checksum "$HOOK_PATH")
230✔
1004
    CURRENT_HASHES=$(grep "$HOOK_PATH" "${CURRENT_GIT_DIR}/.githooks.checksum" 2>/dev/null)
230✔
1005

1006
    # check against the previous hash
1007
    if echo "$CURRENT_HASHES" | grep -q "disabled> $HOOK_PATH" >/dev/null 2>&1; then
230✔
1008
        echo "disabled"
18✔
1009
    elif ! echo "$CURRENT_HASHES" | grep -F -q "$SHA_HASH $HOOK_PATH" >/dev/null 2>&1; then
194✔
1010
        if [ -z "$CURRENT_HASHES" ]; then
84✔
1011
            echo "pending / new"
83✔
1012
        else
1013
            echo "pending / changed"
1✔
1014
        fi
1015
    else
1016
        echo "active"
13✔
1017
    fi
1018
}
1019

1020
#####################################################
1021
# List the shared hooks from the
1022
#  $INSTALL_DIR/shared directory.
1023
#
1024
# Returns the list of paths to the hook files
1025
#   in the shared hook repositories found locally.
1026
#####################################################
1027
list_hooks_in_shared_repos() {
1028
    SHARED_LIST_TYPE="$1"
1,361✔
1029
    ALREADY_LISTED=""
1,361✔
1030

1031
    IFS="$IFS_NEWLINE"
1,361✔
1032
    for SHARED_REPO_ITEM in $SHARED_REPOS_LIST; do
476✔
1033
        unset IFS
476✔
1034

1035
        set_shared_root "$SHARED_REPO_ITEM"
476✔
1036

1037
        if [ -e "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}" ]; then
476✔
1038
            echo "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}"
15✔
1039
        elif [ -e "${SHARED_ROOT}/${SHARED_LIST_TYPE}" ]; then
461✔
1040
            echo "${SHARED_ROOT}/${SHARED_LIST_TYPE}"
5✔
1041
        fi
1042

1043
        ALREADY_LISTED="$ALREADY_LISTED
1044
        ${SHARED_ROOT}"
476✔
1045
    done
1046

1047
    if [ ! -d "$INSTALL_DIR/shared" ]; then
1,361✔
1048
        return
828✔
1049
    fi
1050

1051
    IFS="$IFS_NEWLINE"
533✔
1052
    for SHARED_ROOT in "$INSTALL_DIR/shared/"*; do
591✔
1053
        unset IFS
591✔
1054
        if [ ! -d "$SHARED_ROOT" ]; then
591✔
1055
            continue
×
1056
        fi
1057

1058
        if echo "$ALREADY_LISTED" | grep -F -q "$SHARED_ROOT"; then
1,182✔
1059
            continue
247✔
1060
        fi
1061

1062
        REMOTE_URL=$(git -C "$SHARED_ROOT" config --get remote.origin.url)
688✔
1063
        ACTIVE_REPO=$(echo "$SHARED_REPOS_LIST" | grep -F -o "$REMOTE_URL")
1,032✔
1064
        if [ "$ACTIVE_REPO" != "$REMOTE_URL" ]; then
344✔
1065
            continue
286✔
1066
        fi
1067

1068
        if [ -e "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}" ]; then
58✔
1069
            echo "${SHARED_ROOT}/.githooks/${SHARED_LIST_TYPE}"
7✔
1070
        elif [ -e "${SHARED_ROOT}/${LIST_TYPE}" ]; then
51✔
1071
            echo "${SHARED_ROOT}/${LIST_TYPE}"
6✔
1072
        fi
1073
        IFS="$IFS_NEWLINE"
58✔
1074
    done
1075
}
1076

1077
#####################################################
1078
# Manages the shared hook repositories set either
1079
#   globally, or locally within the repository.
1080
# Changes the \`githooks.shared\` global Git
1081
#   configuration, or the contents of the
1082
#   \`.githooks/.shared\` file in the local
1083
#   Git repository.
1084
#
1085
# Returns:
1086
#   0 on success, 1 on failure (exit code)
1087
#####################################################
1088
manage_shared_hook_repos() {
1089
    if [ "$1" = "help" ]; then
119✔
1090
        print_help_header
2✔
1091
        echo "
2✔
1092
git hooks shared [add|remove] [--shared|--local|--global] <git-url>
2✔
1093
git hooks shared clear [--shared|--local|--global|--all]
2✔
1094
git hooks shared purge
2✔
1095
git hooks shared list [--shared|--local|--global|--all]
2✔
1096
git hooks shared [update|pull]
2✔
1097

2✔
1098
    Manages the shared hook repositories set either in the \`.githooks.shared\` file locally in the repository or
2✔
1099
    in the local or global Git configuration \`githooks.shared\`.
2✔
1100
    The \`add\` or \`remove\` subcommands adds or removes an item, given as \`git-url\` from the list.
2✔
1101
    If \`--local|--global\` is given, then the \`githooks.shared\` local/global Git configuration
2✔
1102
    is modified, or if the \`--shared\` option (default) is set, the \`.githooks/.shared\`
2✔
1103
    file is modified in the local repository.
2✔
1104
    The \`clear\` subcommand deletes every item on either the global or the local list,
2✔
1105
    or both when the \`--all\` option is given.
2✔
1106
    The \`purge\` subcommand deletes the shared hook repositories already pulled locally.
2✔
1107
    The \`list\` subcommand list the shared, local, global or all (default) shared hooks repositories.
2✔
1108
    The \`update\` or \`pull\` subcommands update all the shared repositories, either by
2✔
1109
    running \`git pull\` on existing ones or \`git clone\` on new ones.
2✔
1110
"
2✔
1111
        return
2✔
1112
    fi
1113

1114
    if [ "$1" = "update" ] || [ "$1" = "pull" ]; then
224✔
1115
        update_shared_hook_repos
20✔
1116
        return
20✔
1117
    fi
1118

1119
    if [ "$1" = "clear" ]; then
97✔
1120
        shift
6✔
1121
        clear_shared_hook_repos "$@"
6✔
1122
        return
2✔
1123
    fi
1124

1125
    if [ "$1" = "purge" ]; then
91✔
1126
        [ -w "$INSTALL_DIR/shared" ] &&
7✔
1127
            rm -rf "$INSTALL_DIR/shared" &&
5✔
1128
            echo "All existing shared hook repositories have been deleted locally" &&
5✔
1129
            return
5✔
1130

1131
        echo "! Cannot delete existing shared hook repositories locally (maybe there is none)" >&2
2✔
1132
        exit 1
2✔
1133
    fi
1134

1135
    if [ "$1" = "list" ]; then
84✔
1136
        shift
36✔
1137
        list_shared_hook_repos "$@"
36✔
1138
        return
32✔
1139
    fi
1140

1141
    if [ "$1" = "add" ]; then
48✔
1142
        shift
29✔
1143
        add_shared_hook_repo "$@"
29✔
1144
        return
23✔
1145
    fi
1146

1147
    if [ "$1" = "remove" ]; then
19✔
1148
        shift
17✔
1149
        remove_shared_hook_repo "$@"
17✔
1150
        return
13✔
1151
    fi
1152

1153
    echo "! Unknown subcommand: \`$1\`" >&2
2✔
1154
    exit 1
2✔
1155
}
1156

1157
#####################################################
1158
# Adds the URL of a new shared hook repository to
1159
#   the global or local list.
1160
#####################################################
1161
add_shared_hook_repo() {
1162
    SET_SHARED_TYPE="--shared"
29✔
1163
    SHARED_REPO_URL=
29✔
1164

1165
    if echo "$1" | grep -qE "\-\-(shared|local|global)"; then
58✔
1166
        SET_SHARED_TYPE="$1"
24✔
1167
        SHARED_REPO_URL="$2"
24✔
1168
    else
1169
        SHARED_REPO_URL="$1"
5✔
1170
    fi
1171

1172
    if [ -z "$SHARED_REPO_URL" ]; then
29✔
1173
        echo "! Usage: \`git hooks shared add [--shared|--local|--global] <git-url>\`" >&2
2✔
1174
        exit 1
2✔
1175
    fi
1176

1177
    if [ "$SET_SHARED_TYPE" != "--shared" ]; then
27✔
1178

1179
        if [ "$SET_SHARED_TYPE" = "--local" ] && ! is_running_in_git_repo_root; then
15✔
1180
            echo "! The current directory \`$(pwd)\` does not" >&2
×
1181
            echo "  seem to be the root of a Git repository!" >&2
×
1182
            exit 1
×
1183
        fi
1184

1185
        git config "$SET_SHARED_TYPE" --add githooks.shared "$SHARED_REPO_URL" &&
13✔
1186
            echo "The new shared hook repository is successfully added" &&
13✔
1187
            return
13✔
1188

1189
        echo "! Failed to add the new shared hook repository" >&2
×
1190
        exit 1
×
1191

1192
    else
1193
        if ! is_running_in_git_repo_root; then
14✔
1194
            echo "! The current directory \`$(pwd)\` does not" >&2
4✔
1195
            echo "  seem to be the root of a Git repository!" >&2
2✔
1196
            exit 1
2✔
1197
        fi
1198

1199
        if is_local_path "$SHARED_REPO_URL" ||
12✔
1200
            is_local_url "$SHARED_REPO_URL"; then
11✔
1201
            echo "! Adding a local path:" >&2
2✔
1202
            echo "  \`$SHARED_REPO_URL\`" >&2
2✔
1203
            echo "  to the local shared hooks is forbidden." >&2
2✔
1204
            exit 1
2✔
1205
        fi
1206

1207
        mkdir -p "$(pwd)/.githooks"
20✔
1208

1209
        [ -f "$(pwd)/.githooks/.shared" ] &&
20✔
1210
            echo "" >>"$(pwd)/.githooks/.shared"
8✔
1211

1212
        echo "# Added on $(date)" >>"$(pwd)/.githooks/.shared" &&
30✔
1213
            echo "$SHARED_REPO_URL" >>"$(pwd)/.githooks/.shared" &&
20✔
1214
            echo "The new shared hook repository is successfully added" &&
10✔
1215
            echo_if_non_bare_repo "  Do not forget to commit the change!" &&
10✔
1216
            return
10✔
1217

1218
        echo "! Failed to add the new shared hook repository" >&2
×
1219
        exit 1
×
1220

1221
    fi
1222
}
1223

1224
#####################################################
1225
# Removes the URL of a new shared hook repository to
1226
#   the global or local list.
1227
#####################################################
1228
remove_shared_hook_repo() {
1229
    SET_SHARED_TYPE="--shared"
17✔
1230
    SHARED_REPO_URL=
17✔
1231

1232
    if echo "$1" | grep -qE "\-\-(shared|local|global)"; then
34✔
1233
        SET_SHARED_TYPE="$1"
13✔
1234
        SHARED_REPO_URL="$2"
13✔
1235
    else
1236
        SHARED_REPO_URL="$1"
4✔
1237
    fi
1238

1239
    if [ -z "$SHARED_REPO_URL" ]; then
17✔
1240
        echo "! Usage: \`git hooks shared remove [--shared|--local|--global] <git-url>\`" >&2
2✔
1241
        exit 1
2✔
1242
    fi
1243

1244
    if [ "$SET_SHARED_TYPE" != "--shared" ]; then
15✔
1245

1246
        if [ "$SET_SHARED_TYPE" = "--local" ] && ! is_running_in_git_repo_root; then
7✔
1247
            echo "! The current directory \`$(pwd)\` does not" >&2
×
1248
            echo "  seem to be the root of a Git repository!" >&2
×
1249
            exit 1
×
1250
        fi
1251

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

1254
        # Unset all and add them back
1255
        git config "$SET_SHARED_TYPE" --unset-all githooks.shared
7✔
1256

1257
        IFS="$IFS_NEWLINE"
7✔
1258
        for SHARED_REPO_ITEM in $CURRENT_LIST; do
13✔
1259
            unset IFS
13✔
1260
            if [ "$SHARED_REPO_ITEM" = "$SHARED_REPO_URL" ]; then
13✔
1261
                continue
7✔
1262
            fi
1263

1264
            git config "$SET_SHARED_TYPE" --add githooks.shared "$SHARED_REPO_ITEM"
6✔
1265
            IFS="$IFS_NEWLINE"
6✔
1266
        done
1267
        unset IFS
7✔
1268

1269
        echo "The list of shared hook repositories is successfully changed"
7✔
1270
        return
7✔
1271

1272
    else
1273
        if ! is_running_in_git_repo_root; then
8✔
1274
            echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
4✔
1275
            exit 1
2✔
1276
        fi
1277

1278
        if [ ! -f "$(pwd)/.githooks/.shared" ]; then
12✔
1279
            echo "! No \`.githooks/.shared\` in current repository" >&2
×
1280
            return
×
1281
        fi
1282

1283
        NEW_LIST=""
6✔
1284
        ONLY_COMMENTS="true"
6✔
1285
        IFS="$IFS_NEWLINE"
6✔
1286
        while read -r LINE || [ -n "$LINE" ]; do
54✔
1287
            unset IFS
42✔
1288

1289
            if echo "$LINE" | grep -qE "^[^#\n\r ].*$"; then
84✔
1290
                if [ "$LINE" = "$SHARED_REPO_URL" ]; then
12✔
1291
                    continue
6✔
1292
                fi
1293
                ONLY_COMMENTS="false"
6✔
1294
            elif echo "$LINE" | grep -qE "^ *[#].*$"; then
60✔
1295
                : # nothing to do for comments ...
18✔
1296
            else
1297
                : # skip empty lines
12✔
1298
            fi
1299

1300
            if [ -z "$NEW_LIST" ]; then
36✔
1301
                NEW_LIST="$LINE"
6✔
1302
            else
1303
                NEW_LIST="${NEW_LIST}
1304
${LINE}"
30✔
1305
            fi
1306

1307
            IFS="$IFS_NEWLINE"
36✔
1308
        done <"$(pwd)/.githooks/.shared"
×
1309
        unset IFS
6✔
1310

1311
        if [ -z "$NEW_LIST" ] || [ "$ONLY_COMMENTS" = "true" ]; then
12✔
1312
            clear_shared_hook_repos "$SET_SHARED_TYPE" && return || exit 1
4✔
1313
        fi
1314

1315
        echo "$NEW_LIST" >"$(pwd)/.githooks/.shared" &&
8✔
1316
            echo "The list of shared hook repositories is successfully changed" &&
4✔
1317
            echo_if_non_bare_repo "  Do not forget to commit the change!" &&
4✔
1318
            return
4✔
1319

1320
        echo "! Failed to remove a shared hook repository" >&2
×
1321
        exit 1
×
1322

1323
    fi
1324
}
1325

1326
#####################################################
1327
# Clears the list of shared hook repositories
1328
#   from the global or local list, or both.
1329
#####################################################
1330
clear_shared_hook_repos() {
1331
    CLEAR_GLOBAL_REPOS=""
8✔
1332
    CLEAR_LOCAL_REPOS=""
8✔
1333
    CLEAR_SHARED_REPOS=""
8✔
1334
    CLEAR_REPOS_FAILED=""
8✔
1335

1336
    case "$1" in
8✔
1337
    "--shared")
1338
        CLEAR_SHARED_REPOS=1
2✔
1339
        ;;
1340
    "--local")
1341
        CLEAR_LOCAL_REPOS=1
×
1342
        ;;
1343
    "--global")
1344
        CLEAR_GLOBAL_REPOS=1
×
1345
        ;;
1346
    "--all")
1347
        CLEAR_SHARED_REPOS=1
2✔
1348
        CLEAR_LOCAL_REPOS=1
2✔
1349
        CLEAR_GLOBAL_REPOS=1
2✔
1350
        ;;
1351
    *)
1352
        echo "! Unknown clear option \`$1\`" >&2
4✔
1353
        echo "  Usage: \`git hooks shared clear [--shared|--local|--global|--all]\`" >&2
4✔
1354
        exit 1
4✔
1355
        ;;
1356
    esac
1357

1358
    if [ -n "$CLEAR_LOCAL_REPOS" ]; then
4✔
1359
        if ! is_running_in_git_repo_root; then
2✔
1360
            echo "! The current directory \`$(pwd)\` does not" >&2
×
1361
            echo "  seem to be the root of a Git repository!" >&2
×
1362
            CLEAR_REPOS_FAILED=1
×
1363
        else
1364
            git config --local --unset-all githooks.shared
2✔
1365
            echo "Shared hook repository list in local Git config cleared"
2✔
1366
        fi
1367
    fi
1368

1369
    if [ -n "$CLEAR_GLOBAL_REPOS" ]; then
4✔
1370
        git config --global --unset-all githooks.shared
2✔
1371
        echo "Shared hook repository list in global Git config cleared"
2✔
1372
    fi
1373

1374
    if [ -n "$CLEAR_SHARED_REPOS" ] && [ -f "$(pwd)/.githooks/.shared" ]; then
12✔
1375
        rm -f "$(pwd)/.githooks/.shared" &&
4✔
1376
            echo "Shared hook repository list in \".githooks/.shared\` file cleared" ||
2✔
1377
            CLEAR_REPOS_FAILED=1
×
1378
    fi
1379

1380
    if [ -n "$CLEAR_REPOS_FAILED" ]; then
4✔
1381
        echo "! There were some problems clearing the shared hook repository list" >&2
×
1382
        exit 1
×
1383
    fi
1384
}
1385

1386
#####################################################
1387
# Prints the list of shared hook repositories,
1388
#   along with their Git URLs optionally, from
1389
#   the global or local list, or both.
1390
#####################################################
1391
list_shared_hook_repos() {
1392
    LIST_SHARED=1
44✔
1393
    LIST_CONFIGS="global,local"
44✔
1394

1395
    for ARG in "$@"; do
24✔
1396
        case "$ARG" in
24✔
1397
        "--shared")
1398
            LIST_CONFIGS=""
6✔
1399
            ;;
1400
        "--local")
1401
            LIST_CONFIGS="local"
×
1402
            LIST_SHARED=""
×
1403
            ;;
1404
        "--global")
1405
            LIST_CONFIGS="global"
11✔
1406
            LIST_SHARED=""
10✔
1407
            ;;
1408
        "--all") ;;
×
1409
        *)
1410
            echo "! Unknown list option \`$ARG\`" >&2
2✔
1411
            echo "  Usage: \`git hooks shared list [--shared|--local|--global|--all]\`" >&2
2✔
1412
            exit 1
2✔
1413
            ;;
1414
        esac
1415
    done
1416

1417
    IFS=","
42✔
1418
    for LIST_CONFIG in $LIST_CONFIGS; do
62✔
1419
        unset IFS
62✔
1420

1421
        echo "Shared hook repositories in $LIST_CONFIG Git config:"
63✔
1422
        SHARED_REPOS_LIST=$(git config "--$LIST_CONFIG" --get-all githooks.shared)
124✔
1423

1424
        if [ -z "$SHARED_REPOS_LIST" ]; then
62✔
1425
            echo "  - None"
41✔
1426
        else
1427

1428
            IFS="$IFS_NEWLINE"
22✔
1429
            for LIST_ITEM in $SHARED_REPOS_LIST; do
41✔
1430
                unset IFS
41✔
1431

1432
                set_shared_root "$LIST_ITEM"
41✔
1433

1434
                LIST_ITEM_STATE="invalid"
41✔
1435

1436
                if [ "$SHARED_REPO_IS_CLONED" = "true" ]; then
41✔
1437
                    if [ -d "$SHARED_ROOT" ]; then
39✔
1438
                        if [ "$(git -C "$SHARED_ROOT" config --get remote.origin.url)" = "$SHARED_REPO_CLONE_URL" ]; then
18✔
1439
                            LIST_ITEM_STATE="active"
7✔
1440
                        fi
1441
                    else
1442
                        LIST_ITEM_STATE="pending"
31✔
1443
                    fi
1444
                else
1445
                    [ -d "$SHARED_ROOT" ] && LIST_ITEM_STATE="active"
4✔
1446
                fi
1447

1448
                echo "  - $LIST_ITEM ($LIST_ITEM_STATE)"
41✔
1449

1450
                IFS="$IFS_NEWLINE"
42✔
1451
            done
1452
            unset IFS
21✔
1453
        fi
1454

1455
        IFS=","
62✔
1456
    done
1457
    unset IFS
43✔
1458

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

1462
        if ! is_running_in_git_repo_root; then
33✔
1463
            echo "  - Current folder does not seem to be a Git repository"
2✔
1464
            [ -z "$LIST_CONFIGS" ] && exit 1
4✔
1465
        elif [ ! -f "$(pwd)/.githooks/.shared" ]; then
60✔
1466
            echo "  - None"
15✔
1467
        else
1468
            SHARED_REPOS_LIST=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
45✔
1469

1470
            IFS="$IFS_NEWLINE"
15✔
1471
            for LIST_ITEM in $SHARED_REPOS_LIST; do
29✔
1472
                unset IFS
29✔
1473

1474
                set_shared_root "$LIST_ITEM"
29✔
1475

1476
                LIST_ITEM_STATE="invalid"
29✔
1477

1478
                if [ "$SHARED_REPO_IS_CLONED" != "true" ]; then
29✔
1479
                    [ -d "$SHARED_ROOT" ] && LIST_ITEM_STATE="active"
×
1480
                else
1481
                    if [ -d "$SHARED_ROOT" ]; then
29✔
1482
                        if [ "$(git -C "$SHARED_ROOT" config --get remote.origin.url)" = "$SHARED_REPO_CLONE_URL" ]; then
16✔
1483
                            LIST_ITEM_STATE="active"
6✔
1484
                        fi
1485
                    else
1486
                        LIST_ITEM_STATE="pending"
21✔
1487
                    fi
1488
                fi
1489

1490
                echo "  - $LIST_ITEM ($LIST_ITEM_STATE)"
29✔
1491

1492
                IFS="$IFS_NEWLINE"
29✔
1493
            done
1494
            unset IFS
15✔
1495
        fi
1496
    fi
1497

1498
}
1499

1500
#####################################################
1501
# Updates the configured shared hook repositories.
1502
#
1503
# Returns:
1504
#   None
1505
#####################################################
1506
update_shared_hook_repos() {
1507
    if [ "$1" = "help" ]; then
21✔
1508
        print_help_header
1✔
1509
        echo "
1✔
1510
git hooks shared pull
1✔
1511

1✔
1512
    Updates the shared repositories found either
1✔
1513
    in the global Git configuration, or in the
1✔
1514
    \`.githooks/.shared\` file in the local repository.
1✔
1515

1✔
1516
> Please use \`git hooks shared pull\` instead, this version is now deprecated.
1✔
1517
"
1✔
1518
        return
1✔
1519
    fi
1520

1521
    if [ -f "$(pwd)/.githooks/.shared" ]; then
40✔
1522
        SHARED_HOOKS=$(grep -E "^[^#\n\r ].*$" <"$(pwd)/.githooks/.shared")
39✔
1523
        update_shared_hooks_in --shared "$SHARED_HOOKS"
13✔
1524
    fi
1525

1526
    SHARED_HOOKS=$(git config --local --get-all githooks.shared 2>/dev/null)
40✔
1527
    if [ -n "$SHARED_HOOKS" ]; then
20✔
1528
        update_shared_hooks_in --local "$SHARED_HOOKS"
2✔
1529
    fi
1530

1531
    SHARED_HOOKS=$(git config --global --get-all githooks.shared)
40✔
1532
    if [ -n "$SHARED_HOOKS" ]; then
21✔
1533
        update_shared_hooks_in --global "$SHARED_HOOKS"
10✔
1534
    fi
1535

1536
    echo "Finished"
20✔
1537
}
1538

1539
#####################################################
1540
# Check if `$1` is not a supported git clone url and
1541
#   is treated as a local path to a repository.
1542
#   See `https://tools.ietf.org/html/rfc3986#appendix-B`
1543

1544
# Returns: 0 if it is a local path, 1 otherwise
1545
#####################################################
1546
is_local_path() {
1547
    if echo "$1" | grep -Eq "^[^:/?#]+://" ||  # its a URL `<scheme>://...``
1,170✔
1548
        echo "$1" | grep -Eq "^.+@.+:.+"; then # or its a short scp syntax
293✔
1549
        return 1
439✔
1550
    fi
1551
    return 0
147✔
1552
}
1553

1554
#####################################################
1555
# Check if url `$1`is a local url, e.g `file://`.
1556
#
1557
# Returns: 0 if it is a local url, 1 otherwise
1558
#####################################################
1559
is_local_url() {
1560
    if echo "$1" | grep -iEq "^\s*file://"; then
1,314✔
1561
        return 0
142✔
1562
    fi
1563
    return 1
300✔
1564
}
1565

1566
#####################################################
1567
# Sets the `SHARED_ROOT` and `NORMALIZED_NAME`
1568
#   for the shared hook repo url `$1` and sets
1569
#   `SHARED_REPO_IS_CLONED` to `true` and its
1570
#   `SHARED_REPO_CLONE_URL` if is needs to get
1571
#    cloned and `SHARED_REPO_IS_LOCAL` to `true`
1572
#    if `$1` points to to a local path.
1573
#
1574
# Returns:
1575
#   none
1576
#####################################################
1577
set_shared_root() {
1578

1579
    SHARED_ROOT=""
573✔
1580
    SHARED_REPO_CLONE_URL=""
573✔
1581
    SHARED_REPO_CLONE_BRANCH=""
573✔
1582
    SHARED_REPO_IS_LOCAL="false"
573✔
1583
    SHARED_REPO_IS_CLONED="true"
573✔
1584
    DO_SPLIT="true"
573✔
1585

1586
    if is_local_path "$1"; then
573✔
1587
        SHARED_REPO_IS_LOCAL="true"
145✔
1588

1589
        if is_bare_repo "$1"; then
145✔
1590
            DO_SPLIT="false"
×
1591
        else
1592
            # We have a local path to a non-bare repo
1593
            SHARED_REPO_IS_CLONED="false"
146✔
1594
            SHARED_ROOT="$1"
145✔
1595
            return
145✔
1596
        fi
1597
    elif is_local_url "$1"; then
428✔
1598
        SHARED_REPO_IS_LOCAL="true"
139✔
1599
    fi
1600

1601
    if [ "$SHARED_REPO_IS_CLONED" = "true" ]; then
428✔
1602
        # Here we now have a supported Git URL or
1603
        # a local bare-repo `<localpath>`
1604

1605
        # Split "...@(.*)"
1606
        if [ "$DO_SPLIT" = "true" ] && echo "$1" | grep -q "@"; then
1,284✔
1607
            SHARED_REPO_CLONE_URL="$(echo "$1" | sed -E "s|^(.+)@.+$|\\1|")"
150✔
1608
            SHARED_REPO_CLONE_BRANCH="$(echo "$1" | sed -E "s|^.+@(.+)$|\\1|")"
150✔
1609
        else
1610
            SHARED_REPO_CLONE_URL="$1"
378✔
1611
            SHARED_REPO_CLONE_BRANCH=""
378✔
1612
        fi
1613

1614
        # Double-check what we did above
1615
        if echo "$SHARED_REPO_CLONE_BRANCH" | grep -q ":"; then
856✔
1616
            # the branch name had a ":" so it was probably not a branch name
1617
            SHARED_REPO_CLONE_URL="${SHARED_REPO_CLONE_URL}@${SHARED_REPO_CLONE_BRANCH}"
×
1618
            SHARED_REPO_CLONE_BRANCH=""
×
1619

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

1626
        # Define the shared clone folder
1627
        SHA_HASH=$(echo "$1" | git hash-object --stdin 2>/dev/null)
1,285✔
1628
        NAME=$(echo "$1" | tail -c 48 | sed -E "s/[^a-zA-Z0-9]/-/g")
1,712✔
1629
        SHARED_ROOT="$INSTALL_DIR/shared/$SHA_HASH-$NAME"
429✔
1630
    fi
1631
}
1632

1633
#####################################################
1634
# Updates the shared hooks repositories
1635
#   on the list passed in on the first argument.
1636
#####################################################
1637
update_shared_hooks_in() {
1638
    SHARED_HOOKS_TYPE="$1"
26✔
1639
    SHARED_REPOS_LIST="$2"
25✔
1640

1641
    IFS="$IFS_NEWLINE"
26✔
1642
    for SHARED_REPO in $SHARED_REPOS_LIST; do
27✔
1643
        unset IFS
27✔
1644

1645
        set_shared_root "$SHARED_REPO"
28✔
1646

1647
        if [ "$SHARED_REPO_IS_CLONED" != "true" ]; then
27✔
1648
            # Non-cloned roots are ignored
1649
            continue
10✔
1650
        elif [ "$SHARED_HOOKS_TYPE" = "--shared" ] &&
17✔
1651
            [ "$SHARED_REPO_IS_LOCAL" = "true" ]; then
10✔
1652
            echo "! Warning: Shared hooks in \`.githooks/.shared\` contain a local path" >&2
×
1653
            echo "  \`$SHARED_REPO\`" >&2
×
1654
            echo "  which is forbidden. It will be skipped." >&2
×
1655
            echo ""
×
1656
            echo "  You can only have local paths for shared hooks defined" >&2
2✔
1657
            echo "  in the local or global Git configuration." >&2
×
1658
            echo ""
×
1659
            echo "  This can be achieved by running" >&2
×
1660
            echo "    \$ git hooks shared add [--local|--global] \"$SHARED_REPO\"" >&2
×
1661
            echo "  and deleting it from the \`.shared\` file by" >&2
×
1662
            echo "    \$ git hooks shared remove --shared \"$SHARED_REPO\"" >&2
×
1663
            continue
×
1664
        fi
1665

1666
        if [ -d "$SHARED_ROOT/.git" ]; then
23✔
1667
            echo "* Updating shared hooks from: $SHARED_REPO"
8✔
1668

1669
            # shellcheck disable=SC2086
1670
            PULL_OUTPUT="$(execute_git "$SHARED_ROOT" pull 2>&1)"
4✔
1671

1672
            # shellcheck disable=SC2181
1673
            if [ $? -ne 0 ]; then
2✔
1674
                echo "! Update failed, git pull output:" >&2
×
1675
                echo "$PULL_OUTPUT" >&2
×
1676
            fi
1677
        else
1678
            echo "* Retrieving shared hooks from: $SHARED_REPO_CLONE_URL"
15✔
1679

1680
            ADD_ARGS=""
15✔
1681
            [ "$SHARED_REPO_IS_LOCAL" != "true" ] && ADD_ARGS="--depth=1"
26✔
1682

1683
            [ -d "$SHARED_ROOT" ] &&
15✔
1684
                rm -rf "$SHARED_ROOT" &&
1✔
1685
                mkdir -p "$SHARED_ROOT"
×
1686

1687
            if [ -n "$SHARED_REPO_CLONE_BRANCH" ]; then
15✔
1688
                # shellcheck disable=SC2086
1689
                CLONE_OUTPUT=$(git clone \
×
1690
                    -c core.hooksPath=/dev/null \
1691
                    --template=/dev/null \
1692
                    --single-branch \
1693
                    --branch "$SHARED_REPO_CLONE_BRANCH" \
1694
                    $ADD_ARGS \
1695
                    "$SHARED_REPO_CLONE_URL" \
1696
                    "$SHARED_ROOT" 2>&1)
1697
            else
1698
                # shellcheck disable=SC2086
1699
                CLONE_OUTPUT=$(git clone \
×
1700
                    -c core.hooksPath=/dev/null \
1701
                    --template=/dev/null \
1702
                    --single-branch \
1703
                    $ADD_ARGS \
1704
                    "$SHARED_REPO_CLONE_URL" \
1705
                    "$SHARED_ROOT" 2>&1)
1706
            fi
1707

1708
            # shellcheck disable=SC2181
1709
            if [ $? -ne 0 ]; then
15✔
1710
                echo "! Clone failed, git clone output:" >&2
×
1711
                echo "$CLONE_OUTPUT" >&2
×
1712
            fi
1713
        fi
1714

1715
        IFS="$IFS_NEWLINE"
17✔
1716
    done
1717

1718
    unset IFS
25✔
1719
}
1720

1721
#####################################################
1722
# Executes an ondemand installation
1723
#   of the latest Githooks version.
1724
#
1725
# Returns:
1726
#   1 if the installation fails,
1727
#   0 otherwise
1728
#####################################################
1729
run_ondemand_installation() {
1730
    if [ "$1" = "help" ]; then
8✔
1731
        print_help_header
2✔
1732
        echo "
2✔
1733
git hooks install [--global]
2✔
1734

2✔
1735
    Installs the Githooks hooks into the current repository.
2✔
1736
    If the \`--global\` flag is given, it executes the installation
2✔
1737
    globally, including the hook templates for future repositories.
2✔
1738
"
2✔
1739
        return
2✔
1740
    fi
1741

1742
    INSTALL_FLAGS="--single"
6✔
1743
    if [ "$1" = "--global" ]; then
6✔
1744
        INSTALL_FLAGS=""
2✔
1745
    elif [ -n "$1" ]; then
4✔
1746
        echo "! Invalid argument: \`$1\`" >&2 && exit 1
×
1747
    fi
1748

1749
    if ! fetch_latest_updates; then
6✔
1750
        echo "! Failed to fetch the latest install script" >&2
×
1751
        echo "  You can retry manually using one of the alternative methods," >&2
×
1752
        echo "  see them here: https://github.com/rycus86/githooks#installation" >&2
×
1753
        exit 1
×
1754
    fi
1755

1756
    # shellcheck disable=SC2086
1757
    if ! execute_install_script $INSTALL_FLAGS; then
6✔
1758
        echo "! Failed to execute the installation" >&2
3✔
1759
        exit 1
2✔
1760
    fi
1761
}
1762

1763
#####################################################
1764
# Executes an ondemand uninstallation of Githooks.
1765
#
1766
# Returns:
1767
#   1 if the uninstallation fails,
1768
#   0 otherwise
1769
#####################################################
1770
run_ondemand_uninstallation() {
1771
    if [ "$1" = "help" ]; then
4✔
1772
        print_help_header
2✔
1773
        echo "
2✔
1774
git hooks uninstall [--global]
2✔
1775

2✔
1776
    Uninstalls the Githooks hooks from the current repository.
2✔
1777
    If the \`--global\` flag is given, it executes the uninstallation
2✔
1778
    globally, including the hook templates and all local repositories.
2✔
1779
"
2✔
1780
        return
2✔
1781
    fi
1782

1783
    UNINSTALL_ARGS="--single"
1✔
1784
    if [ "$1" = "--global" ]; then
1✔
1785
        UNINSTALL_ARGS="--global"
1✔
1786
    elif [ -n "$1" ]; then
×
1787
        echo "! Invalid argument: \`$1\`" >&2 && exit 1
×
1788
    fi
1789

1790
    if ! execute_uninstall_script $UNINSTALL_ARGS; then
1✔
1791
        echo "! Failed to execute the uninstallation" >&2
×
1792
        exit 1
×
1793
    fi
1794
}
1795

1796
#####################################################
1797
# Executes an update check, and potentially
1798
#   the installation of the latest version.
1799
#
1800
# Returns:
1801
#   1 if the latest version cannot be retrieved,
1802
#   0 otherwise
1803
#####################################################
1804
run_update_check() {
1805
    if [ "$1" = "help" ]; then
12✔
1806
        print_help_header
2✔
1807
        echo "
2✔
1808
git hooks update [force]
2✔
1809
git hooks update [enable|disable]
2✔
1810

2✔
1811
    Executes an update check for a newer Githooks version.
2✔
1812
    If it finds one, or if \`force\` was given, the downloaded
2✔
1813
    install script is executed for the latest version.
2✔
1814
    The \`enable\` and \`disable\` options enable or disable
2✔
1815
    the automatic checks that would normally run daily
2✔
1816
    after a successful commit event.
2✔
1817
"
2✔
1818
        return 0
2✔
1819
    fi
1820

1821
    if [ "$1" = "enable" ]; then
10✔
1822
        git config --global githooks.autoupdate.enabled true &&
2✔
1823
            echo "Automatic update checks have been enabled" &&
2✔
1824
            return 0
2✔
1825

1826
        echo "! Failed to enable automatic updates" >&2 && exit 1
×
1827

1828
    elif [ "$1" = "disable" ]; then
8✔
1829
        git config --global githooks.autoupdate.enabled false &&
2✔
1830
            echo "Automatic update checks have been disabled" &&
2✔
1831
            return 0
2✔
1832

1833
        echo "! Failed to disable automatic updates" >&2 && exit 1
×
1834

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

1838
    fi
1839

1840
    record_update_time
5✔
1841

1842
    if ! fetch_latest_updates; then
5✔
1843
        echo "! Failed to check for updates: cannot fetch updates"
×
1844
        exit 1
×
1845
    fi
1846

1847
    if [ "$1" != "force" ]; then
5✔
1848
        if ! is_update_available; then
3✔
1849
            echo "  Githooks is already on the latest version"
1✔
1850
            return 0
1✔
1851
        fi
1852
    fi
1853

1854
    # shellcheck disable=SC2086
1855
    if ! execute_install_script $INSTALL_FLAGS; then
4✔
1856
        echo "! Failed to execute the installation"
×
1857
        print_update_disable_info
×
1858
        return 1
×
1859
    fi
1860
    return 0
4✔
1861
}
1862

1863
#####################################################
1864
# Saves the last update time into the
1865
#   githooks.autoupdate.lastrun global Git config.
1866
#
1867
# Returns:
1868
#   None
1869
#####################################################
1870
record_update_time() {
1871
    git config --global githooks.autoupdate.lastrun "$(date +%s)"
10✔
1872
}
1873

1874
#####################################################
1875
# Returns the script path e.g. `run` for the app
1876
#   `$1`
1877
#
1878
# Returns:
1879
#   0 and "$INSTALL_DIR/tools/$1/run"
1880
#   1 and "" otherwise
1881
#####################################################
1882
get_tool_script() {
1883
    if [ -f "$INSTALL_DIR/tools/$1/run" ]; then
×
1884
        echo "$INSTALL_DIR/tools/$1/run" && return 0
×
1885
    fi
1886
    return 1
×
1887
}
1888

1889
#####################################################
1890
# Call a script "$1". If it is not executable
1891
# call it as a shell script.
1892
#
1893
# Returns:
1894
#   Error code of the script.
1895
#####################################################
1896
call_script() {
1897
    SCRIPT="$1"
×
1898
    shift
×
1899

1900
    if [ -x "$SCRIPT" ]; then
×
1901
        "$SCRIPT" "$@"
×
1902
    else
1903
        sh "$SCRIPT" "$@"
×
1904

1905
    fi
1906
    return $?
×
1907
}
1908

1909
#####################################################
1910
# Does a update clone repository exist in the
1911
#  install folder
1912
#
1913
# Returns: 0 if `true`, 1 otherwise
1914
#####################################################
1915
is_release_clone_existing() {
1916
    if git -C "$INSTALL_DIR/release" rev-parse >/dev/null 2>&1; then
×
1917
        return 0
×
1918
    fi
1919
    return 1
×
1920
}
1921

1922
#####################################################
1923
# Checks if there is an update in the release clone
1924
#   waiting for a fast-forward merge.
1925
#
1926
# Returns:
1927
#   0 if an update needs to be applied, 1 otherwise
1928
#####################################################
1929
is_update_available() {
1930
    [ "$GITHOOKS_CLONE_UPDATE_AVAILABLE" = "true" ] || return 1
4✔
1931
}
1932

1933
#####################################################
1934
# Fetches updates in the release clone.
1935
#   If the release clone is newly created the variable
1936
#   `$GITHOOKS_CLONE_CREATED` is set to
1937
#   `true`
1938
#   If an update is available
1939
#   `GITHOOKS_CLONE_UPDATE_AVAILABLE` is set to `true`
1940
#
1941
# Returns:
1942
#   1 if failed, 0 otherwise
1943
#####################################################
1944
fetch_latest_updates() {
1945

1946
    echo "^ Checking for updates ..."
11✔
1947

1948
    GITHOOKS_CLONE_CREATED="false"
11✔
1949
    GITHOOKS_CLONE_UPDATE_AVAILABLE="false"
11✔
1950

1951
    GITHOOKS_CLONE_URL=$(git config --global githooks.cloneUrl)
22✔
1952
    GITHOOKS_CLONE_BRANCH=$(git config --global githooks.cloneBranch)
22✔
1953

1954
    # We do a fresh clone if there is not a repository
1955
    # or wrong url/branch configured and the user agrees.
1956
    if is_git_repo "$GITHOOKS_CLONE_DIR"; then
11✔
1957

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

1961
        if [ "$URL" != "$GITHOOKS_CLONE_URL" ] ||
10✔
1962
            [ "$BRANCH" != "$GITHOOKS_CLONE_BRANCH" ]; then
10✔
1963

1964
            CREATE_NEW_CLONE="false"
×
1965

1966
            echo "! Cannot fetch updates because \`origin\` of update clone" >&2
×
1967
            echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
1968
            echo "  points to url:" >&2
×
1969
            echo "  \`$URL\`" >&2
×
1970
            echo "  on branch \`$BRANCH\`" >&2
×
1971
            echo "  which is not configured." >&2
×
1972

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

1976
            if [ "$ANSWER" = "y" ] || [ "$ANSWER" = "Y" ]; then
×
1977
                CREATE_NEW_CLONE="true"
×
1978
            fi
1979

1980
            if [ "$CREATE_NEW_CLONE" != "true" ]; then
×
1981
                echo "! See \`git hooks config [set|print] clone-url\` and" >&2
×
1982
                echo "      \`git hooks config [set|print] clone-branch\`" >&2
×
1983
                echo "  Either fix this or delete the clone" >&2
×
1984
                echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
1985
                echo "  to trigger a new checkout." >&2
×
1986
                return 1
×
1987
            fi
1988
        fi
1989

1990
        # Check if the update clone is dirty which it really should not.
1991
        if ! execute_git "$GITHOOKS_CLONE_DIR" diff-index --quiet HEAD >/dev/null 2>&1; then
10✔
1992
            echo "! Cannot pull updates because the update clone" >&2
×
1993
            echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
1994
            echo "  is dirty!" >&2
×
1995

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

1999
            if [ "$ANSWER" = "y" ] || [ "$ANSWER" = "Y" ]; then
×
2000
                CREATE_NEW_CLONE="true"
×
2001
            fi
2002

2003
            if [ "$CREATE_NEW_CLONE" != "true" ]; then
×
2004
                echo "! Either fix this or delete the clone" >&2
×
2005
                echo "  \`$GITHOOKS_CLONE_DIR\`" >&2
×
2006
                echo "  to trigger a new checkout." >&2
×
2007
                return 1
×
2008
            fi
2009
        fi
2010
    else
2011
        CREATE_NEW_CLONE="true"
1✔
2012
    fi
2013

2014
    if [ "$CREATE_NEW_CLONE" = "true" ]; then
11✔
2015
        clone_release_repository || return 1
1✔
2016

2017
        # shellcheck disable=SC2034
2018
        GITHOOKS_CLONE_CREATED="true"
1✔
2019
        GITHOOKS_CLONE_UPDATE_AVAILABLE="true"
1✔
2020
    else
2021

2022
        FETCH_OUTPUT=$(
×
2023
            execute_git "$GITHOOKS_CLONE_DIR" fetch origin "$GITHOOKS_CLONE_BRANCH" 2>&1
×
2024
        )
2025

2026
        # shellcheck disable=SC2181
2027
        if [ $? -ne 0 ]; then
10✔
2028
            echo "! Fetching updates in  \`$GITHOOKS_CLONE_DIR\` failed with:" >&2
×
2029
            echo "$FETCH_OUTPUT" >&2
×
2030
            return 1
×
2031
        fi
2032

2033
        RELEASE_COMMIT=$(execute_git "$GITHOOKS_CLONE_DIR" rev-parse "$GITHOOKS_CLONE_BRANCH")
20✔
2034
        UPDATE_COMMIT=$(execute_git "$GITHOOKS_CLONE_DIR" rev-parse "origin/$GITHOOKS_CLONE_BRANCH")
20✔
2035

2036
        if [ "$RELEASE_COMMIT" != "$UPDATE_COMMIT" ]; then
10✔
2037
            # We have an update available
2038
            # install.sh deals with updating ...
2039
            GITHOOKS_CLONE_UPDATE_AVAILABLE="true"
5✔
2040
        fi
2041
    fi
2042

2043
    return 0
11✔
2044
}
2045

2046
############################################################
2047
# Checks whether the given directory
2048
#   is a Git repository (bare included) or not.
2049
#
2050
# Returns:
2051
#   1 if failed, 0 otherwise
2052
############################################################
2053
is_git_repo() {
2054
    git -C "$1" rev-parse >/dev/null 2>&1 || return 1
24✔
2055
}
2056

2057
############################################################
2058
# Checks whether the given directory
2059
#   is a Git bare repository.
2060
#
2061
# Returns:
2062
#   1 if failed, 0 otherwise
2063
############################################################
2064
is_bare_repo() {
2065
    [ "$(git -C "$1" rev-parse --is-bare-repository 2>/dev/null)" = "true" ] || return 1
435✔
2066
}
2067

2068
#####################################################
2069
# Safely execute a git command in the standard
2070
#   clone dir `$1`.
2071
#
2072
# Returns: Error code from `git`
2073
#####################################################
2074
execute_git() {
2075
    REPO="$1"
71✔
2076
    shift
71✔
2077

2078
    git -C "$REPO" \
71✔
2079
        --work-tree="$REPO" \
2080
        --git-dir="$REPO/.git" \
2081
        -c core.hooksPath=/dev/null \
2082
        "$@"
2083
}
2084

2085
#####################################################
2086
#  Creates the release clone if needed.
2087
# Returns:
2088
#   1 if failed, 0 otherwise
2089
#####################################################
2090
assert_release_clone() {
2091

2092
    # We do a fresh clone if there is no clone
2093
    if ! is_git_repo "$GITHOOKS_CLONE_DIR"; then
12✔
2094
        clone_release_repository || return 1
×
2095
    fi
2096

2097
    return 0
12✔
2098
}
2099

2100
############################################################
2101
# Clone the URL `$GITHOOKS_CLONE_URL` into the install
2102
# folder `$INSTALL_DIR/release` for further updates.
2103
#
2104
# Returns: 0 if succesful, 1 otherwise
2105
############################################################
2106
clone_release_repository() {
2107

2108
    GITHOOKS_CLONE_URL=$(git config --global githooks.cloneUrl)
2✔
2109
    GITHOOKS_CLONE_BRANCH=$(git config --global githooks.cloneBranch)
2✔
2110

2111
    if [ -z "$GITHOOKS_CLONE_URL" ]; then
1✔
2112
        GITHOOKS_CLONE_URL="https://github.com/rycus86/githooks.git"
1✔
2113
    fi
2114

2115
    if [ -z "$GITHOOKS_CLONE_BRANCH" ]; then
1✔
2116
        GITHOOKS_CLONE_BRANCH="master"
1✔
2117
    fi
2118

2119
    if [ -d "$GITHOOKS_CLONE_DIR" ]; then
1✔
2120
        if ! rm -rf "$GITHOOKS_CLONE_DIR" >/dev/null 2>&1; then
×
2121
            echo "! Failed to remove an existing githooks release repository" >&2
×
2122
            return 1
×
2123
        fi
2124
    fi
2125

2126
    echo "Cloning \`$GITHOOKS_CLONE_URL\` to \`$GITHOOKS_CLONE_DIR\` ..."
1✔
2127

2128
    CLONE_OUTPUT=$(git clone \
×
2129
        -c core.hooksPath=/dev/null \
2130
        --template=/dev/null \
2131
        --depth=1 \
2132
        --single-branch \
2133
        --branch "$GITHOOKS_CLONE_BRANCH" \
2134
        "$GITHOOKS_CLONE_URL" "$GITHOOKS_CLONE_DIR" 2>&1)
2135

2136
    # shellcheck disable=SC2181
2137
    if [ $? -ne 0 ]; then
1✔
2138
        echo "! Cloning \`$GITHOOKS_CLONE_URL\` to \`$GITHOOKS_CLONE_DIR\` failed with output: " >&2
×
2139
        echo "$CLONE_OUTPUT" >&2
×
2140
        return 1
×
2141
    fi
2142

2143
    git config --global githooks.cloneUrl "$GITHOOKS_CLONE_URL"
1✔
2144
    git config --global githooks.cloneBranch "$GITHOOKS_CLONE_BRANCH"
1✔
2145

2146
    return 0
1✔
2147
}
2148

2149
#####################################################
2150
# Checks if updates are enabled.
2151
#
2152
# Returns:
2153
#   0 if updates are enabled, 1 otherwise
2154
#####################################################
2155
is_autoupdate_enabled() {
2156
    [ "$(git config --global githooks.autoupdate.enabled)" = "Y" ] || return 1
×
2157
}
2158

2159
#####################################################
2160
# Performs the installation of the previously
2161
#   fetched install script.
2162
#
2163
# Returns:
2164
#   0 if the installation was successful, 1 otherwise
2165
#####################################################
2166
execute_install_script() {
2167

2168
    if ! assert_release_clone; then
10✔
2169
        echo "! Could not create a release clone in \`$GITHOOKS_CLONE_DIR\`" >&2
×
2170
        exit 1
×
2171
    fi
2172

2173
    # Set the install script
2174
    INSTALL_SCRIPT="$INSTALL_DIR/release/install.sh"
10✔
2175
    if [ ! -f "$INSTALL_SCRIPT" ]; then
10✔
2176
        echo "! Non-existing \`install.sh\` in  \`$INSTALL_DIR/release\`" >&2
×
2177
        return 1
×
2178
    fi
2179

2180
    sh -s -- "$@" <"$INSTALL_SCRIPT" || return 1
12✔
2181
    return 0
8✔
2182
}
2183

2184
#####################################################
2185
# Performs the uninstallation of the previously
2186
#   fetched uninstall script.
2187
#
2188
# Returns:
2189
#   0 if the uninstallation was successful,
2190
#   1 otherwise
2191
#####################################################
2192
execute_uninstall_script() {
2193

2194
    # Set the install script
2195
    UNINSTALL_SCRIPT="$INSTALL_DIR/release/uninstall.sh"
1✔
2196
    if [ ! -f "$UNINSTALL_SCRIPT" ]; then
1✔
2197
        echo "! Non-existing \`uninstall.sh\` in  \`$INSTALL_DIR/release\`" >&2
×
2198
        return 1
×
2199
    fi
2200

2201
    sh -s -- "$@" <"$UNINSTALL_SCRIPT" || return 1
1✔
2202

2203
    return 0
1✔
2204
}
2205

2206
#####################################################
2207
# Prints some information on how to disable
2208
#   automatic update checks.
2209
#
2210
# Returns:
2211
#   None
2212
#####################################################
2213
print_update_disable_info() {
2214
    echo "  If you would like to disable auto-updates, run:"
×
2215
    echo "    \$ git hooks update disable"
×
2216
}
2217

2218
#####################################################
2219
# Adds or updates the Githooks README in
2220
#   the current local repository.
2221
#
2222
# Returns:
2223
#   1 on failure, 0 otherwise
2224
#####################################################
2225
manage_readme_file() {
2226
    case "$1" in
6✔
2227
    "add")
2228
        FORCE_README=""
3✔
2229
        ;;
2230
    "update")
2231
        FORCE_README="y"
1✔
2232
        ;;
2233
    *)
2234
        print_help_header
2✔
2235
        echo "
2✔
2236
git hooks readme [add|update]
2✔
2237

2✔
2238
    Adds or updates the Githooks README in the \`.githooks\` folder.
2✔
2239
    If \`add\` is used, it checks first if there is a README file already.
2✔
2240
    With \`update\`, the file is always updated, creating it if necessary.
2✔
2241
    This command needs to be run at the root of a repository.
2✔
2242
"
2✔
2243
        if [ "$1" = "help" ]; then
2✔
2244
            exit 0
2✔
2245
        else
2246
            exit 1
×
2247
        fi
2248
        ;;
2249
    esac
2250

2251
    if ! is_running_in_git_repo_root; then
4✔
2252
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2253
        exit 1
1✔
2254
    fi
2255

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

2262
    if ! assert_release_clone; then
2✔
2263
        exit 1
×
2264
    fi
2265

2266
    README_FILE="$INSTALL_DIR/release/.githooks/README.md"
2✔
2267
    mkdir -p "$(pwd)/.githooks" &&
4✔
2268
        cat "$README_FILE" >"$(pwd)/.githooks/README.md" &&
4✔
2269
        echo "The README file is updated." &&
2✔
2270
        echo_if_non_bare_repo "  Do not forget to commit and push it!" ||
2✔
2271
        echo "! Failed to update the README file in the current repository" >&2
×
2272
}
2273

2274
#####################################################
2275
# Adds or updates Githooks ignore files in
2276
#   the current local repository.
2277
#
2278
# Returns:
2279
#   1 on failure, 0 otherwise
2280
#####################################################
2281
manage_ignore_files() {
2282
    if [ "$1" = "help" ]; then
14✔
2283
        print_help_header
4✔
2284
        echo "
4✔
2285
git hooks ignore [pattern...]
4✔
2286
git hooks ignore [trigger] [pattern...]
4✔
2287

4✔
2288
    Adds new file name patterns to the Githooks \`.ignore\` file, either
4✔
2289
    in the main \`.githooks\` folder, or in the Git event specific one.
4✔
2290
    Note, that it may be required to surround the individual pattern
4✔
2291
    parameters with single quotes to avoid expanding or splitting them.
4✔
2292
    The \`trigger\` parameter should be the name of the Git event if given.
4✔
2293
    This command needs to be run at the root of a repository.
4✔
2294
"
4✔
2295
        return
4✔
2296
    fi
2297

2298
    if ! is_running_in_git_repo_root; then
10✔
2299
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2300
        exit 1
1✔
2301
    fi
2302

2303
    TRIGGER_TYPES="
2304
        applypatch-msg pre-applypatch post-applypatch
2305
        pre-commit prepare-commit-msg commit-msg post-commit
2306
        pre-rebase post-checkout post-merge pre-push
2307
        pre-receive update post-receive post-update
2308
        push-to-checkout pre-auto-gc post-rewrite sendemail-validate"
9✔
2309

2310
    TARGET_DIR="$(pwd)/.githooks"
18✔
2311

2312
    for TRIGGER_TYPE in $TRIGGER_TYPES; do
114✔
2313
        if [ "$1" = "$TRIGGER_TYPE" ]; then
114✔
2314
            TARGET_DIR="$(pwd)/.githooks/$TRIGGER_TYPE"
8✔
2315
            shift
4✔
2316
            break
4✔
2317
        fi
2318
    done
2319

2320
    if [ -z "$1" ]; then
9✔
2321
        manage_ignore_files "help"
2✔
2322
        echo "! Missing pattern parameter" >&2
2✔
2323
        exit 1
2✔
2324
    fi
2325

2326
    if ! mkdir -p "$TARGET_DIR" && touch "$TARGET_DIR/.ignore"; then
7✔
2327
        echo "! Failed to prepare the ignore file at $TARGET_DIR/.ignore" >&2
×
2328
        exit 1
×
2329
    fi
2330

2331
    [ -f "$TARGET_DIR/.ignore" ] &&
7✔
2332
        echo "" >>"$TARGET_DIR/.ignore"
2✔
2333

2334
    for PATTERN in "$@"; do
7✔
2335
        if ! echo "$PATTERN" >>"$TARGET_DIR/.ignore"; then
7✔
2336
            echo "! Failed to update the ignore file at $TARGET_DIR/.ignore" >&2
1✔
2337
            exit 1
1✔
2338
        fi
2339
    done
2340

2341
    echo "The ignore file at $TARGET_DIR/.ignore is updated"
6✔
2342
    echo_if_non_bare_repo "  Do not forget to commit the changes!"
6✔
2343
}
2344

2345
#####################################################
2346
# Manages various Githooks settings,
2347
#   that is stored in Git configuration.
2348
#
2349
# Returns:
2350
#   1 on failure, 0 otherwise
2351
#####################################################
2352
manage_configuration() {
2353
    if [ "$1" = "help" ]; then
106✔
2354
        print_help_header
10✔
2355
        echo "
10✔
2356
git hooks config list [--local|--global]
10✔
2357

10✔
2358
    Lists the Githooks related settings of the Githooks configuration.
10✔
2359
    Can be either global or local configuration, or both by default.
10✔
2360

10✔
2361
git hooks config [set|reset|print] disable
10✔
2362

10✔
2363
    Disables running any Githooks files in the current repository,
10✔
2364
    when the \`set\` option is used.
10✔
2365
    The \`reset\` option clears this setting.
10✔
2366
    The \`print\` option outputs the current setting.
10✔
2367
    This command needs to be run at the root of a repository.
10✔
2368

10✔
2369
[deprecated] git hooks config [set|reset|print] single
10✔
2370

10✔
2371
    This command is deprecated and will be removed in the future.
10✔
2372
    Marks the current local repository to be managed as a single Githooks
10✔
2373
    installation, or clears the marker, with \`set\` and \`reset\` respectively.
10✔
2374
    The \`print\` option outputs the current setting of it.
10✔
2375
    This command needs to be run at the root of a repository.
10✔
2376

10✔
2377
git hooks config set search-dir <path>
10✔
2378
git hooks config [reset|print] search-dir
10✔
2379

10✔
2380
    Changes the previous search directory setting used during installation.
10✔
2381
    The \`set\` option changes the value, and the \`reset\` option clears it.
10✔
2382
    The \`print\` option outputs the current setting of it.
10✔
2383

10✔
2384
git hooks config set shared [--local] <git-url...>
10✔
2385
git hooks config [reset|print] shared [--local]
10✔
2386

10✔
2387
    Updates the list of global (or local) shared hook repositories when
10✔
2388
    the \`set\` option is used, which accepts multiple <git-url> arguments,
10✔
2389
    each containing a clone URL of a hook repository.
10✔
2390
    The \`reset\` option clears this setting.
10✔
2391
    The \`print\` option outputs the current setting.
10✔
2392

10✔
2393
git hooks config [accept|deny|reset|print] trusted
10✔
2394

10✔
2395
    Accepts changes to all existing and new hooks in the current repository
10✔
2396
    when the trust marker is present and the \`set\` option is used.
10✔
2397
    The \`deny\` option marks the repository as
10✔
2398
    it has refused to trust the changes, even if the trust marker is present.
10✔
2399
    The \`reset\` option clears this setting.
10✔
2400
    The \`print\` option outputs the current setting.
10✔
2401
    This command needs to be run at the root of a repository.
10✔
2402

10✔
2403
git hooks config [enable|disable|reset|print] update
10✔
2404

10✔
2405
    Enables or disables automatic update checks with
10✔
2406
    the \`enable\` and \`disable\` options respectively.
10✔
2407
    The \`reset\` option clears this setting.
10✔
2408
    The \`print\` option outputs the current setting.
10✔
2409

10✔
2410
git hooks config set clone-url <git-url>
10✔
2411
git hooks config [set|print] clone-url
10✔
2412

10✔
2413
    Sets or prints the configured githooks clone url used
10✔
2414
    for any update.
10✔
2415

10✔
2416
git hooks config set clone-branch <branch-name>
10✔
2417
git hooks config print clone-branch
10✔
2418

10✔
2419
    Sets or prints the configured branch of the update clone
10✔
2420
    used for any update.
10✔
2421

10✔
2422
git hooks config [reset|print] update-time
10✔
2423

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

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

10✔
2431
Enable or disable failing hooks with an error when any
10✔
2432
shared hooks configured in \`.shared\` are missing,
10✔
2433
which usually means \`git hooks update\` has not been called yet.
10✔
2434

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

10✔
2437
By default, detected LFS hooks during install are disabled and backed up.
10✔
2438
The \`yes\` option remembers to always delete these hooks.
10✔
2439
The \`no\` option remembers the default behavior.
10✔
2440
The decision is reset with \`reset\` to the default behavior.
10✔
2441
The \`print\` option outputs the current behavior.
10✔
2442
"
10✔
2443
        return
10✔
2444
    fi
2445

2446
    CONFIG_OPERATION="$1"
96✔
2447

2448
    if [ "$CONFIG_OPERATION" = "list" ]; then
96✔
2449
        if [ "$2" = "--local" ] && ! is_running_in_git_repo_root; then
6✔
2450
            echo "! Local configuration can only be printed from a Git repository" >&2
1✔
2451
            exit 1
1✔
2452
        fi
2453

2454
        if [ -z "$2" ]; then
4✔
2455
            git config --get-regexp "(^githooks|alias.hooks)" | sort
4✔
2456
        else
2457
            git config "$2" --get-regexp "(^githooks|alias.hooks)" | sort
4✔
2458
        fi
2459
        exit $?
4✔
2460
    fi
2461

2462
    CONFIG_ARGUMENT="$2"
91✔
2463

2464
    [ "$#" -ge 1 ] && shift
181✔
2465
    [ "$#" -ge 1 ] && shift
181✔
2466

2467
    case "$CONFIG_ARGUMENT" in
91✔
2468
    "disable")
2469
        config_disable "$CONFIG_OPERATION"
12✔
2470
        ;;
2471
    "search-dir")
2472
        config_search_dir "$CONFIG_OPERATION" "$@"
10✔
2473
        ;;
2474
    "shared")
2475
        config_shared_hook_repos "$CONFIG_OPERATION" "$@"
15✔
2476
        ;;
2477
    "trusted")
2478
        config_trust_all_hooks "$CONFIG_OPERATION"
17✔
2479
        ;;
2480
    "update")
2481
        config_update_state "$CONFIG_OPERATION" "$@"
15✔
2482
        ;;
2483
    "clone-url")
2484
        config_update_clone_url "$CONFIG_OPERATION" "$@"
×
2485
        ;;
2486
    "clone-branch")
2487
        config_update_clone_branch "$CONFIG_OPERATION" "$@"
×
2488
        ;;
2489
    "update-time")
2490
        config_update_last_run "$CONFIG_OPERATION"
9✔
2491
        ;;
2492
    "fail-on-non-existing-shared-hooks")
2493
        config_fail_on_not_existing_shared_hooks "$CONFIG_OPERATION" "$@"
5✔
2494
        ;;
2495
    "delete-detected-lfs-hooks")
2496
        config_delete_detected_lfs_hooks "$CONFIG_OPERATION" "$@"
3✔
2497
        ;;
2498
    *)
2499
        manage_configuration "help"
5✔
2500
        echo "! Invalid configuration option: \`$CONFIG_ARGUMENT\`" >&2
5✔
2501
        exit 1
5✔
2502
        ;;
2503
    esac
2504
}
2505

2506
#####################################################
2507
# Manages Githooks disable settings for
2508
#   the current repository.
2509
# Prints or modifies the \`githooks.disable\`
2510
#   local Git configuration.
2511
#####################################################
2512
config_disable() {
2513
    if ! is_running_in_git_repo_root; then
12✔
2514
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2515
        exit 1
1✔
2516
    fi
2517

2518
    if [ "$1" = "set" ]; then
11✔
2519
        git config githooks.disable true
4✔
2520
    elif [ "$1" = "reset" ]; then
7✔
2521
        git config --unset githooks.disable
2✔
2522
    elif [ "$1" = "print" ]; then
5✔
2523
        if is_repository_disabled; then
4✔
2524
            echo "Githooks is disabled in the current repository"
2✔
2525
        else
2526
            echo "Githooks is NOT disabled in the current repository"
2✔
2527
        fi
2528
    else
2529
        echo "! Invalid operation: \`$1\` (use \`set\`, \`reset\` or \`print\`)" >&2
1✔
2530
        exit 1
1✔
2531
    fi
2532
}
2533

2534
#####################################################
2535
# Manages previous search directory setting
2536
#   used during Githooks installation.
2537
# Prints or modifies the
2538
#   \`githooks.previousSearchDir\`
2539
#   global Git configuration.
2540
#####################################################
2541
config_search_dir() {
2542
    if [ "$1" = "set" ]; then
10✔
2543
        if [ -z "$2" ]; then
3✔
2544
            manage_configuration "help"
1✔
2545
            echo "! Missing <path> parameter" >&2
1✔
2546
            exit 1
1✔
2547
        fi
2548

2549
        git config --global githooks.previousSearchDir "$2"
2✔
2550
    elif [ "$1" = "reset" ]; then
7✔
2551
        git config --global --unset githooks.previousSearchDir
2✔
2552
    elif [ "$1" = "print" ]; then
5✔
2553
        CONFIG_SEARCH_DIR=$(git config --global --get githooks.previousSearchDir)
8✔
2554
        if [ -z "$CONFIG_SEARCH_DIR" ]; then
4✔
2555
            echo "No previous search directory is set"
2✔
2556
        else
2557
            echo "Search directory is set to: $CONFIG_SEARCH_DIR"
2✔
2558
        fi
2559
    else
2560
        echo "! Invalid operation: \`$1\` (use \`set\`, \`reset\` or \`print\`)" >&2
1✔
2561
        exit 1
2✔
2562
    fi
2563
}
2564

2565
#####################################################
2566
# Manages global shared hook repository list setting.
2567
# Prints or modifies the \`githooks.shared\`
2568
#   global Git configuration.
2569
#####################################################
2570
config_shared_hook_repos() {
2571

2572
    if [ "$1" = "set" ]; then
15✔
2573

2574
        SHARED_TYPE="--global"
4✔
2575
        [ "$2" = "--local" ] && SHARED_TYPE="$2" && shift
4✔
2576

2577
        if [ -z "$2" ]; then
4✔
2578
            manage_configuration "help"
1✔
2579
            echo "! Missing <git-url> parameter" >&2
1✔
2580
            exit 1
1✔
2581
        fi
2582

2583
        shift
3✔
2584

2585
        for SHARED_REPO_ITEM in "$@"; do
5✔
2586
            git config "$SHARED_TYPE" --add githooks.shared "$SHARED_REPO_ITEM"
5✔
2587
        done
2588

2589
    elif [ "$1" = "reset" ]; then
11✔
2590
        SHARED_TYPE="--global"
2✔
2591
        if echo "$2" | grep -qE "\-\-(local)"; then
4✔
2592
            SHARED_TYPE="$2"
×
2593
        elif [ -n "$2" ]; then
3✔
2594
            manage_configuration "help"
×
2595
            echo "! Wrong argument \`$2\`" >&2
×
2596
        fi
2597

2598
        git config "$SHARED_TYPE" --unset-all githooks.shared
2✔
2599

2600
    elif [ "$1" = "print" ]; then
9✔
2601
        SHARED_TYPE="--global"
8✔
2602
        if echo "$2" | grep -qE "\-\-(local)"; then
16✔
2603
            SHARED_TYPE="$2"
×
2604
        elif [ -n "$2" ]; then
8✔
2605
            manage_configuration "help"
×
2606
            echo "! Wrong argument $($2)" >&2
×
2607
        fi
2608
        list_shared_hook_repos "$SHARED_TYPE"
8✔
2609
    else
2610
        manage_configuration "help"
1✔
2611
        echo "! Invalid operation: \`$1\` (use \`set\`, \`reset\` or \`print\`)" >&2
1✔
2612
        exit 1
1✔
2613
    fi
2614
}
2615

2616
#####################################################
2617
# Manages the trust-all-hooks setting
2618
#   for the current repository.
2619
# Prints or modifies the \`githooks.trust.all\`
2620
#   local Git configuration.
2621
#####################################################
2622
config_trust_all_hooks() {
2623
    if ! is_running_in_git_repo_root; then
18✔
2624
        echo "The current directory \`$(pwd)\` does not seem to be the root of a Git repository!"
2✔
2625
        exit 1
1✔
2626
    fi
2627

2628
    if [ "$1" = "accept" ]; then
16✔
2629
        git config githooks.trust.all Y
4✔
2630
    elif [ "$1" = "deny" ]; then
12✔
2631
        git config githooks.trust.all N
3✔
2632
    elif [ "$1" = "reset" ]; then
9✔
2633
        git config --unset githooks.trust.all
2✔
2634
    elif [ "$1" = "print" ]; then
7✔
2635
        CONFIG_TRUST_ALL=$(git config --local --get githooks.trust.all)
12✔
2636
        if [ "$CONFIG_TRUST_ALL" = "Y" ]; then
6✔
2637
            echo "The current repository trusts all hooks automatically"
2✔
2638
        elif [ -z "$CONFIG_TRUST_ALL" ]; then
4✔
2639
            echo "The current repository does NOT have trust settings"
2✔
2640
        else
2641
            echo "The current repository does NOT trust hooks automatically"
2✔
2642
        fi
2643
    else
2644
        echo "! Invalid operation: \`$1\` (use \`accept\`, \`deny\`, \`reset\` or \`print\`)" >&2
1✔
2645
        exit 1
1✔
2646
    fi
2647
}
2648

2649
#####################################################
2650
# Manages the automatic update check setting.
2651
# Prints or modifies the
2652
#   \`githooks.autoupdate.enabled\`
2653
#   global Git configuration.
2654
#####################################################
2655
config_update_state() {
2656
    if [ "$1" = "enable" ]; then
15✔
2657
        git config --global githooks.autoupdate.enabled true
4✔
2658
    elif [ "$1" = "disable" ]; then
11✔
2659
        git config --global githooks.autoupdate.enabled false
2✔
2660
    elif [ "$1" = "reset" ]; then
9✔
2661
        git config --global --unset githooks.autoupdate.enabled
2✔
2662
    elif [ "$1" = "print" ]; then
7✔
2663
        CONFIG_UPDATE_ENABLED=$(git config --get githooks.autoupdate.enabled)
12✔
2664
        if [ "$CONFIG_UPDATE_ENABLED" = "true" ] ||
6✔
2665
            [ "$CONFIG_UPDATE_ENABLED" = "Y" ]; then
4✔
2666
            echo "Automatic update checks are enabled"
2✔
2667
        else
2668
            echo "Automatic update checks are NOT enabled"
4✔
2669
        fi
2670
    else
2671
        echo "! Invalid operation: \`$1\` (use \`enable\`, \`disable\`, \`reset\` or \`print\`)" >&2
1✔
2672
        exit 1
1✔
2673
    fi
2674
}
2675

2676
#####################################################
2677
# Manages the automatic update clone url.
2678
# Prints or modifies the
2679
#   \`githooks.cloneUrl\`
2680
#   global Git configuration.
2681
#####################################################
2682
config_update_clone_url() {
2683
    if [ "$1" = "print" ]; then
×
2684
        echo "Update clone url set to: $(git config --global githooks.cloneUrl)"
×
2685
    elif [ "$1" = "set" ]; then
×
2686
        if [ -z "$2" ]; then
×
2687
            echo "! No valid url given" >&2
×
2688
            exit 1
×
2689
        fi
2690
        git config --global githooks.cloneUrl "$2"
×
2691
        config_update_clone_url "print"
×
2692
    else
2693
        echo "! Invalid operation: \`$1\` (use \`set\`, or \`print\`)" >&2
×
2694
        exit 1
×
2695
    fi
2696
}
2697

2698
#####################################################
2699
# Manages the automatic update clone branch.
2700
# Prints or modifies the
2701
#   \`githooks.cloneUrl\`
2702
#   global Git configuration.
2703
#####################################################
2704
config_update_clone_branch() {
2705
    if [ "$1" = "print" ]; then
×
2706
        echo "Update clone branch set to: $(git config --global githooks.cloneBranch)"
×
2707
    elif [ "$1" = "set" ]; then
×
2708
        if [ -z "$2" ]; then
×
2709
            echo "! No valid branch name given" >&2
×
2710
            exit 1
×
2711
        fi
2712
        git config --global githooks.cloneBranch "$2"
×
2713
        config_update_clone_branch "print"
×
2714
    else
2715
        echo "! Invalid operation: \`$1\` (use \`set\`, or \`print\`)" >&2
×
2716
        exit 1
×
2717
    fi
2718
}
2719

2720
#####################################################
2721
# Manages the timestamp for the last update check.
2722
# Prints or modifies the
2723
#   \`githooks.autoupdate.lastrun\`
2724
#   global Git configuration.
2725
#####################################################
2726
config_update_last_run() {
2727
    if [ "$1" = "reset" ]; then
9✔
2728
        git config --global --unset githooks.autoupdate.lastrun
2✔
2729
    elif [ "$1" = "print" ]; then
7✔
2730
        LAST_UPDATE=$(git config --global --get githooks.autoupdate.lastrun)
12✔
2731
        if [ -z "$LAST_UPDATE" ]; then
6✔
2732
            echo "The update has never run"
4✔
2733
        else
2734
            if ! date --date="@${LAST_UPDATE}" 2>/dev/null; then
2✔
2735
                if ! date -j -f "%s" "$LAST_UPDATE" 2>/dev/null; then
×
2736
                    echo "Last update timestamp: $LAST_UPDATE"
×
2737
                fi
2738
            fi
2739
        fi
2740
    else
2741
        echo "! Invalid operation: \`$1\` (use \`reset\` or \`print\`)" >&2
1✔
2742
        exit 1
1✔
2743
    fi
2744
}
2745

2746
#####################################################
2747
# Manages the failOnNonExistingSharedHook switch.
2748
# Prints or modifies the
2749
#   `githooks.failOnNonExistingSharedHooks`
2750
#   local or global Git configuration.
2751
#####################################################
2752
config_fail_on_not_existing_shared_hooks() {
2753
    CONFIG="--local"
5✔
2754
    if [ -n "$2" ]; then
5✔
2755
        if [ "$2" = "--local" ] || [ "$2" = "--global" ]; then
7✔
2756
            CONFIG="$2"
4✔
2757
        else
2758
            echo "! Invalid option: \`$2\` (use \`--local\` or \`--global\`)" >&2
×
2759
            exit 1
×
2760
        fi
2761
    fi
2762

2763
    if [ "$1" = "enable" ]; then
5✔
2764
        if ! git config "$CONFIG" githooks.failOnNonExistingSharedHooks "true"; then
2✔
2765
            echo "! Failed to enable \`fail-on-non-existing-shared-hooks\`" >&2
×
2766
            exit 1
×
2767
        fi
2768

2769
        echo "Failing on not existing shared hooks is enabled"
2✔
2770

2771
    elif [ "$1" = "disable" ]; then
3✔
2772
        if ! git config "$CONFIG" githooks.failOnNonExistingSharedHooks "false"; then
×
2773
            echo "! Failed to disable \`fail-on-non-existing-shared-hooks\`" >&2
×
2774
            exit 1
×
2775
        fi
2776

2777
        echo "Failing on not existing shared hooks is disabled"
×
2778

2779
    elif [ "$1" = "print" ]; then
3✔
2780
        FAIL_ON_NOT_EXISTING=$(git config "$CONFIG" --get githooks.failOnNonExistingSharedHooks)
6✔
2781
        if [ "$FAIL_ON_NOT_EXISTING" = "true" ]; then
3✔
2782
            echo "Failing on not existing shared hooks is enabled"
2✔
2783
        else
2784
            # default also if it does not exist
2785
            echo "Failing on not existing shared hooks is disabled"
1✔
2786
        fi
2787

2788
    else
2789
        echo "! Invalid operation: \`$1\` (use \`enable\`, \`disable\` or \`print\`)" >&2
×
2790
        exit 1
×
2791
    fi
2792
}
2793

2794
#####################################################
2795
# Manages the deleteDetectedLFSHooks default bahavior.
2796
# Modifies or prints
2797
#   `githooks.deleteDetectedLFSHooks`
2798
#   global Git configuration.
2799
#####################################################
2800
config_delete_detected_lfs_hooks() {
2801
    if [ "$1" = "yes" ]; then
4✔
2802
        git config --global githooks.deleteDetectedLFSHooks "a"
×
2803
        config_delete_detected_lfs_hooks "print"
×
2804
    elif [ "$1" = "no" ]; then
4✔
2805
        git config --global githooks.deleteDetectedLFSHooks "n"
×
2806
        config_delete_detected_lfs_hooks "print"
×
2807
    elif [ "$1" = "reset" ]; then
4✔
2808
        git config --global --unset githooks.deleteDetectedLFSHooks
1✔
2809
        config_delete_detected_lfs_hooks "print"
1✔
2810
    elif [ "$1" = "print" ]; then
3✔
2811
        VALUE=$(git config --global githooks.deleteDetectedLFSHooks)
6✔
2812
        if [ "$VALUE" = "Y" ]; then
3✔
2813
            echo "Detected LFS hooks are by default deleted"
×
2814
        else
2815
            echo "Detected LFS hooks are by default disabled and backed up"
3✔
2816
        fi
2817
    else
2818
        echo "! Invalid operation: \`$1\` (use \`yes\`, \`no\` or \`reset\`)" >&2
×
2819
        exit 1
×
2820
    fi
2821
}
2822

2823
#####################################################
2824
# Manages the app script folders.
2825
#
2826
# Returns:
2827
#   1 on failure, 0 otherwise
2828
#####################################################
2829
manage_tools() {
2830
    if [ "$1" = "help" ]; then
5✔
2831
        print_help_header
2✔
2832
        echo "
2✔
2833
git hooks tools register <toolName> <scriptFolder>
2✔
2834

2✔
2835
    Install the script folder \`<scriptFolder>\` in
2✔
2836
    the installation directory under \`tools/<toolName>\`.
2✔
2837

2✔
2838
    Currently the following tools are supported:
2✔
2839

2✔
2840
    >> Dialog Tool (<toolName> = \"dialog\")
2✔
2841

2✔
2842
    The interface of the dialog tool is as follows.
2✔
2843

2✔
2844
    # if \`run\` is executable
2✔
2845
    \$ run <title> <text> <options> <long-options>
2✔
2846
    # otherwise, assuming \`run\` is a shell script
2✔
2847
    \$ sh run <title> <text> <options> <long-options>
2✔
2848

2✔
2849
    The arguments of the dialog tool are:
2✔
2850
    - \`<title>\` the title for the GUI dialog
2✔
2851
    - \`<text>\` the text for the GUI dialog
2✔
2852
    - \`<short-options>\` the button return values, slash-delimited,
2✔
2853
        e.g. \`Y/n/d\`.
2✔
2854
        The default button is the first capital character found.
2✔
2855
    - \`<long-options>\` the button texts in the GUI,
2✔
2856
        e.g. \`Yes/no/disable\`
2✔
2857

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

2✔
2861
git hooks tools unregister <toolName>
2✔
2862

2✔
2863
    Uninstall the script folder in the installation
2✔
2864
    directory under \`tools/<toolName>\`.
2✔
2865
"
2✔
2866
        return
2✔
2867
    fi
2868

2869
    TOOLS_OPERATION="$1"
3✔
2870

2871
    shift
3✔
2872

2873
    case "$TOOLS_OPERATION" in
3✔
2874
    "register")
2875
        tools_register "$@"
2✔
2876
        ;;
2877
    "unregister")
2878
        tools_unregister "$@"
1✔
2879
        ;;
2880
    *)
2881
        manage_tools "help"
×
2882
        echo "! Invalid tools option: \`$TOOLS_OPERATION\`" >&2
×
2883
        exit 1
×
2884
        ;;
2885
    esac
2886
}
2887

2888
#####################################################
2889
# Installs a script folder of a tool.
2890
#
2891
# Returns:
2892
#   1 on failure, 0 otherwise
2893
#####################################################
2894
tools_register() {
2895
    if [ "$1" = "dialog" ]; then
2✔
2896
        SCRIPT_FOLDER="$2"
2✔
2897

2898
        if [ -d "$SCRIPT_FOLDER" ]; then
2✔
2899
            SCRIPT_FOLDER=$(cd "$SCRIPT_FOLDER" && pwd)
6✔
2900

2901
            if [ ! -f "$SCRIPT_FOLDER/run" ]; then
2✔
2902
                echo "! File \`run\` does not exist in \`$SCRIPT_FOLDER\`" >&2
×
2903
                exit 1
×
2904
            fi
2905

2906
            if ! tools_unregister "$1" --quiet; then
2✔
2907
                echo "! Unregister failed!" >&2
×
2908
                exit 1
×
2909
            fi
2910

2911
            TARGET_FOLDER="$INSTALL_DIR/tools/$1"
2✔
2912

2913
            mkdir -p "$TARGET_FOLDER" >/dev/null 2>&1 # Install new
2✔
2914
            if ! cp -r "$SCRIPT_FOLDER"/* "$TARGET_FOLDER"/; then
2✔
2915
                echo "! Registration failed" >&2
×
2916
                exit 1
×
2917
            fi
2918
            echo "Registered \`$SCRIPT_FOLDER\` as \`$1\` tool"
2✔
2919
        else
2920
            echo "! The \`$SCRIPT_FOLDER\` directory does not exist!" >&2
×
2921
            exit 1
×
2922
        fi
2923
    else
2924
        echo "! Invalid operation: \`$1\` (use \`dialog\`)" >&2
×
2925
        exit 1
×
2926
    fi
2927
}
2928

2929
#####################################################
2930
# Uninstalls a script folder of a tool.
2931
#
2932
# Returns:
2933
#   1 on failure, 0 otherwise
2934
#####################################################
2935
tools_unregister() {
2936
    [ "$2" = "--quiet" ] && QUIET="Y"
5✔
2937

2938
    if [ "$1" = "dialog" ]; then
3✔
2939
        if [ -d "$INSTALL_DIR/tools/$1" ]; then
3✔
2940
            rm -r "$INSTALL_DIR/tools/$1"
2✔
2941
            [ -n "$QUIET" ] || echo "Uninstalled the \`$1\` tool"
3✔
2942
        else
2943
            [ -n "$QUIET" ] || echo "! The \`$1\` tool is not installed" >&2
1✔
2944
        fi
2945
    else
2946
        [ -n "$QUIET" ] || echo "! Invalid tool: \`$1\` (use \`dialog\`)" >&2
×
2947
        exit 1
×
2948
    fi
2949
}
2950

2951
#####################################################
2952
# Prints the version number of this script,
2953
#   that would match the latest installed version
2954
#   of Githooks in most cases.
2955
#####################################################
2956
print_current_version_number() {
2957
    if [ "$1" = "help" ]; then
5✔
2958
        print_help_header
2✔
2959
        echo "
2✔
2960
git hooks version
2✔
2961

2✔
2962
    Prints the version number of the \`git hooks\` helper and exits.
2✔
2963
"
2✔
2964
        return
2✔
2965
    fi
2966

2967
    CURRENT_VERSION=$(execute_git "$GITHOOKS_CLONE_DIR" rev-parse --short=6 HEAD)
6✔
2968
    CURRENT_COMMIT_DATE=$(execute_git "$GITHOOKS_CLONE_DIR" log -1 "--date=format:%y%m.%d%H%M" --format="%cd" HEAD)
6✔
2969
    CURRENT_COMMIT_LOG=$(execute_git "$GITHOOKS_CLONE_DIR" log --pretty="format:%h (%s, %ad)" --date=short -1)
6✔
2970
    print_help_header
3✔
2971

2972
    echo
3✔
2973
    echo "Version: $CURRENT_COMMIT_DATE-$CURRENT_VERSION"
3✔
2974
    echo "Commit: $CURRENT_COMMIT_LOG"
3✔
2975
    echo
2✔
2976
}
2977

2978
#####################################################
2979
# Dispatches the command to the
2980
#   appropriate helper function to process it.
2981
#
2982
# Returns:
2983
#   1 if an unknown command was given,
2984
#   the exit code of the command otherwise
2985
#####################################################
2986
choose_command() {
2987
    CMD="$1"
428✔
2988
    [ -n "$CMD" ] && shift
854✔
2989

2990
    case "$CMD" in
428✔
2991
    "disable")
2992
        disable_hook "$@"
17✔
2993
        ;;
2994
    "enable")
2995
        enable_hook "$@"
11✔
2996
        ;;
2997
    "accept")
2998
        accept_changes "$@"
12✔
2999
        ;;
3000
    "exec")
3001
        execute_hook "$@"
10✔
3002
        ;;
3003
    "trust")
3004
        manage_trusted_repo "$@"
14✔
3005
        ;;
3006
    "list")
3007
        list_hooks "$@"
81✔
3008
        ;;
3009
    "shared")
3010
        manage_shared_hook_repos "$@"
119✔
3011
        ;;
3012
    "pull")
3013
        update_shared_hook_repos "$@"
1✔
3014
        ;;
3015
    "install")
3016
        run_ondemand_installation "$@"
8✔
3017
        ;;
3018
    "uninstall")
3019
        run_ondemand_uninstallation "$@"
3✔
3020
        ;;
3021
    "update")
3022
        run_update_check "$@"
12✔
3023
        ;;
3024
    "readme")
3025
        manage_readme_file "$@"
6✔
3026
        ;;
3027
    "ignore")
3028
        manage_ignore_files "$@"
12✔
3029
        ;;
3030
    "config")
3031
        manage_configuration "$@"
98✔
3032
        ;;
3033
    "tools")
3034
        manage_tools "$@"
5✔
3035
        ;;
3036
    "version")
3037
        print_current_version_number "$@"
5✔
3038
        ;;
3039
    "help")
3040
        print_help
6✔
3041
        ;;
3042
    *)
3043
        print_help
8✔
3044
        [ -n "$CMD" ] && echo "! Unknown command: $CMD" >&2
14✔
3045
        exit 1
8✔
3046
        ;;
3047
    esac
3048
}
3049

3050
set_main_variables
428✔
3051
# Choose and execute the command
3052
choose_command "$@"
428✔
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