When I moved my development workflow into my own private Gitea instance, one of the first few things I set up was repository mirroring. Gitea acts as my main hub, handling CI/CD pipelines with multiple git runners, a container registry for Kubernetes, package management, and more. I also sync my repositories with GitHub, which works great for public projects and gives me a reliable backup for everything else.
One of the challenges with Gitea repository mirroring is handling expired access tokens and keeping credentials rotated across dozens of repositories.
Managing Many Repository Mirrors Can Be a Chore
Gitea stores mirror credentials inside each repository's config
file.
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "remote_mirror_fIARmGEuNg"]
url = https://{username}:{access_token}@github.com/someuser/some-remote-repo.git
mirror = true
push = +refs/heads/*:refs/heads/*
push = +refs/tags/*:refs/tags/*
Unfortunately:
- These credentials aren't managed centrally in the database.
- There's no bulk βupdateβ option in the UI.
- When a token expires, you have to manually update the mirror settings in each repository.
Best practices recommend rotating access tokens every 90 to 180 days. Maybe even less depending on your security posture. If you have 60+ repositories, it quickly becomes a chore to figure out which repos use which token and then manually re-create each mirror with the updated token.
It would be cool if you could create these as secrets, like how Actions handles secrets across repositories. Updates could then be made in one place and automatically applied everywhere. While that's not currently possible, we can script a quick solution to update all repositories at once. Down the road, this could turn into another fun project to tackle.π
A Simple Script to the Rescue
To make life easier, I wrote a small Bash script to streamline this process for myself. Over time, I evolved it into a more robust tool and now I'm sharing it with you. Essentially, it:
- Lists all repos with mirrors, showing the remote URL, username, and access token.
- Optionally replaces tokens in one go if you pass in
--old-token
and--new-token
.
This works for any mirror remote in Gitea, whether you're pushing to GitHub, GitLab, Bitbucket, or something elseβas long as the remote URL contains a match.
manage-mirrors.sh
#!/bin/bash
BASE_DIR="$(pwd)/gitea/git/repositories"
print_usage() {
echo "Usage: $0 [--old-token OLD --new-token NEW]"
echo
echo "Without arguments:"
echo " Lists all repos with remote mirror configs."
echo
echo "With both arguments:"
echo " Replaces OLD token with NEW across all repos."
echo
echo "Options:"
echo " --old-token OLD The token to replace"
echo " --new-token NEW The new token to use"
echo " --help, -h Show this help message"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--old-token)
if [[ -z "$2" || "$2" == --* ]]; then
echo "β Error: --old-token requires a value"
exit 1
fi
OLD_TOKEN="$2"
shift 2
;;
--new-token)
if [[ -z "$2" || "$2" == --* ]]; then
echo "β Error: --new-token requires a value"
exit 1
fi
NEW_TOKEN="$2"
shift 2
;;
--help|-h)
print_usage
exit 0
;;
*)
echo "β Unknown argument: $1"
echo
print_usage
exit 1
;;
esac
done
# If only one of OLD/NEW is set, that's an error
if { [[ -n "$OLD_TOKEN" ]] && [[ -z "$NEW_TOKEN" ]]; } || \
{ [[ -z "$OLD_TOKEN" ]] && [[ -n "$NEW_TOKEN" ]]; }; then
echo "β Error: both --old-token and --new-token must be provided together"
echo
print_usage
exit 1
fi
# If both are set, replacement mode
if [[ -n "$OLD_TOKEN" && -n "$NEW_TOKEN" ]]; then
echo "π Replacing tokens in repo configs..."
find "$BASE_DIR" -type d -name "*.git" | while read -r repo; do
config_file="$repo/config"
if grep -q "$OLD_TOKEN" "$config_file"; then
sed -i "s/$OLD_TOKEN/$NEW_TOKEN/g" "$config_file"
echo "β
Updated token in $(basename "$repo")"
fi
done
exit 0
fi
# Default: listing mode
printf "%-30s %-60s %-20s %-20s\n" "Repo" "Remote Repo" "Username" "Token"
printf "%-30s %-60s %-20s %-20s\n" "----" "------------" "--------" "-----"
find "$BASE_DIR" -type d -name "*.git" | while read -r repo; do
remote_urls=$(git --git-dir="$repo" config --get-regexp '^remote\.remote_mirror_.*\.url$' 2>/dev/null | awk '{print $2}')
for url in $remote_urls; do
user=$(echo "$url" | sed -n 's#.*://\([^:@]*\):.*@.*#\1#p')
token=$(echo "$url" | sed -n 's#.*://[^:@]*:\([^@]*\)@.*#\1#p')
masked_url=$(echo "$url" | sed 's#://[^@]*@#://#')
printf "%-30s %-60s %-20s %-20s\n" "$(basename "$repo")" "$masked_url" "$user" "$token"
done
done
BASE_DIR
variable in the script to point to your repository location.
Examples
List all repos with mirrors:
root@github:/gitea# ./manage-mirrors.sh
Repo Remote Repo Username Token
---- ------------ -------- -----
project-alpha.git https://github.com/alice/project-alpha.git alice abc123token
hms-service.git https://gitlab.com/devteam/hms-service.git devteam xyz789token
python-utils.git https://github.com/bob/python-utils.git bob pyu456token
message-queue.git https://bitbucket.org/ops-team/message-queue.git ops-team mq987token
dashboard-v2.git https://github.com/alice/dashboard-v2.git alice abc123token
analytics-api.git https://gitlab.com/devteam/analytics-api.git devteam xyz789token
twitch-bot.git https://github.com/botdev/twitch-bot.git botdev bot654token
geoip-db.git https://github.com/bob/geoip-db.git bob geo321token
network-configs.git https://gitlab.com/network/network-configs.git networkteam net111token
website-main.git https://github.com/alice/website-main.git alice abc123token
nextcloud-deploy.git https://gitlab.com/devteam/nextcloud-deploy.git devteam xyz789token
sapphire-dashboard.git https://github.com/bob/sapphire-dashboard.git bob sap999token
discord-bot.git https://bitbucket.org/ops-team/discord-bot.git ops-team dsf555token
linktree-clone.git https://github.com/alice/linktree-clone.git alice abc123token
k8s-deploy.git https://gitlab.com/devteam/k8s-deploy.git devteam xyz789token
sync-tools.git https://github.com/bob/sync-tools.git bob syn888token
nginx-setup.git https://github.com/alice/nginx-setup.git alice abc123token
Replace tokens (GitHub, GitLab, or any remote that uses HTTP URL userinfo):
root@github:/gitea# ./manage-mirrors.sh --old-token github_pat_11AGF --new-token github_pat_ABC123
π Replacing tokens in repo configs...
β
Updated token in project-alpha.git
β
Updated token in hms-service.git
β
Updated token in python-utils.git
β
Updated token in message-queue.git
β
Updated token in dashboard-v2.git
β
Updated token in analytics-api.git
β
Updated token in twitch-bot.git
β
Updated token in geoip-db.git
β
Updated token in network-configs.git
β
Updated token in website-main.git
β
Updated token in sapphire-dashboard.git
β
Updated token in discord-bot.git
β
Updated token in linktree-clone.git
β
Updated token in k8s-deploy.git
β
Updated token in sync-tools.git
β
Updated token in nginx-setup.git
If you only ever mirror a handful of repos, the built-in Gitea UI works fine. But if you manage lots of mirrors across GitHub, GitLab, or other services, this little script saves a ton of clicking and frustration, keeping your workflow smooth until Gitea eventually offers centralized secret management.