grall, my small git helper for multiple remotes
I’m currently on paternity leave, have recently moved into a new house, and have been reading too many articles and watching too many videos in the style of “level up your command line”. This has resulted in me ditching bash in favor of zsh. I’ve used bash since I first started out on RedHat 6.2—or was it even before then? In any case, that’s more than 26 years!
Another project I’ve started is to self-host more things. Among other things, I’ve discovered ntfy.sh, tried and discarded Dozzle and Beszel (both great, but didn’t fit my needs), and set up Forgejo for hosting my code repos alongside GitHub. However, there’s now an inconvenience: keeping the different repositories in sync.
The inconvenience
I have two remotes, and have them added as follows:
origin(https://github.com/robertjacobsen/.dotfiles.git)forgejo(https://git.pilze.no/robert/.dotfiles.git)
The typical workflow for keeping them in sync is to:
git remote add forgejo https://git.pilze.no/robert/.dotfiles.git
git push # the same as “git push origin main” with default upstream
git push forgejo mainIt’s also worth mentioning that you now have to do git remote update to pull all remotes, instead of merely git fetch or git pull, which would typically pull only one of them.
I am fond of shorthands for my commands. To reload zsh, I have zr (aliased to exec zsh). The git remote update command above: gru. Doing a git merge --ff-only: gff. Doing a git remote update and then a git merge --ff-only: gruff. I have all my aliases documented (some more used than others, I really need to clean that up).
The solution
Git has a way of adding multiple targets to be pushed with one git push. This allows you to add other targets to remotes (such as origin), e.g., git remote set-url --add --push origin https://git.pilze.no/robert/.dotfiles.git. However, that means that I now have a bloated origin that points to multiple things. I don’t like it.
Instead, I added a new remote called all which does this:
git remote add all https://github.com/robertjacobsen/.dotfiles.git
git remote set-url --add --push all https://github.com/robertjacobsen/.dotfiles.git
git remote set-url --add --push all https://git.pilze.no/robert/.dotfiles.gitNow that’s a mouthful to remember. Building on top of the shorthands, I added the function grall (from “git remote add all […]”). This allows me to add this with ease. I didn’t want to remember the long names, and I assume that the repository name is the same across different remotes.
I set this in my ~/.zshrc.local file (which is sourced from my ~/.zshrc):
export GITHUB_USER=https://github.com/robertjacobsen
export FORGEJO_USER=https://git.pilze.no/robertAnd then have this script added to something that .zshrc sources. I have this as part of an aliases.zsh file:
grall() {
local repo="${1:-$(basename "$PWD")}"
repo="${repo%.git}"
if [ -z "$GITHUB_USER" ] || [ -z "$FORGEJO_USER" ]; then
echo "GITHUB_USER and FORGEJO_USER must be set (in ~/.zshrc.local)" >&2
return 1
fi
if git remote get-url all >/dev/null 2>&1; then
echo "remote 'all' already exists; remove it first if you want to recreate" >&2
return 1
fi
local github_url="$GITHUB_USER/$repo.git"
local forgejo_url="$FORGEJO_USER/$repo.git"
git remote add all "$github_url"
git remote set-url --add --push all "$github_url"
git remote set-url --add --push all "$forgejo_url"
}After zsh is reloaded (or we’ve run zr), this can then be used like this:
$ grall .dotfiles.gitgrall then adds the remotes to all, which can be verified through git remote -v:
$ git remote -v
all https://github.com/robertjacobsen/.dotfiles.git (fetch)
all https://github.com/robertjacobsen/.dotfiles.git (push)
all https://git.pilze.no/robert/.dotfiles.git (push)
forgejo https://git.pilze.no/robert/.dotfiles.git (fetch)
forgejo https://git.pilze.no/robert/.dotfiles.git (push)
origin https://github.com/robertjacobsen/.dotfiles.git (fetch)
origin https://github.com/robertjacobsen/.dotfiles.git (push)The last thing we need to do is to set all/main as upstream:
$ git push -u all mainThis allows subsequent git pushes to primarily use the all remote for main instead of origin/main, which in turn means that both origin and forgejo will be updated. Neat!