From a92476734e8e1a4a61e622e83e89f43b2c63ea02 Mon Sep 17 00:00:00 2001 From: James Coleman Date: Wed, 22 Jul 2026 10:57:26 -0500 Subject: [PATCH] Add git update-server-info for bare repos and hide_remote option Bare mirrors served over dumb HTTP need info/refs and objects/info/packs kept current, so run git update-server-info after every successful bare sync and clone. The post-update hook cannot be used since it only fires on push, not on the fetch a mirror performs. Add a hide_remote option (requires bare + source) that keeps the upstream URL out of the served files: it removes the stored remote from config, deletes FETCH_HEAD after each fetch, and fetches directly from source with a +refs/*:refs/* mirror refspec on every run. --- README.md | 11 +++++++++-- mirror-sync.sh | 53 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 97350ec..8b2a31c 100644 --- a/README.md +++ b/README.md @@ -117,14 +117,19 @@ If the destination (`repo`) does not yet contain a git repository, it is cloned Whether a repository is bare can be set explicitly with the `bare` configuration, otherwise existing repositories are auto-detected. +Bare repositories are usually served to clients over "dumb" HTTP (a plain file server). After each successful sync (and after the initial clone) the script runs `git update-server-info` so the `info/refs` and `objects/info/packs` files clients need stay up to date. This is run every time rather than detected, as it is cheap; the packaged `post-update` hook cannot be relied on because it only fires on push, never on the fetch a mirror performs. + #### source The git URL to clone the repository from. Required when the destination does not already contain a git repository. #### bare -Set to `true` to treat the repository as bare. When cloning, this clones with `git clone --mirror`. When updating, this forces use of `git remote update --prune`. If unset, existing repositories are auto-detected. +Set to `true` to treat the repository as bare. When cloning, this clones with `git clone --mirror`. When updating, this forces use of `git remote update --prune` (or a direct fetch when `hide_remote` is set). If unset, existing repositories are auto-detected. + +#### hide_remote +Set to `true` (requires `bare` and `source`) to keep the upstream URL out of the served repository so clients cannot probe the dumb-HTTP files to discover where the mirror pulls from. When enabled, the stored remote is removed from the repository's `config` and the `FETCH_HEAD` file (which records the URL on every fetch) is deleted after each sync. Because there is no stored remote to update, the sync fetches directly from `source` with a mirror refspec (`+refs/*:refs/*`) on each run. #### options -Extra options appended to the git command (`git clone` when cloning, `git pull` when updating a working-tree repository, or `git remote update --prune` when updating a bare repository). +Extra options appended to the git command (`git clone` when cloning, `git pull` when updating a working-tree repository, or `git remote update --prune` / `git fetch` when updating a bare repository). #### Example ```bash @@ -141,6 +146,8 @@ example_source="https://github.com/example/example.git" example_repo="/home/mirror/git/example.git" example_bare="true" example_timestamp="/home/mirror/timestamp/example" +# Optional: keep the upstream URL out of the served files. +example_hide_remote="true" ``` ### aws diff --git a/mirror-sync.sh b/mirror-sync.sh index 2bbc8b1..754f49d 100644 --- a/mirror-sync.sh +++ b/mirror-sync.sh @@ -5,7 +5,7 @@ PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:$HOME/.local/ # Variables for trace generation. PROGRAM="mirror-sync" -VERSION="20260721" +VERSION="20260722" TRACEHOST=$(hostname -f) mirror_hostname=$(hostname -f) DATE_STARTED=$(LC_ALL=POSIX LANG=POSIX date -u -R) @@ -646,6 +646,29 @@ module_config() { log_start_header } +# Maintenance to run after a successful sync (or clone) of a bare repository. +# Bare repositories are typically served to clients over "dumb" HTTP (a plain +# file server), which requires the info files kept up to date and, optionally, +# the upstream URL scrubbed so it cannot be read from the served files. +git_bare_post_sync() { + # When hiding the remote, strip anything that records the upstream URL so it + # cannot be probed over dumb HTTP. `config` holds it under [remote ...] and + # `git fetch` writes it into FETCH_HEAD on every run. + if [[ $hide_remote ]]; then + local _remote + for _remote in $(git -C "${repo:?}" remote 2>/dev/null); do + git -C "${repo:?}" remote remove "$_remote" + done + rm -f "${repo:?}/FETCH_HEAD" + fi + + # Regenerate the dumb-HTTP info files (info/refs and objects/info/packs). + # A mirror fetch never fires the post-update hook (that only runs on push), + # so this has to run explicitly after each sync. It is cheap, so rather than + # try to detect whether it is needed we simply run it every time. + git -C "${repo:?}" update-server-info +} + # Sync git based mirrors. git_sync() { # Start the module. @@ -654,6 +677,7 @@ git_sync() { # Read git specific configuration. eval source="\$${MODULE}_source" eval bare="\$${MODULE}_bare" + eval hide_remote="\$${MODULE}_hide_remote" # Normalize the bare flag to either "true" or empty. case "${bare,,}" in @@ -661,6 +685,18 @@ git_sync() { *) bare="" ;; esac + # Normalize the hide_remote flag to either "true" or empty. Hiding the remote + # requires a configured source, since without a stored remote the URL to + # fetch from must come from the configuration on every sync. + case "${hide_remote,,}" in + 1|true|yes) hide_remote="true" ;; + *) hide_remote="" ;; + esac + if [[ $hide_remote ]] && [[ ! $source ]]; then + echo "hide_remote requires a source to be configured." + exit 1 + fi + # If the destination does not yet contain a git repository, clone it from # the configured source. Bare repositories are cloned with --mirror so the # configured fetch refspec mirrors all refs from upstream. @@ -677,6 +713,7 @@ git_sync() { fi RT=$? if (( RT == 0 )); then + [[ $bare ]] && git_bare_post_sync post_successful_sync else post_failed_sync @@ -693,18 +730,26 @@ git_sync() { # Determine whether the repository is bare. Honor an explicit configuration # if set, otherwise auto-detect. Bare repositories have no working tree, so a - # `git pull` is not possible; instead use `git remote update` which honors the - # configured fetch refspec (e.g. a mirror clone created with --mirror). + # `git pull` is not possible; instead update by fetching the configured + # refspec. if [[ ! $bare ]] && [[ $(git rev-parse --is-bare-repository 2>/dev/null) == "true" ]]; then bare="true" fi if [[ $bare ]]; then - eval git remote update --prune ${options:+$options} + # When hiding the remote there is no stored remote to update, so fetch + # directly from the source with a mirror refspec. Otherwise honor the + # remote's configured fetch refspec (e.g. from a --mirror clone). + if [[ $hide_remote ]]; then + eval git fetch --prune ${options:+$options} "'${source}'" "'+refs/*:refs/*'" + else + eval git remote update --prune ${options:+$options} + fi else eval git pull ${options:+$options} fi RT=$? if (( RT == 0 )); then + [[ $bare ]] && git_bare_post_sync post_successful_sync else post_failed_sync