forgejo-mirror.sh
edited
1#!/usr/bin/env bash
2
3# This script mirrors repositories from my private Forgejo instance to any
4# external Git hosts (in this case, Tangled.)
5#
6# 1. Set `FORGEJO_DATA_DIR` to your Forgejo's repositories directory,
7# `STATE_FILE` is how this script takes note of changes.
8#
9# 2. Create a repository (e.g. mary/mirror-config, configurable via
10# `CONFIG_REPO_PATH`) and write a `repos.txt` file containing the
11# repositories you want to mirror:
12#
13# mary/pkg-protobuf=git@tangled.sh:mary.my.id/pkg-protobuf
14# # comments and blank lines are ignored
15#
16# 3. Generate a new SSH key, or use your existing one, and set `GIT_SSH_COMMAND`
17# to make use of it.
18#
19# 4. Run this script as a cron job.
20
21set -euo pipefail
22
23GIT_SSH_COMMAND="ssh -i ~/.ssh/forgejo_mirror_bot -o StrictHostKeyChecking=no"
24export GIT_SSH_COMMAND
25
26STATE_FILE="$HOME/.forgejo-mirror-state"
27FORGEJO_DATA_DIR="$HOME/dockers/data/forgejo/git/repositories"
28CONFIG_REPO_PATH="$FORGEJO_DATA_DIR/mary/mirror-config.git"
29
30# Read repo mapping from config
31cd "$CONFIG_REPO_PATH"
32REPOS=$(git show HEAD:config.txt)
33
34# Load previous state
35declare -A last_hashes
36if [[ -f "$STATE_FILE" ]]; then
37 while IFS='=' read -r repo hash; do
38 last_hashes["$repo"]="$hash"
39 done < "$STATE_FILE"
40fi
41
42# Prepare new state and track if anything changed
43declare -A new_hashes
44changes_made=false
45
46# Process each repo mapping
47while IFS= read -r line; do
48 # Skip empty lines and comments
49 [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
50
51 # Parse source=target format
52 if [[ "$line" =~ ^([^=]+)=(.+)$ ]]; then
53 source_repo="${BASH_REMATCH[1]}"
54 target_url="${BASH_REMATCH[2]}"
55
56 repo_path="$FORGEJO_DATA_DIR/$source_repo.git"
57 if [ -d "$repo_path" ]; then
58 cd "$repo_path"
59 current_hash=$(git show-ref | sha256sum | cut -d' ' -f1)
60 new_hashes["$source_repo"]="$current_hash"
61
62 if [[ "${last_hashes[$source_repo]:-}" != "$current_hash" ]]; then
63 echo "[-] Mirroring $source_repo -> $target_url (changed)"
64 git push --mirror "$target_url" || {
65 echo "Failed to mirror $source_repo"
66 continue
67 }
68 changes_made=true
69 else
70 echo "[-] Skipping $source_repo (no changes)"
71 fi
72 else
73 echo "[-] Repository $source_repo not found at $repo_path"
74 fi
75 else
76 echo "[-] Invalid format: $line (expected source=target)"
77 fi
78done <<< "$REPOS"
79
80# Only save new state if there were changes
81if [[ "$changes_made" == true ]]; then
82 > "$STATE_FILE"
83 for repo in "${!new_hashes[@]}"; do
84 echo "$repo=${new_hashes[$repo]}" >> "$STATE_FILE"
85 done
86fi