gitpages

A collection of scripts to securely webhost and publish git repositories.
git clone https://scm.kuandu.systems/git-raw/gitpages.git
Log | Files | Refs | README | LICENSE

commit e3cf35ff4b002e699d5cdb01b89ecf072ffb6468
parent ac78252135f2c65c6bfb41d32f6d8f465084dced
Author: Fred Großkopf <fred@kuandu.systems>
Date:   Thu, 30 Apr 2026 17:07:14 +0200

Use /var/spool/gitpages/queue

to ensure post-receive hooks
and gitpages-update don't interfere.

Diffstat:
Mgitpages-update.sh | 34++++++++++++++++++++++------------
Minstall.sh | 38+++++++++++++++++++++++++++++---------
Mpost-receive.hook | 54+++++++++++++++++++++++++++++++++++++++++++++++++-----
Mtest-gitpages-update.sh | 24++++++++++++++++--------
Muninstall.sh | 73++++++++++++++++++++++++++++++++++++++++++++-----------------------------
5 files changed, 160 insertions(+), 63 deletions(-)

diff --git a/gitpages-update.sh b/gitpages-update.sh @@ -6,7 +6,7 @@ PATH=/usr/local/bin:/usr/bin:/bin:$PATH # --- Configuration --- LOCK_DIR="${GITPAGES_LOCK_DIR:-/tmp/gitpages-update.lock}" -QUEUE_FILE="${GITPAGES_QUEUE_FILE:-/var/spool/gitpages-update-queue}" +QUEUE_DIR="${GITPAGES_QUEUE_DIR:-/var/spool/gitpages/queue}" LOG_FILE="${GITPAGES_LOG_FILE:-/var/log/gitpages/update.log}" DEFAULT_CONFIG="/etc/gitpages.conf" @@ -55,20 +55,30 @@ main() { fi trap 'rmdir "$LOCK_DIR"' EXIT - if [ -f "$QUEUE_FILE" ] && [ -s "$QUEUE_FILE" ]; then - paths=$(sort -u "$QUEUE_FILE") - : > "$QUEUE_FILE" + if [ ! -d "$QUEUE_DIR" ]; then + log "Queue directory not found: $QUEUE_DIR" + return 0 + fi + + for jobfile in "$QUEUE_DIR"/*; do + [ -f "$jobfile" ] || continue + + repo_path=$(cat "$jobfile") - log "Starting update for: $paths" - for repo_path in $paths; do - if [ -d "$repo_path" ]; then - process_repo "$repo_path" "" "$CONFIG_FILE" + if [ -d "$repo_path" ]; then + log "Starting update for: $repo_path (job: $jobfile)" + if process_repo "$repo_path" "" "$CONFIG_FILE"; then + rm -f "$jobfile" else - error "Repository directory $repo_path not found" + error "Failed to process $repo_path (job: $jobfile)" fi - done - log "Update completed." - fi + else + error "Repository directory $repo_path not found (job: $jobfile)" + fi + done + + log "Update completed." + } mkdir -p "$(dirname "$LOG_FILE")" diff --git a/install.sh b/install.sh @@ -14,18 +14,36 @@ usage() { } setup_dirs() { - install -d -m 755 -o gitpages -g gitpages "$VAR_DIR" "$LOG_DIR" + install -d -m 755 -o "$USER_NAME" -g "$GROUP_NAME" "$VAR_DIR" "$LOG_DIR" install -d -m 755 "$BIN_DIR" "$SPOOL_DIR" } +setup_group() { + if ! groupinfo "$GROUP_NAME" > /dev/null 2>&1; then + if command -v groupadd > /dev/null 2>&1; then + groupadd "$GROUP_NAME" + log "Created group $GROUP_NAME" + else + error "Neither groupinfo nor groupadd found; cannot create group $GROUP_NAME. Please create it manually." + fi + fi +} + setup_user() { - if ! id "gitpages" > /dev/null 2>&1; then - useradd -s /sbin/nologin -d "$VAR_DIR" -c "Gitpages service user" gitpages + setup_group + + if ! id "$USER_NAME" > /dev/null 2>&1; then + useradd -s /sbin/nologin \ + -d "$VAR_DIR" \ + -c "Gitpages service user" \ + -g "$GROUP_NAME" \ + "$USER_NAME" + log "Created user $USER_NAME (primary group: $GROUP_NAME)" fi } setup_spool() { - install -m 664 -o "$GIT_USER" -g gitpages /dev/null "$QUEUE_FILE" + install -d -m 2775 -o "$GIT_USER" -g "$GROUP_NAME" "$QUEUE_DIR" } install_scripts() { @@ -46,6 +64,7 @@ GIT_SRC=/var/git/repos WEB_ROOT=/var/www/htdocs GIT_RAW=git-raw GIT_HTML=git +ASSETS_DIR=/var/gitpages/assets EOF log "Created default config in $ETC_DIR/gitpages.conf" fi @@ -53,7 +72,7 @@ EOF setup_logrotation() { if ! grep -q "$LOG_DIR/updates.log" "$ETC_DIR"/newsyslog.conf; then - echo "$LOG_DIR/updates.log gitpages:gitpages 640 5 100 * J" >> "$ETC_DIR"/newsyslog.conf + echo "$LOG_DIR/updates.log ${USER_NAME}:${GROUP_NAME} 640 5 100 * J" >> "$ETC_DIR"/newsyslog.conf log "Log rotation appended to $ETC_DIR/newsyslog.conf." else log "Log rotation already configured in $ETC_DIR/newsyslog.conf." @@ -67,11 +86,10 @@ setup_cron() { error "Cannot setup cron: $BIN_DIR/gitpages-update.sh not found." fi - if crontab -u gitpages -l 2> /dev/null | grep -qF "$cron_line"; then + if crontab -u "$USER_NAME" -l 2> /dev/null | grep -qF "$cron_line"; then log "Cronjob already exists and is configured correctly." else - # Just echo the line into the crontab command - echo "$cron_line" | crontab -u gitpages - + echo "$cron_line" | crontab -u "$USER_NAME" - log "Cronjob set for gitpages user." fi } @@ -79,13 +97,15 @@ setup_cron() { main() { [ $# -ne 1 ] && usage GIT_USER="$1" + USER_NAME="_gitpages" + GROUP_NAME="gitpages" BIN_DIR="/usr/local/sbin" ETC_DIR="/etc" VAR_DIR="/var/gitpages" LOG_DIR="/var/log/gitpages" SPOOL_DIR="/var/spool" - QUEUE_FILE="$SPOOL_DIR/gitpages-update-queue" + QUEUE_DIR="$SPOOL_DIR/gitpages/queue" if ! id "$GIT_USER" > /dev/null 2>&1; then error "User '$GIT_USER' does not exist." diff --git a/post-receive.hook b/post-receive.hook @@ -1,8 +1,52 @@ #!/bin/sh -# -# Place this in: /var/git/repos/<repo>.git/hooks/post-receive -QUEUE_FILE="/var/spool/gitpages-update-queue" +set -euf -# Append the full path of the current directory to the queue -echo "$PWD" >> "$QUEUE_FILE" +QUEUE_DIR="/var/spool/gitpages/queue" + +sanitize_repo_name() { + name="$1" + + [ -z "$name" ] && name="unknown" + + # Keep only printable chars, strip leading/trailing ./ + name=$(printf '%s\n' "$name" \ + | tr -cd '[:print:]\n' \ + | sed 's|^[./]*||; s|[./]*$||') + + # Keep only safe chars, replace others with - + name=$(printf '%s\n' "$name" \ + | sed 's|[^A-Za-z0-9._-]|-|g') + + # Collapse multiple - or . sequences + name=$(printf '%s\n' "$name" \ + | sed 's|[-.]\{2,\}|-|g') + + # Final sanity fall<E2><80><91>back + case "$name" in + '' | '.' | '..') name="invalid-repo" ;; + esac + + printf '%s\n' "$name" +} + +main() { + REPO_PATH="$PWD" + + # Extract repo name from path (e.g., "A" from /var/git/A.git) + repo_name=$(basename "$REPO_PATH" .git) + + # Sanitize repo name for safe filename + safe_repo_name=$(sanitize_repo_name "$repo_name") + + # Queue job file: /var/spool/gitpages/queue/<safe_repo_name> + jobfile="$QUEUE_DIR/$safe_repo_name" + + umask 002 + + # Write repo_path into the job file, atomically + printf '%s\n' "$REPO_PATH" > "$jobfile.$$" + mv "$jobfile.$$" "$jobfile" +} + +main diff --git a/test-gitpages-update.sh b/test-gitpages-update.sh @@ -1,11 +1,14 @@ #!/bin/sh + set -e # Configuration TEST_DIR=$(realpath "$(mktemp -d)") + export GITPAGES_LOCK_DIR="$TEST_DIR/lock" -export GITPAGES_QUEUE_FILE="$TEST_DIR/queue.txt" +export GITPAGES_QUEUE_DIR="$TEST_DIR/queue" export GITPAGES_LOG_FILE="$TEST_DIR/log.txt" + CONFIG_FILE="$TEST_DIR/gitpages.conf" GIT_RAW="$TEST_DIR/raw" REPO_PATH="$GIT_RAW/myrepo.git" @@ -17,7 +20,7 @@ export PATH="$TEST_DIR/bin:$PATH" mock_bin() { cat << EOF > "$TEST_DIR/bin/$1" #!/bin/sh -echo "MOCK: $1 called with \$*" +printf "MOCK: %s called with \$*\n" "$1" >&2 exit 0 EOF chmod +x "$TEST_DIR/bin/$1" @@ -28,8 +31,11 @@ mock_bin gitpages.sh # Setup test environment mkdir -p "$GIT_RAW" -touch "$REPO_PATH" -echo "$REPO_PATH" > "$GITPAGES_QUEUE_FILE" +mkdir -p "$GITPAGES_QUEUE_DIR" +mkdir -p "$REPO_PATH" + +# Create a job file whose content is the repo path +printf '%s\n' "$REPO_PATH" > "$GITPAGES_QUEUE_DIR/myrepo" # Create test config cat << EOF > "$CONFIG_FILE" @@ -39,11 +45,13 @@ EOF echo "Running test on gitpages-update.sh..." ./gitpages-update.sh -c "$CONFIG_FILE" -# Verify -if [ ! -s "$GITPAGES_QUEUE_FILE" ]; then - echo "PASS: Queue was cleared." +# Verify: queue directory should be empty (no job files left) +files=$(find "$GITPAGES_QUEUE_DIR" -type f | wc -l) +if [ "$files" -eq 0 ]; then + echo "PASS: Queue directory is empty." else - echo "FAIL: Queue was not cleared." + echo "FAIL: Queue directory is not empty (has $files files)." + find "$GITPAGES_QUEUE_DIR" -type f exit 1 fi diff --git a/uninstall.sh b/uninstall.sh @@ -8,50 +8,65 @@ BIN_DIR="/usr/local/sbin" ETC_DIR="/etc" VAR_DIR="/var/gitpages" LOG_DIR="/var/log/gitpages" +USER_NAME="_gitpages" +GROUP_NAME="gitpages" remove_cron() { - if id "gitpages" >/dev/null 2>&1; then - crontab -u gitpages -r 2>/dev/null || true - log "Removed crontab for gitpages user." - fi + if id "$USER_NAME" > /dev/null 2>&1; then + crontab -u "$USER_NAME" -r 2> /dev/null || true + log "Removed crontab for ${USER_NAME} user." + fi } remove_files() { - rm -f "$ETC_DIR/gitpages.conf" - rm -f "$BIN_DIR/gitpages.sh" "$BIN_DIR/gitpages-init.sh" \ - "$BIN_DIR/gitpages-mirror-git.sh" "$BIN_DIR/gitpages-update.sh" - log "Removed configuration and binaries." + rm -f "$ETC_DIR/gitpages.conf" + rm -f "$BIN_DIR/gitpages.sh" "$BIN_DIR/gitpages-init.sh" \ + "$BIN_DIR/gitpages-mirror-git.sh" "$BIN_DIR/gitpages-update.sh" + log "Removed configuration and binaries." } remove_logrotation() { - if [ -f "$ETC_DIR/newsyslog.conf" ]; then - # Use a different delimiter for sed since the path contains slashes - sed -i "\%$LOG_DIR/updates.log%d" "$ETC_DIR/newsyslog.conf" - log "Removed log rotation entry." - fi + if [ -f "$ETC_DIR/newsyslog.conf" ]; then + # Use a different delimiter for sed since the path contains slashes + sed -i "\%$LOG_DIR/updates.log%d" "$ETC_DIR/newsyslog.conf" + log "Removed log rotation entry." + fi } remove_user_and_data() { - if id "gitpages" >/dev/null 2>&1; then - userdel gitpages - log "Removed gitpages user." - fi - - if [ -d "$VAR_DIR" ]; then - rm -rf "$VAR_DIR" - log "Removed service directory $VAR_DIR." + if id "$USER_NAME" > /dev/null 2>&1; then + userdel "$USER_NAME" + log "Removed ${USER_NAME} user." + fi + + if [ -d "$VAR_DIR" ]; then + rm -rf "$VAR_DIR" + log "Removed service directory $VAR_DIR." + fi +} + +remove_group() { + if groupinfo "$GROUP_NAME" > /dev/null 2>&1; then + if command -v groupdel > /dev/null 2>&1; then + groupdel "$GROUP_NAME" + log "Removed group $GROUP_NAME" + else + # On some systems groupdel may not exist; fall back to manual /etc/group edit + log "WARNING: groupdel not found; please remove group $GROUP_NAME manually from /etc/group." fi + fi } main() { - log "Starting uninstallation of gitpages..." - - remove_cron - remove_files - remove_logrotation - remove_user_and_data - - log "Uninstallation complete." + log "Starting uninstallation of gitpages..." + + remove_cron + remove_files + remove_logrotation + remove_user_and_data + remove_group + + log "Uninstallation complete." } main "$@"