commit 95734851209d836b96267443704fabe68fcac50b Author: Fred Großkopf <fred@kuandu.systems> Date: Wed, 15 Apr 2026 10:19:21 +0200 Init Diffstat:
| A | README.md | | | 87 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | gitpages-init.sh | | | 53 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | gitpages.sh | | | 230 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | install.sh | | | 64 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | mirror-git.sh | | | 70 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | post-receive.hook | | | 21 | +++++++++++++++++++++ |
| A | test-gitpages.sh | | | 335 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | test-mirror-git.sh | | | 142 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
8 files changed, 1002 insertions(+), 0 deletions(-)
diff --git a/README.md b/README.md @@ -0,0 +1,87 @@ +# Git Web Hosting (OpenBSD + httpd + stagit) + +Static git web interface using bare repos, httpd, and stagit. + +## Directory Structure + +Example directory structure: + +``` +scm.mydomain.com/ +├── raw-git/ ← Mirrored bare repos (git-http-backend) +└── git/ ← Stagit HTML (static web pages) +``` + +## Scripts + +### mirror-git.sh - Mirror bare git repos + + +`mirror-git.sh` replicates bare Git repositories from a secure central server to a +web-accessible location, enabling HTTP(S) Git access and static HTML generation +(stagit/gitpages) while keeping push targets isolated from public web exposure. + +``` +./mirror-git.sh /home/vcs/git /var/www/htdocs/scm.mydomain.com/raw-git +``` + +### gitpages-init.sh - Initial setup + +`gitpages-init.sh` mirrors Git repositories and generates HTML files in one go. + +``` +./init-gitweb.sh /home/vcs/git /var/www/htdocs/scm.mydomain.com/raw-git /var/www/htdocs/scm.mydomain.com/git +``` + +### gitpages.sh - Generate HTML only + +A single repo: + +``` +./gitpages.sh /raw-git/myrepo.git /git/ +``` + +All repos: + +``` +./gitpages.sh /raw-git/ /git/ +``` + +With assets: + +``` +./gitpages.sh /raw-git/ /git/ /assets/ +``` + +When `ASSETS_DIR` is given: + +- Assets are copied into the gitpages root +- Then symlinked into every repo‑HTML directory (e.g., `git/myrepo/favicon.ico` → `../favicon.ico`) + +### Customizing HTML + +You can use an optional config file `gitpages.conf` to change titles and +descriptions in stagit and stagit-index output. + +Example: + +```ini +SITE_NAME=mycompany +INDEX_TITLE=New Title +INDEX_DESCRIPTION=New Description +``` + +If gitpages.conf exists or is passed with -c, gitpages.sh rewrites: + +- index.html title and description: + +```html +<title>New Title</title> +<span class="desc">New Description</span> +``` + +- repository pages title tags: + +```html +<title>Log - myrepo - mycompany</title> +``` diff --git a/gitpages-init.sh b/gitpages-init.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -eu + +usage() { + cat << EOF >&2 +usage: $(basename "$0") SRC RAW_DST HTML_DST [ASSETS_DIR] + +Requires: mirror-git.sh gitpages.sh git stagit stagit-index + +One-command git hosting setup: + 1. Mirror repos: SRC → RAW_DST + 2. Generate HTML: RAW_DST → HTML_DST + +Examples: + $(basename "$0") /home/user/repos /var/www/git-raw /var/www/git +EOF + exit 1 +} + +error() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} + +check_deps() { + for cmd in mirror-git.sh gitpages.sh git stagit stagit-index; do + command -v "$cmd" > /dev/null 2>&1 || error "$cmd not found (install required dependencies)" + done +} + +check_dir_writable() { + [ -d "$1" ] || error "$1 not a directory" + [ -w "$1" ] || error "$1 not writable" +} + +main() { + [ $# -lt 3 ] || [ $# -gt 4 ] || usage + + SRC="$1" RAW_DST="$2" HTML_DST="$3" ASSETS_DIR="${4:-}" + + check_deps + check_dir_writable "$RAW_DST" + check_dir_writable "$HTML_DST" + + printf 'info: 1/2 Mirror repos %s → %s\n' "$SRC" "$RAW_DST" + mirror-git.sh "$SRC" "$RAW_DST" + + printf 'info: 2/2 Generate HTML %s → %s\n' "$RAW_DST" "$HTML_DST" + gitpages.sh "$RAW_DST" "$HTML_DST" "$ASSETS_DIR" +} + +main "$@" diff --git a/gitpages.sh b/gitpages.sh @@ -0,0 +1,230 @@ +#!/bin/sh + +set -eu + +# === CONSTANTS === + +CONFIG_FILE_NAME="gitpages.conf" + +usage() { + cat << EOF +usage: $(basename "$0") [-c CONFIG_FILE] SRC DST_DIR [ASSETS_DIR] + +Generates static git HTML pages + index using stagit. + +Arguments: + SRC Single git repo OR directory containing *.git repos + DST_DIR Output directory for HTML pages + ASSETS_DIR Optional: CSS, favicon, etc. directory + +Options: + -c CONFIG_FILE Path to config (default: \$(dirname "\$0")/$CONFIG_FILE_NAME) + -h Show this help + +Examples: + $(basename "$0") /path/to/repo.git /var/www/git + $(basename "$0") /path/to/repos /var/www/git + $(basename "$0") -c /etc/$CONFIG_FILE_NAME /path/to/repos /var/www/git +EOF +} + +# === UTILITY FUNCTIONS === +error() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} +warning() { printf 'warning: %s\n' "$1" >&2; } +info() { printf 'info: %s\n' "$1"; } + +repo_base() { basename "$1" ".git"; } +html_dir_for() { printf '%s/%s' "$2" "$(repo_base "$1")"; } + +sed_escape() { printf '%s\n' "$1" | sed 's/[\\/&]/\\&/g'; } + +# === VALIDATION === +check_args() { + case $# in 2 | 3) ;; *) error "usage: $0 SRC DST_DIR [ASSETS_DIR]" ;; esac +} + +check_html_writable() { + [ -d "$DST_DIR" ] || error "$DST_DIR does not exist" + [ -w "$DST_DIR" ] || error "cannot write to $DST_DIR" +} + +check_stagit() { + command -v stagit > /dev/null 2>&1 || error "stagit not found" + command -v stagit-index > /dev/null 2>&1 || error "stagit-index not found" +} + +check_assets() { + [ -n "${ASSETS_DIR:-}" ] || return 0 + [ -d "$ASSETS_DIR" ] || error "ASSETS_DIR '$ASSETS_DIR' is not a directory" + [ -r "$ASSETS_DIR" ] || error "cannot read ASSETS_DIR '$ASSETS_DIR'" + + found=0 + for f in "$ASSETS_DIR"/*; do [ -e "$f" ] && { + found=1 + break + }; done + [ "$found" -eq 1 ] || warning "ASSETS_DIR '$ASSETS_DIR' is empty" +} + +is_git_repo() { git -C "$1" rev-parse --git-dir > /dev/null 2>&1; } +has_git_repos() { + for repo in "$1"/*.git; do [ -e "$repo" ] && return 0; done + false +} + +# === CORE FUNCTIONS === +generate_repo_html() { + repo="$1" dst_dir="$2" repo_name="$3" + html_repo_dir="$dst_dir/$repo_name" + + rm -rf "$html_repo_dir" + mkdir -p "$html_repo_dir" + cd "$html_repo_dir" + stagit "$repo" > /dev/null 2>&1 +} + +process_repo_titles() { + repo="$1" repo_name="$2" html_repo_dir="$3" + [ -d "$html_repo_dir" ] || return 0 + [ -n "${SITE_NAME:-}" ] || return 0 + + esc_site=$(sed_escape "$SITE_NAME") + find "$html_repo_dir" -name "*.html" -type f -exec sed -i \ + "s|<title>\([^<]* - $repo_name - \)[^<]*|<title>\1$esc_site|g" {} + +} + +generate_index() { + cd "$DST_DIR" + stagit-index "$SRC_DIR"/*.git > index.html +} + +process_index() { + [ -f "$DST_DIR/index.html" ] || return 0 + + [ -n "${INDEX_TITLE:-}" ] && { + esc_title=$(sed_escape "$INDEX_TITLE") + sed -i "s|<title>Repositories</title>|<title>$esc_title</title>|g" "$DST_DIR/index.html" \ + || warning "Failed to update index.html title" + } + + [ -n "${INDEX_DESC:-}" ] && { + esc_desc=$(sed_escape "$INDEX_DESC") + sed -i "s|<span class=\"desc\">Repositories</span>|<span class=\"desc\">$esc_desc</span>|g" \ + "$DST_DIR/index.html" || warning "Failed to update index.html description" + } +} + +deploy_assets() { + [ -d "$ASSETS_DIR" ] || return 0 + + # Copy assets to root + mkdir -p "$DST_DIR" + for f in "$ASSETS_DIR"/*; do + [ -f "$f" ] || continue + cp -L "$f" "$DST_DIR/$(basename "$f")" + done + + # Symlink assets into each repo dir + for repo_dir in "$DST_DIR"/*/log.html; do + [ -f "$repo_dir" ] || continue + repo_path="${repo_dir%/*}" + cd "$repo_path" + for f in "$ASSETS_DIR"/*; do + [ -f "$f" ] || continue + ln -sf "../$(basename "$f")" . + done + done +} + +load_config() { + cfg="$1" + [ -r "$cfg" ] || return 0 + + for key in SITE_NAME INDEX_TITLE INDEX_DESCRIPTION; do + val=$(awk -F= -v k="$key" '$1 == k {print $2; exit}' "$cfg") || continue + [ -n "$val" ] || continue + case "$key" in + SITE_NAME) SITE_NAME="$val" ;; + INDEX_TITLE) INDEX_TITLE="$val" ;; + INDEX_DESCRIPTION) INDEX_DESC="$val" ;; + esac + done +} + +# === MAIN LOGIC === +main() { + CONFIG_FILE="" + + # Parse options + while getopts ":hc:" opt; do + case "$opt" in + c) CONFIG_FILE="$OPTARG" ;; + h) + usage + exit 0 + ;; + :) error "option requires an argument: -$OPTARG" ;; + \?) error "invalid option: -$OPTARG" ;; + esac + done + shift $((OPTIND - 1)) + + check_args "$@" + SRC="$1" DST_DIR="$2" ASSETS_DIR="${3:-}" + SITE_NAME="" INDEX_TITLE="" INDEX_DESC="" + + # Validate prerequisites + check_html_writable && check_stagit && check_assets + + # Determine repo source + if is_git_repo "$SRC"; then + SRC_DIR="$(dirname "$SRC")" + SINGLE_REPO="$SRC" + else + SRC_DIR="$SRC" + SINGLE_REPO="" + has_git_repos "$SRC_DIR" || error "$SRC is neither git repo nor dir with *.git repos" + fi + + # Load config + [ -z "$CONFIG_FILE" ] && CONFIG_FILE="$(dirname "$0")/${CONFIG_FILE_NAME}" + load_config "$CONFIG_FILE" + + # Clear destination (safety-checKed earlier) + # shellcheck disable=SC2115 # wants non-posix compatible ${var:?} + [ "$DST_DIR" != "/" ] && rm -rf "$DST_DIR"/* + + # Unified repo processing + if [ -n "$SINGLE_REPO" ]; then + repo="$SINGLE_REPO" + repo_name=$(repo_base "$repo") + generate_repo_html "$repo" "$DST_DIR" "$repo_name" + process_repo_titles "$repo" "$repo_name" "$DST_DIR/$repo_name" + generate_index + info "Generated single repo: $repo → $DST_DIR" + else + for repo in "$SRC_DIR"/*.git; do + [ -e "$repo" ] || continue + is_git_repo "$repo" || { + info "Skipping $repo: not a git repository" + continue + } + + repo_name=$(repo_base "$repo") + generate_repo_html "$repo" "$DST_DIR" "$repo_name" + process_repo_titles "$repo" "$repo_name" "$DST_DIR/$repo_name" + done + generate_index + info "Generated all repos: $SRC → $DST_DIR" + fi + + process_index + [ -n "$ASSETS_DIR" ] && deploy_assets + + info "Generation complete" +} + +main "$@" diff --git a/install.sh b/install.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +set -eu + +SRC_DIR="$(cd "$(dirname "$0")" && pwd)" +DST_DIR="${1:-$HOME/bin}" + +error() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} + +info() { + printf 'info: %s\n' "$1" +} + +print_next_steps() { + cat << EOF + +Next: make sure to update your path, e.g.: + +echo 'export PATH="$DST_DIR:\$PATH"' >> ~/.profile && source ~/.profile + +EOF +} + +ensure_destination() { + mkdir -p "$DST_DIR" 2> /dev/null || error "$DST_DIR: cannot create directory" + [ -w "$DST_DIR" ] || error "$DST_DIR: not writable" +} + +check_source() { + [ -n "$(ls "$SRC_DIR"/*.sh 2> /dev/null)" ] \ + || error "no *.sh files found in $SRC_DIR" +} + +install() { + for src in "$SRC_DIR"/*.sh; do + [ -f "$src" ] || continue + name="$(basename "$src" .sh)" + target="$DST_DIR/$name" + + if [ -L "$target" ]; then + rm "$target" + info "Updated: $name" + else + info "Created: $name" + fi + + ln -sf "$src" "$target" + chmod +x "$target" + done + + info "Installation complete: $DST_DIR" + print_next_steps +} + +main() { + check_source + ensure_destination + install +} + +main "$@" diff --git a/mirror-git.sh b/mirror-git.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -eu + +usage() { + cat << EOF +usage: $(basename "$0") SRC DST_DIR + +Mirror bare git repositories to destination. + + SRC Bare repo OR directory of *.git repos + DST_DIR Output directory + +Examples: + $(basename "$0") /src/repo.git /srv/git + $(basename "$0") /src/repos /srv/git +EOF +} + +error() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} +warning() { printf 'warning: %s\n' "$1" >&2; } +info() { printf 'info: %s\n' "$1"; } + +check_args() { [ $# -eq 2 ] || { + usage >&2 + error "usage: $0 SRC DST_DIR" +}; } + +is_bare_repo() { git --git-dir="$1" rev-parse --is-bare-repository > /dev/null 2>&1; } +has_repos() { [ -d "$1" ] && ls "$1"/*.git > /dev/null 2>&1 2> /dev/null; } + +sync_repo() { + src="$1" dst_dir="$2" name=$(basename "$src") + dst="$dst_dir/$name" + + is_bare_repo "$src" || { + warning "skipping $name (not bare)" + return 0 + } + + rm -rf "$dst" + cp -R "$src" "$dst" + git --git-dir="$dst" update-server-info > /dev/null 2>&1 +} + +main() { + check_args "$@" + SRC="$1" DST_DIR="$2" + + [ -d "$DST_DIR" ] && [ -w "$DST_DIR" ] || error "DST_DIR '$DST_DIR' not writable" + + if is_bare_repo "$SRC"; then + sync_repo "$SRC" "$DST_DIR" + info "synced: $SRC → $DST_DIR" + elif has_repos "$SRC"; then + [ "$DST_DIR" != "/" ] && rm -rf "$DST_DIR"/* + for repo in "$SRC"/*.git; do + [ -d "$repo" ] || break + sync_repo "$repo" "$DST_DIR" + done + info "synced all: $SRC → $DST_DIR" + else + error "$SRC: neither bare repo nor *.git directory" + fi +} + +main "$@" diff --git a/post-receive.hook b/post-receive.hook @@ -0,0 +1,21 @@ +#!/bin/sh + +set -eu + +repo_path="$(pwd)" +repo_name="$(basename "$repo_path" .git)" + +check_dir() { + [ -d "$1" ] || { + printf 'error: %s is not a directory\n' "$1" >&2 + exit 1 + } +} + +check_dir "$RAW_DST" +check_dir "$HTML_DST" + +printf 'Updating web view for %s\n' "$repo_name" + +mirror-git "$repo_path" "$RAW_DST" +gitpages "$RAW_DST/$repo_name.git" "$HTML_DST" diff --git a/test-gitpages.sh b/test-gitpages.sh @@ -0,0 +1,335 @@ +#!/bin/sh + +set -eu + +SCRIPT_NAME="./gitpages.sh" + +check_script() { + [ ! -x "$SCRIPT_NAME" ] && error "$SCRIPT_NAME not found or not executable" +} + +check_environment() { + command -v git > /dev/null 2>&1 || error "git not found" + command -v stagit > /dev/null 2>&1 || error "stagit not found" + command -v stagit-index > /dev/null 2>&1 || error "stagit-index not found" + command -v realpath > /dev/null 2>&1 || error "realpath not found" +} + +TEST_BASE="" +cleanup() { + [ -n "$TEST_BASE" ] && rm -rf "$TEST_BASE" +} +trap cleanup EXIT INT TERM + +error() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +success() { + printf 'PASS: %s\n' "$1" +} + +setup() { + TEST_BASE=$(mktemp -d) || error "Failed to create temp dir" + RAW_SRC="$TEST_BASE/raw-src" + HTML_DST="$TEST_BASE/html-dst" + mkdir -p "$RAW_SRC" "$HTML_DST" +} + +setup_assets() { + dst="$1" + mkdir -p "$dst" + printf 'icon\n' > "$dst/favicon.ico" + printf 'body {}\n' > "$dst/style.css" +} + +assert_fails() { + test_num="$1" + test_desc="$2" + shift 2 + printf '=== Test %s: %s ===\n' "$test_num" "$test_desc" + + if "$@" > /dev/null 2>&1; then + error "Should fail" + else + success "Rejects $test_desc" + fi +} + +assert_html_generated() { + html_dir="$1" + test_desc="$2" + + if [ ! -d "$html_dir" ]; then + error "$test_desc" + elif [ ! -f "$html_dir/log.html" ]; then + error "$test_desc" + else + success "$test_desc" + fi +} + +check_dir_exists() { + dir="$1" + msg="$2" + if [ -d "$dir" ]; then + success "$msg" + else + error "$msg" + fi +} + +check_dir_missing() { + dir="$1" + msg="$2" + if [ ! -d "$dir" ]; then + success "$msg" + else + error "$msg" + fi +} + +check_file_exists() { + file="$1" + msg="$2" + if [ -f "$file" ]; then + success "$msg" + else + error "$msg" + fi +} + +extract_title() { + file="$1" + title_line=$(grep -m1 '<title>' "$file" || true) + if [ -z "$title_line" ]; then + error "no <title> line in $file" + fi + printf '%s\n' "$title_line" | sed 's/^<title>//' | tr -d '\n' +} + +test_args() { + assert_fails 1 "wrong number of arguments" "$SCRIPT_NAME" "$RAW_SRC" +} + +test_bad_html_dst() { + bad_dst="$TEST_BASE/bad_dst" + touch "$bad_dst" + chmod 444 "$bad_dst" + assert_fails 2 "unwritable HTML dir" "$SCRIPT_NAME" "$RAW_SRC" "$bad_dst" +} + +test_empty_src() { + assert_fails 3 "empty source directory" "$SCRIPT_NAME" "$RAW_SRC" "$HTML_DST" +} + +test_single_repo() { + printf '=== Test 4: Single repo ===\n' + mkdir -p "$RAW_SRC/myrepo.git" + (cd "$RAW_SRC/myrepo.git" && git init --bare . > /dev/null 2>&1) + + "$SCRIPT_NAME" "$RAW_SRC/myrepo.git" "$HTML_DST" > /dev/null 2>&1 + + assert_html_generated "$HTML_DST/myrepo" "single repo HTML" +} + +test_full_sync() { + printf '=== Test 5: Full sync ===\n' + mkdir -p "$RAW_SRC/repo1.git" "$RAW_SRC/repo2" + (cd "$RAW_SRC/repo1.git" && git init --bare . > /dev/null 2>&1) + (cd "$RAW_SRC/repo2" && git init > /dev/null 2>&1) + + "$SCRIPT_NAME" "$RAW_SRC" "$HTML_DST" > /dev/null 2>&1 + + check_dir_exists "$HTML_DST/repo1" "repo1 HTML generated" + check_dir_missing "$HTML_DST/repo2" "non-bare repo2 skipped" +} + +test_stale_cleanup() { + printf '=== Test 6: Stale cleanup ===\n' + mkdir -p "$HTML_DST/stale-repo" + echo "stale" > "$HTML_DST/stale-repo/index.html" + mkdir -p "$RAW_SRC/repo1.git" + (cd "$RAW_SRC/repo1.git" && git init --bare . > /dev/null 2>&1) + + "$SCRIPT_NAME" "$RAW_SRC" "$HTML_DST" > /dev/null 2>&1 + + check_dir_missing "$HTML_DST/stale-repo" "stale repo cleaned" + check_dir_exists "$HTML_DST/repo1" "new repo generated" +} + +test_assets_root_copy() { + printf '=== Test 7: Root assets copied ===\n' + + ASSETS_DIR="$TEST_BASE/assets" + setup_assets "$ASSETS_DIR" + + mkdir -p "$RAW_SRC/myrepo.git" + (cd "$RAW_SRC/myrepo.git" && git init --bare . > /dev/null 2>&1) + + "$SCRIPT_NAME" "$RAW_SRC/myrepo.git" "$HTML_DST" "$ASSETS_DIR" > /dev/null 2>&1 + + check_file_exists "$HTML_DST/favicon.ico" "asset favicon.ico copied to root" + check_file_exists "$HTML_DST/style.css" "asset style.css copied to root" +} + +test_assets_repo_symlinks() { + printf '=== Test 8: Asset symlinks in repo dir (self‑contained) ===\n' + + ASSETS_DIR="$TEST_BASE/assets-repo" + setup_assets "$ASSETS_DIR" + + mkdir -p "$ASSETS_DIR" "$RAW_SRC/myrepo.git" + (cd "$RAW_SRC/myrepo.git" && git init --bare . > /dev/null 2>&1) + + "$SCRIPT_NAME" "$RAW_SRC/myrepo.git" "$HTML_DST" "$ASSETS_DIR" > /dev/null 2>&1 + + myrepo_html="$HTML_DST/myrepo" + if [ -d "$myrepo_html" ] && [ -f "$myrepo_html/log.html" ]; then + if [ -L "$myrepo_html/favicon.ico" ] && [ -L "$myrepo_html/style.css" ]; then + success "asset symlinks created in repo dir (single repo)" + else + error "asset symlinks missing or not symlinks in repo dir (single repo)" + fi + else + error "repo HTML dir missing or no log.html" + fi +} + +test_full_sync_assets() { + printf '=== Test 9: Full sync with assets ===\n' + + ASSETS_DIR="$TEST_BASE/assets-full" + setup_assets "$ASSETS_DIR" + + mkdir -p "$ASSETS_DIR" "$RAW_SRC/repo1.git" "$RAW_SRC/repo2.git" + + (cd "$RAW_SRC/repo1.git" && git init --bare . > /dev/null 2>&1) + (cd "$RAW_SRC/repo2.git" && git init --bare . > /dev/null 2>&1) + + "$SCRIPT_NAME" "$RAW_SRC" "$HTML_DST" "$ASSETS_DIR" > /dev/null 2>&1 + + check_file_exists "$HTML_DST/favicon.ico" "asset favicon.ico in root (full sync)" + check_file_exists "$HTML_DST/style.css" "asset style.css in root (full sync)" + + for repo in "$HTML_DST"/repo*; do + [ -d "$repo" ] || continue + if [ -L "$repo/favicon.ico" ] && [ -L "$repo/style.css" ]; then + success "Repo assets ok in $repo" + else + error "asset symlinks missing in repo dir $repo" + fi + done +} + +test_repo_titles_without_rewriting() { + printf '=== Test 10a: Titles do NOT contain " - mycompany" (without rewriting) ===\n' + + mkdir -p "$RAW_SRC/myrepo.git" + (cd "$RAW_SRC/myrepo.git" && git init --bare . > /dev/null 2>&1) + + html_dst_no_c="$HTML_DST/no-mycompany" + mkdir -p "$html_dst_no_c" + + "$SCRIPT_NAME" "$RAW_SRC/myrepo.git" "$html_dst_no_c" > /dev/null 2>&1 || error "$SCRIPT_NAME failed without -c" + + myrepo_html_no_c="$html_dst_no_c/myrepo" + if [ ! -d "$myrepo_html_no_c" ] || [ ! -f "$myrepo_html_no_c/log.html" ]; then + error "repo HTML dir or log.html missing (no -c)" + fi + + find "$myrepo_html_no_c" -name "*.html" -type f | while IFS= read -r f; do + title=$(extract_title "$f") + if printf '%s\n' "$title" | grep -q ' - mycompany'; then + error "title in $f (without -c) contains ' - mycompany': '$title'" + fi + done + + success "Without -c, titles do not contain ' - mycompany'" +} + +test_repo_titles_rewriting() { + printf '=== Test 10b: Titles end with " - mycompany" (with rewriting) ===\n' + + mkdir -p "$RAW_SRC/demo.git" + (cd "$RAW_SRC/demo.git" && git init --bare . > /dev/null 2>&1) + + config_file="$TEST_BASE/gitweb.conf" + printf 'SITE_NAME=mycompany\n' > "$config_file" + + html_dst_with_c="$HTML_DST/with-mycompany" + mkdir -p "$html_dst_with_c" + + #"$SCRIPT_NAME" -c "$config_file" "$RAW_SRC/demo.git" "$html_dst_with_c" > /dev/null 2>&1 || error "$SCRIPT_NAME failed with -c" + "$SCRIPT_NAME" -c "$config_file" "$RAW_SRC/demo.git" "$html_dst_with_c" || error "$SCRIPT_NAME failed with -c" + + myrepo_html_with_c="$html_dst_with_c/demo" + if [ ! -d "$myrepo_html_with_c" ] || [ ! -f "$myrepo_html_with_c/log.html" ]; then + error "repo HTML dir or log.html missing (with -c)" + fi + + find "$myrepo_html_with_c" -name "*.html" -type f | while IFS= read -r f; do + title=$(extract_title "$f") + if printf '%s\n' "$title" | grep -q ' - mycompany$'; then + : # good + else + error "title in $f (with -c) does not end with ' - mycompany': '$title'" + fi + done + + success "With -c, titles end with ' - mycompany'" +} + +test_index_rewriting() { + printf '=== Test 11: Index title and description rewriting ===\n' + + mkdir -p "$RAW_SRC/site1.git" + (cd "$RAW_SRC/site1.git" && git init --bare . > /dev/null 2>&1) + + config_file="$TEST_BASE/gitweb-index.conf" + printf 'INDEX_TITLE=CustomIndex\nINDEX_DESCRIPTION=CustomDesc\n' > "$config_file" + + html_dst_idx="$HTML_DST/with-index" + mkdir -p "$html_dst_idx" + + "$SCRIPT_NAME" -c "$config_file" "$RAW_SRC" "$html_dst_idx" \ + || error "$SCRIPT_NAME failed with -c for index test" + + index_file="$html_dst_idx/index.html" + [ -f "$index_file" ] || error "index.html not generated" + + title_line=$(grep -m1 '<title>' "$index_file" || true) + desc_line=$(grep -m1 '<span class=\"desc\">' "$index_file" || true) + + if printf '%s\n' "$title_line" | grep -q '<title>CustomIndex</title>' && printf '%s\n' "$desc_line" | grep -q '<span class="desc">CustomDesc</span>'; then + success "index.html correctly rewrote title and description" + else + error "index.html did not rewrite title/description properly" + fi +} + +main() { + printf 'Testing %s\n' "$SCRIPT_NAME" + check_script + check_environment + setup + + test_args + test_bad_html_dst + test_empty_src + test_single_repo + test_full_sync + test_stale_cleanup + test_assets_root_copy + test_assets_repo_symlinks + test_full_sync_assets + test_repo_titles_without_rewriting + test_repo_titles_rewriting + test_index_rewriting + + printf '\nALL TESTS PASSED!\n' + printf 'Test environment cleaned up.\n' +} + +[ "${0##*/}" = "test-gitpages.sh" ] && main "$@" diff --git a/test-mirror-git.sh b/test-mirror-git.sh @@ -0,0 +1,142 @@ +#!/bin/sh + +set -eu + +SCRIPT_NAME="./mirror-git.sh" + +check_script() { + if [ ! -x "$SCRIPT_NAME" ]; then + printf 'ERROR: %s not found or not executable\n' "$SCRIPT_NAME" >&2 + printf 'Please run:\n chmod +x git-sync.sh\n ./test-git-sync.sh\n' >&2 + exit 1 + fi +} + +TEST_BASE="" +cleanup() { + [ -n "$TEST_BASE" ] && rm -rf "$TEST_BASE" +} +trap cleanup EXIT INT TERM + +error() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +success() { + printf 'PASS: %s\n' "$1" +} + +setup() { + TEST_BASE=$(mktemp -d) || error "Failed to create temp dir" + SRC_DIR="$TEST_BASE/src" + DST_DIR="$TEST_BASE/dst" + mkdir -p "$SRC_DIR" "$DST_DIR" +} + +assert_fails() { + test_num="$1" + test_desc="$2" + shift 2 + printf '=== Test %s: %s ===\n' "$test_num" "$test_desc" + "$@" && error "Should fail" || success "Rejects $test_desc" +} + +assert_repo_copied() { + repo="$1" + test_name="$2" + [ -d "$repo" ] && success "$test_name" || error "$test_name" +} + +assert_repo_skipped() { + repo="$1" + test_name="$2" + [ ! -d "$repo" ] && success "$test_name" || error "$test_name" +} + +assert_bare_repo() { + repo="$1" + test_name="$2" + git --git-dir="$repo" rev-parse --is-bare-repository >/dev/null 2>&1 || error "$test_name" +} + +assert_server_info() { + repo="$1" + test_name="$2" + [ -f "$repo/info/refs" ] && success "$test_name" || error "$test_name" +} + +test_args() { + assert_fails 1 "Wrong number of arguments" "$SCRIPT_NAME" "$SRC_DIR" "$DST_DIR" extra_arg +} + +test_bad_src_file() { + bad_src="$TEST_BASE/bad_src" + touch "$bad_src" + assert_fails 2 "Source is file" "$SCRIPT_NAME" "$bad_src" "$DST_DIR" +} + +test_bad_dst() { + bad_dst="$TEST_BASE/bad_dst" + touch "$bad_dst" && chmod 444 "$bad_dst" + assert_fails 3 "Destination not writable" "$SCRIPT_NAME" "$SRC_DIR" "$bad_dst" +} + +test_empty_src() { + rm -rf "$SRC_DIR"/* + assert_fails 4 "Empty source directory" "$SCRIPT_NAME" "$SRC_DIR" "$DST_DIR" +} + +test_full_sync() { + printf '=== Test 5: Full sync ===\n' + mkdir -p "$SRC_DIR/myrepo.git" "$SRC_DIR/otherrepo" + (cd "$SRC_DIR/myrepo.git" && git init --bare . >/dev/null 2>&1) + (cd "$SRC_DIR/otherrepo" && git init >/dev/null 2>&1) + + "$SCRIPT_NAME" "$SRC_DIR" "$DST_DIR" + + assert_repo_copied "$DST_DIR/myrepo.git" "Bare repo copied" + assert_repo_skipped "$DST_DIR/otherrepo" "Skipped non-bare" + assert_bare_repo "$DST_DIR/myrepo.git" "Destination is bare" + assert_server_info "$DST_DIR/myrepo.git" "update-server-info ran" +} + +test_single_repo() { + printf '=== Test 6: Single repo ===\n' + mkdir -p "$SRC_DIR/myrepo.git" + (cd "$SRC_DIR/myrepo.git" && git init --bare . >/dev/null 2>&1) + + "$SCRIPT_NAME" "$SRC_DIR/myrepo.git" "$DST_DIR" + + assert_repo_copied "$DST_DIR/myrepo.git" "Single repo copied" + assert_bare_repo "$DST_DIR/myrepo.git" "Single mode: bare repo" + assert_server_info "$DST_DIR/myrepo.git" "Single mode: update-server-info ran" +} + +test_root_protection() { + assert_fails 7 "Root protection" "$SCRIPT_NAME" "$SRC_DIR" "/" +} + +test_non_repo_dir() { + assert_fails 8 "Non-repo directory" "$SCRIPT_NAME" "$TEST_BASE" "$DST_DIR" +} + +main() { + printf 'Testing %s\n' "$SCRIPT_NAME" + check_script + setup + + test_args + test_bad_src_file + test_bad_dst + test_empty_src + test_full_sync + test_single_repo + test_root_protection + test_non_repo_dir + + printf '\nALL TESTS PASSED!\n' + printf 'Test environment cleaned up.\n' +} + +[ "${0##*/}" = "test-mirror-git.sh" ] && main "$@"