While there are many benefits to self-hosting git repositories, there are a number of reasons to mirror them to GitHub: Redundancy, discoverability, integration with other users, etc. It’s actually very easy to set up automatic GitHub repository creation and mirroring using the GitHub CLI and simple git hooks. The process is explained below.

Generating a GitHub Personal Access Token

The first step is to go to “Developer Settings” in settings, then generate a fine-grained personal access token with read/write access to “Administration”. This token will be used to create and update repositories.

Creating a Post-Update Hook

Once the token is generated and the GitHub CLI (gh) is installed, create a post-update hook like the one shown below which creates a repository on GitHub if it doesn’t exist, updates its visibility and description based on files in the local repository, then pushes updates to the repository.

#!/bin/sh

set -eu

# Exit with an error if GIT_DIR isn't set
: ${GIT_DIR:?Expected GIT_DIR to be defined.}

# Load GitHub PAT token for gh
GH_TOKEN="$(cat ~/gh_token)"
export GH_TOKEN

# Get user & repository information
# for git push; gh defaults to the authenticated user
user="$(gh api user --jq .login)"
name="$(basename "${GIT_DIR%.git}")"
path="$user/$name"

# Set to public if git-daemon-export-ok exists
visibility=private
test -f "$GIT_DIR"/git-daemon-export-ok &&
	visibility=public

# Create/update repository on GitHub
echo gh repo create "$path" --"$visibility" ||
    true
echo gh repo edit "$path" \
    --visibility "$visibility" \
    -d "$(cat "$GIT_DIR"/description)" \
    --accept-visibility-change-consequences

# Push updates
git push --mirror git@github.com:"$path"

To use the hook, either put it in hooks/post-update under $GIT_DIR (the root of bare repositories) or place it in a global hooks directory and configure a global hooks directory using git’s core.hooksPath option. The latter method is often preferable because it’s automatically used by new repositories and can be updated in a central location. Once the hook is set up, whenever updates are pushed to the repository they’ll automatically be mirrored to GitHub along with their metadata.

Update Consolidation and the gitsrv Project

While not necessary, to prevent excessive updates I created conque, a simple script which stores only unique arguments in a queue for later execution. In addition, I maintain gitsrv (GitHub)—a set of utilities for serving git repositories, including github-mirror for mirroring to GitHub repositories.

In the following example, cron(8) is used to call gitsrv on the consolidated queue every minute, and the hook—located in ~/hooks, the globally configured hooks directory— adds directories to the queue instead of directly updating them.

$ cat ~/hooks/post-update
#!/bin/sh

conque ~/gitsrv-q add "$(git rev-parse --absolute-git-dir)"
$ crontab -l
* * * * *  conque ~/gitsrv-q run gitsrv