Kyle Buzby

Setting up GitHub as Mirror

28 December, 2020

Background

A common interest of mine is to own as much of my own data as possible, which largely means hosting a lot of the services that we all know and love. This one is about git, or simply put, source control for my personal projects. Source code is something I have less of any issue being in the public domain or on someone else's server (definitely not against open source), but over the years I've seen my GitHub profile become inundated with these small side project repos that are never touched after a few commits. So in order to keep that a little more groomed I've maintained my own git repo on my local Synology NAS device.

All that being said there is only so much you can do with git on a local NAS, and if you want to do more like CI/CD you'll be rolling your own or setting up a bunch more services. One feature I wanted was automated deploys of my website - I had some simple bash scripts running for a while and even involved a second server, but it was more than I wanted to maintain and became clunky after I started letting the second server sleep. What I really wanted was something like GitHub actions

Enter mirroring.

Setting up GitHub to mirror

The first step to setting up a GitHub mirror is to create your repo normally, but make sure to leave it empty / bare, otherwise you'll have to be a little --forceful to make things work.

A simple option would be to set a second remote in your working copy in order to commit there as well, but that would involve remembering to set it up each time you (or someone else) checks out the repo as well as intentionally add commits there. This is a sure-fire way to make sure the mirror gets out of sync.

So, we'll want to have the private remote repo commit directly to the mirror. On the private remote repo add the GitHub repo as a remote using the same command as you would in a working copy where github-mirror is the name I chose to represent that remote URL.

git add remote github-mirror <github git URL>

Mirroring your commits

Traditionally mirroring is a full clone of a repository - including all branches, tags, etc. This is done with the --mirror option when performing a clone / push. This flag creates / updates a cloned repository to be a "mirror" of the remote as opposed to a standard "working copy". GitHub has some official mirrors, but there is no built in support to handle this for you.

If you want GitHub to be a full mirror then you can simply use the --mirror flag in a push command such as

git push --mirror github-mirror

This will push all branches from your remote repo to the mirror, so if you're like me and want to restrict this to only the main branch then you can push specific refs.

The command to push commits from only one branch on a remote to another branch on a second remote is:

git push github-mirror main:refs/heads/main

Dissecting this command we have the first two pieces, git push, which is the standard command for pushing commits to a remote.

The third piece is github-mirror which is the name of the remote created above. This can also directly be a URL if you do not have a remote set up.

The fourth piece, main:refs/heads/main, is actually two in one, separated by the colon. The first half is the name of the local branch we are pushing commits from - or in git terms, the "source". The second piece is the destination, if it's omitted then git will try to use a branch of the same name. If one doesn't exist then it will be created. Specifying the refs/* starting piece means that we are specifically trying to send a specific type of object to a specific location in the remote, expanding it to refs/heads/* means that we want to only send commit objects, which is applicable here since this is simple a mirror to take advantage of GitHub actions. Finally the full refs/heads/main is specifying the precise location to update from our local main.

If you specify a destination, but not its namespace and git will try to to find the best location - most likely under refs/heads, but specificity is probably good in this case since we're moving in an direction where we automatically mirror.

Note: this will only push commits from a single branch - if you want tags as well, then you should use the --tags option to push all tags from your local tags to the remote.

As with most git commands there are a number of other flags / options / flavors of arguments that you can pass to git push. If this doesn't cover what you need exactly - then definitely check out the documentation

Automating commits to mirror

Now, that we have a command to run, we can automate updating the mirror when pushing to the private repo.

The first step will be to make sure you have SSH keys set up for GitHub repo from your remote server so you can push without interactive authentication.

Second we'll make use of server-side git hooks in order to execute this when a new commit is received - specifically the post-receive-hook, which will run once the commits have been successfully received. Without going into too much detail, a hook is essentially a named script that is placed in the .git/hooks directory of your repo and executed at the defined time. There are examples of all these hooks in git repos - removing the .example suffix will allow git to execute them.

Now we're ready to create a shell script to run and execute our push to the github-mirror. For my purposes, since I'm only pushing one branch - I only want to push to the mirror when commits on that same branch are received. This done with the arguments that are passed to the script when executed by git. In order these arguments are oldRevision, newRevision, and ref.

So, with that knowledge and the example script already provided we can make a post-receive script that looks like this:

#!/bin/sh

while read oldrev newrev ref
do
if [[ $ref =~ .*/main$ ]]; # when committing to main branch
    then
        echo "Main ref received. Mirroring to GitHub"
        git push github-mirror main:refs/heads/main
    else
        echo "Ref $ref successfully received. Doing nothing: only the main branch may be deployed on this server."
fi
done

Note: it's a good idea to echo a response - the output of this script and anything it runs will be output to your local terminal when pushing to the private repo and will let you know the hook is running.

Saving that file and making sure it's named post-receive is then all you need to do to have this start running on every commit to your private repo!


Reach out if you have any questions, comments, thoughts!

This website uses cookies to ensure you get the best experience.