# Hooks — Overview ## Table of Contents - [Introduction](#introduction) - [What Are Git Hooks?](#what-are-git-hooks) - [Hook Types in Git](#hook-types-in-git) - [Client-Side Hooks](#client-side-hooks) - [Server-Side Hooks](#server-side-hooks) - [Project-Tick Hook Architecture](#project-tick-hook-architecture) - [The post-receive Hook](#the-post-receive-hook) - [Purpose and Design Goals](#purpose-and-design-goals) - [Script Anatomy](#script-anatomy) - [Configuration Block](#configuration-block) - [Remote Auto-Detection](#remote-auto-detection) - [Logging Subsystem](#logging-subsystem) - [Main Execution Loop](#main-execution-loop) - [Push Strategy](#push-strategy) - [Result Tracking](#result-tracking) - [Failure Notification](#failure-notification) - [Exit Behavior](#exit-behavior) - [Supported Forge Targets](#supported-forge-targets) - [GitHub](#github) - [GitLab](#gitlab) - [Codeberg](#codeberg) - [SourceForge](#sourceforge) - [Authentication Methods](#authentication-methods) - [SSH Key Authentication](#ssh-key-authentication) - [HTTPS Token Authentication](#https-token-authentication) - [Environment Variables](#environment-variables) - [Installation Guide](#installation-guide) - [Directory Layout](#directory-layout) - [Operational Flow Diagram](#operational-flow-diagram) - [Interaction with Other Project-Tick Components](#interaction-with-other-project-tick-components) - [Troubleshooting Common Issues](#troubleshooting-common-issues) - [Security Considerations](#security-considerations) - [Related Documentation](#related-documentation) --- ## Introduction The `hooks/` directory in the Project-Tick monorepo contains Git hook scripts that automate repository management tasks. These hooks are designed to run on the bare repository that serves as the canonical upstream source for the Project-Tick project. The hooks system currently consists of a single, well-structured script: | File | Type | Purpose | |------|------|---------| | `hooks/post-receive` | Bash script | Mirror pushes to multiple forge platforms | This document provides a comprehensive explanation of how the hooks system works, how Git hooks function in general, and how the Project-Tick hook integrates with the broader project infrastructure. --- ## What Are Git Hooks? Git hooks are executable scripts that Git runs automatically at specific points in the version control workflow. They reside in the `.git/hooks/` directory of a repository (or the `hooks/` directory of a bare repository). Git ships with sample hook scripts (with `.sample` extensions) that are inactive by default. Hooks serve as extension points for automating tasks such as: - Enforcing commit message conventions - Running linters or tests before accepting commits - Triggering CI/CD pipelines after pushes - Synchronizing mirrors to external platforms - Sending notifications on repository events A hook is activated by placing an executable file with the correct name (no extension) in the hooks directory. Git invokes the hook at the corresponding event and passes relevant data via standard input or command-line arguments. --- ## Hook Types in Git ### Client-Side Hooks Client-side hooks run on the developer's local machine during operations like committing, merging, and rebasing: | Hook | Trigger | Use Case | |------|---------|----------| | `pre-commit` | Before a commit is created | Lint source files, check formatting | | `prepare-commit-msg` | After default message generated | Auto-populate commit templates | | `commit-msg` | After user enters message | Validate commit message format | | `post-commit` | After commit completes | Post-commit notifications | | `pre-rebase` | Before rebase starts | Prevent rebasing published branches | | `post-merge` | After a merge completes | Restore tracked file permissions | | `pre-push` | Before push to remote | Run tests before sharing code | | `post-checkout` | After `git checkout` | Set up working directory environment | ### Server-Side Hooks Server-side hooks run on the repository that receives pushes. These are the hooks relevant to the Project-Tick infrastructure: | Hook | Trigger | Input | Use Case | |------|---------|-------|----------| | `pre-receive` | Before any refs updated | ` ` per line on stdin | Reject pushes that violate policies | | `update` | Per-ref, before each ref updated | ` ` as arguments | Per-branch access control | | `post-receive` | After all refs updated | ` ` per line on stdin | Trigger CI, mirrors, notifications | | `post-update` | After refs updated | Updated ref names as arguments | Update `info/refs` for dumb HTTP | The **post-receive** hook is the one used by Project-Tick. It fires after all refs have been successfully updated, making it the ideal place for mirror synchronization — the push to the canonical repo has already succeeded, so mirroring can proceed without blocking the original pusher. --- ## Project-Tick Hook Architecture The Project-Tick hooks system follows a minimal, single-script architecture: ``` hooks/ └── post-receive # The only hook script — handles multi-forge mirroring ``` The script is stored in the monorepo source tree at `hooks/post-receive` and is deployed to the bare repository at the path: ``` /path/to/project-tick.git/hooks/post-receive ``` ### Design Principles 1. **Single responsibility** — The script does exactly one thing: mirror pushes to configured forge remotes. 2. **Fail-safe defaults** — If no mirror remotes are configured, the script exits silently without error. 3. **Comprehensive logging** — Every action is logged with UTC timestamps. 4. **Non-blocking on partial failure** — If one remote fails, the script continues pushing to the remaining remotes. 5. **Notification support** — Optional email alerts on failure. 6. **Zero external dependencies** — Uses only bash builtins, `git`, `date`, `tee`, and optionally `mail`. --- ## The post-receive Hook ### Purpose and Design Goals The `post-receive` script in `hooks/post-receive` serves as a multi-forge mirror synchronization tool. When a push lands on the canonical bare repository, this hook automatically replicates all refs (branches, tags, notes) to every configured mirror remote. The opening comment block documents this purpose: ```bash # ============================================================================== # post-receive hook — Mirror push to multiple forges # ============================================================================== ``` ### Script Anatomy The script is structured into four clearly delineated sections: 1. **Header block** (lines 1–33) — Shebang, documentation comments, and usage instructions 2. **Configuration block** (lines 35–53) — Variable initialization and remote auto-detection 3. **Logging function** (lines 55–62) — The `log()` helper 4. **Main execution** (lines 64–112) — Ref reading, push loop, summary, notification, exit The script begins with strict error handling: ```bash #!/usr/bin/env bash set -euo pipefail ``` The `set -euo pipefail` line enables three safety nets: | Flag | Effect | |------|--------| | `-e` | Exit immediately if any command fails | | `-u` | Treat unset variables as errors | | `-o pipefail` | A pipeline fails if any component fails, not just the last command | ### Configuration Block The configuration block initializes three environment-controlled variables: ```bash MIRROR_REMOTES="${MIRROR_REMOTES:-}" MIRROR_LOG="${MIRROR_LOG:-/var/log/git-mirror.log}" ``` | Variable | Default | Purpose | |----------|---------|---------| | `MIRROR_REMOTES` | `""` (empty — triggers auto-detection) | Space-separated list of git remote names | | `MIRROR_LOG` | `/var/log/git-mirror.log` | Path to the log file | | `MIRROR_NOTIFY` | `""` (unset — notifications disabled) | Email address for failure alerts | The `${VAR:-default}` syntax provides defaults while allowing environment variable overrides. This means an administrator can control behavior without modifying the script: ```bash MIRROR_REMOTES="github gitlab" MIRROR_LOG=/tmp/mirror.log ./hooks/post-receive ``` ### Remote Auto-Detection If `MIRROR_REMOTES` is empty (the default), the script auto-detects mirror targets: ```bash if [[ -z "$MIRROR_REMOTES" ]]; then MIRROR_REMOTES=$(git remote | grep -v '^origin$' || true) fi ``` This runs `git remote` to list all configured remotes, then filters out `origin` with `grep -v '^origin$'`. The `|| true` suffix prevents `set -e` from terminating the script if `grep` finds no matches (which would produce exit code 1). The rationale: `origin` typically points to the canonical repository itself. Everything else is assumed to be a mirror target. This convention allows adding new mirrors simply by running: ```bash git remote add ``` If after auto-detection the list is still empty, the script exits cleanly: ```bash if [[ -z "$MIRROR_REMOTES" ]]; then echo "[mirror] No mirror remotes configured. Skipping." >&2 exit 0 fi ``` This is a **non-error exit** (`exit 0`) because having no mirrors is a valid configuration — the hook should not cause the push to appear to have failed. ### Logging Subsystem The `log()` function provides timestamped logging to both stdout and a persistent log file: ```bash log() { local timestamp timestamp="$(date -u '+%Y-%m-%d %H:%M:%S UTC')" echo "[$timestamp] $*" | tee -a "$MIRROR_LOG" 2>/dev/null || echo "[$timestamp] $*" } ``` Key characteristics: - **UTC timestamps** — `date -u` ensures consistent timestamps regardless of server timezone. - **Format** — `[2026-04-05 14:30:00 UTC] message` — ISO 8601 date with human-readable time. - **Dual output** — `tee -a` appends to `$MIRROR_LOG` while also writing to stdout. - **Graceful fallback** — If the log file is not writable (permissions, missing directory), `2>/dev/null` suppresses the `tee` error, and the `||` fallback ensures the message still appears on stdout. - **`local` variable** — The `timestamp` variable is scoped to the function to avoid polluting the global namespace. ### Main Execution Loop The main section begins by reading the ref update data from stdin: ```bash log "=== Mirror push triggered ===" REFS=() while read -r oldrev newrev refname; do REFS+=("$refname") log " ref: $refname ($oldrev -> $newrev)" done ``` Git's `post-receive` hook receives one line per updated ref on stdin, formatted as: ``` ``` For example: ``` abc123 def456 refs/heads/main 000000 789abc refs/tags/v1.0.0 ``` The `read -r` flag prevents backslash interpretation. Each ref name is accumulated in the `REFS` bash array for later use in notifications. ### Push Strategy For each mirror remote, the script performs a `--mirror --force` push: ```bash for remote in $MIRROR_REMOTES; do log "Pushing to remote: $remote" if git push --mirror --force "$remote" 2>&1 | tee -a "$MIRROR_LOG" 2>/dev/null; then SUCCEEDED_REMOTES+=("$remote") log " ✓ Successfully pushed to $remote" else FAILED_REMOTES+=("$remote") log " ✗ FAILED to push to $remote" fi done ``` The `git push` flags are critical: | Flag | Effect | |------|--------| | `--mirror` | Push all refs under `refs/` — branches, tags, notes, replace refs, everything. Also deletes remote refs that no longer exist locally. | | `--force` | Force-update refs that have diverged. Ensures the mirror is an exact copy. | The `2>&1` redirects stderr to stdout so both success and error messages are captured by `tee`. The `if` statement checks the exit code of the entire pipeline — if `git push` fails (non-zero exit), the remote is added to `FAILED_REMOTES`. **Important**: The loop does **not** use `set -e` behavior for individual pushes because the `if` statement captures the exit code rather than triggering an immediate exit. This ensures all remotes are attempted even if some fail. ### Result Tracking Two arrays track the outcome: ```bash FAILED_REMOTES=() SUCCEEDED_REMOTES=() ``` After the loop, a summary is logged: ```bash log "--- Summary ---" log " Succeeded: ${SUCCEEDED_REMOTES[*]:-none}" log " Failed: ${FAILED_REMOTES[*]:-none}" ``` The `${array[*]:-none}` syntax expands all array elements separated by spaces, or prints "none" if the array is empty. ### Failure Notification When mirrors fail and `MIRROR_NOTIFY` is set, the script sends an email: ```bash if [[ ${#FAILED_REMOTES[@]} -gt 0 && -n "${MIRROR_NOTIFY:-}" ]]; then if command -v mail &>/dev/null; then { echo "Mirror push failed for the following remotes:" printf ' - %s\n' "${FAILED_REMOTES[@]}" echo "" echo "Repository: $(pwd)" echo "Refs updated:" printf ' %s\n' "${REFS[@]}" echo "" echo "Check log: $MIRROR_LOG" } | mail -s "[git-mirror] Push failure in $(basename "$(pwd)")" "$MIRROR_NOTIFY" fi fi ``` The notification includes: - Which remotes failed (`FAILED_REMOTES`) - The repository path (`$(pwd)`) - Which refs were updated (`REFS`) - Where to find detailed logs (`$MIRROR_LOG`) The subject line uses the repository directory name: `[git-mirror] Push failure in project-tick.git`. The `command -v mail &>/dev/null` check ensures the script doesn't crash if `mail` is not installed — it simply skips notification silently. ### Exit Behavior ```bash if [[ ${#FAILED_REMOTES[@]} -gt 0 ]]; then log "=== Finished with errors ===" exit 1 fi log "=== Finished successfully ===" exit 0 ``` | Condition | Exit Code | Meaning | |-----------|-----------|---------| | All remotes succeeded | `0` | Success — the pusher sees no error | | One or more remotes failed | `1` | Failure — the pusher sees an error message | | No remotes configured | `0` | No-op — silent success | **Note**: A non-zero exit from `post-receive` does **not** reject the push (the refs are already updated). It only causes Git to display the hook's output as an error to the pusher. This alerts the developer that mirroring failed without rolling back their work. --- ## Supported Forge Targets The script header documents four forge platforms with example remote URLs: ### GitHub ```bash # SSH git remote add github git@github.com:Project-Tick/Project-Tick.git # HTTPS with token git remote add github https://x-access-token:TOKEN@github.com/Project-Tick/Project-Tick.git ``` GitHub uses `x-access-token` as the username for personal access tokens and GitHub App installation tokens. ### GitLab ```bash # SSH git remote add gitlab git@gitlab.com:Project-Tick/Project-Tick.git # HTTPS with token git remote add gitlab https://oauth2:TOKEN@gitlab.com/Project-Tick/Project-Tick.git ``` GitLab uses `oauth2` as the username for personal access tokens with HTTPS. ### Codeberg ```bash # SSH git remote add codeberg git@codeberg.org:Project-Tick/Project-Tick.git # HTTPS with token git remote add codeberg https://TOKEN@codeberg.org/Project-Tick/Project-Tick.git ``` Codeberg (Gitea-based) accepts the token directly as the username with no password. ### SourceForge ```bash # SSH only git remote add sourceforge ssh://USERNAME@git.code.sf.net/p/project-tick/code ``` SourceForge uses a non-standard SSH URL format with a username prefix and a project-specific path structure. --- ## Authentication Methods ### SSH Key Authentication SSH-based authentication requires: 1. An SSH keypair accessible to the user running the Git daemon 2. The public key registered on each forge platform 3. Correct SSH host key verification (or entries in `~/.ssh/known_hosts`) For automated server-side usage, a dedicated deploy key is recommended: ```bash # Generate a dedicated mirror key ssh-keygen -t ed25519 -f ~/.ssh/mirror_key -N "" # Configure SSH to use it for each host cat >> ~/.ssh/config <