Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/ops/git/git-mirror.sh
blob: a36f978af46d9af68c71051bda63bde6e68a19cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/bin/sh
# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

# Mirrors the repositories of /etc/git-mirror/repositories from https://github.com/Progmasoft into
# /srv/git/mirrors/<name>.git: branches and tags only, never the pull-request refs of GitHub, with
# what GitHub removed removed here too. Runs as gitmirror from git-mirror.service; installed as
# /usr/local/libexec/git-mirror.
set -eu

list=${GIT_MIRROR_LIST:-/etc/git-mirror/repositories}
root=${GIT_MIRROR_ROOT:-/srv/git/mirrors}
status=0

# A for loop, not a pipe: a pipe would run the loop in a subshell and lose the status.
names=$(grep -Ev '^[[:space:]]*(#|$)' "$list")
for name in $names; do
  case "$name" in
    *[!A-Za-z0-9._-]* | .* ) echo "git-mirror: skipping the name '$name'" >&2; status=1; continue ;;
  esac
  repository="$root/$name.git"
  if [ ! -d "$repository" ]; then
    git init --quiet --bare "$repository.new"
    git -C "$repository.new" config remote.origin.url "https://github.com/Progmasoft/$name.git"
    git -C "$repository.new" config remote.origin.fetch '+refs/heads/*:refs/heads/*'
    git -C "$repository.new" config --add remote.origin.fetch '+refs/tags/*:refs/tags/*'
    git -C "$repository.new" config remote.origin.tagOpt --no-tags
    git -C "$repository.new" config http.receivepack false
    mv "$repository.new" "$repository"
  fi
  # A clone is served from one pack with a bitmap, so it starts sending at once.
  git -C "$repository" config repack.writeBitmaps true
  git -C "$repository" config pack.writeBitmapHashCache true
  git -C "$repository" config gc.auto 0
  mkdir -p "$repository/info/web"
  before=$(git -C "$repository" for-each-ref --format="%(objectname) %(refname)" | sha256sum)
  if git -C "$repository" fetch --quiet --prune origin; then
    after=$(git -C "$repository" for-each-ref --format="%(objectname) %(refname)" | sha256sum)
    if [ "$before" != "$after" ] || [ ! -f "$repository/info/web/repacked" ]; then
      git -C "$repository" repack -a -d -b -q
      touch "$repository/info/web/repacked"
    fi
    # HEAD follows the default branch of GitHub; a failed lookup fails the run.
    if advertised=$(git -C "$repository" ls-remote --symref origin HEAD); then
      head=$(printf '%s\n' "$advertised" | sed -n 's|^ref: refs/heads/\([^[:space:]]*\)[[:space:]]*HEAD$|\1|p')
      [ -n "$head" ] && git -C "$repository" symbolic-ref HEAD "refs/heads/$head"
    else
      echo "git-mirror: the default branch of $name could not be read" >&2
      status=1
    fi
    # git-http-backend serves only repositories with this file.
    touch "$repository/git-daemon-export-ok"
    # cgit shows the age of the last change of a repository from this file.
    git -C "$repository" for-each-ref --sort=-committerdate --count=1 \
      --format='%(committerdate:iso8601)' refs/heads > "$repository/info/web/last-modified"
  else
    echo "git-mirror: fetching $name failed" >&2
    status=1
  fi
done
# A repository no longer listed stops being served at once and is removed.
for repository in "$root"/*.git; do
  [ -d "$repository" ] || continue
  name=$(basename "$repository" .git)
  case " $(printf '%s ' $names)" in
    *" $name "*) ;;
    *)
      rm -f "$repository/git-daemon-export-ok"
      rm -rf -- "$repository"
      echo "git-mirror: removed $name, which is no longer listed" >&2
      ;;
  esac
done
exit "$status"