The idea
I have a home server that I want to treat as a Git remote, but I don't want to have to log in somewhere and create repositories whenever I want to run a small experiment. Lets set it up so that any repository that doesn't already exist is automatically initialized.
At first I looked around for self-hosted Git... hubs? Applications like GitLab and Gitea that come with a lot of bells and whistles that are generally aimed at collaborative setups and very overkill for my usecase. While researching and comparing, I ran into a comment that made me audibly facepalm;
Why not use
git
overssh
?
Git is so simple it hurts sometimes.
It's not uncommon to be a little scared of Git, thinking it's enshrouded in CLI mystery and can only safely be channeled through a catalyst with a UI. If you dig into the deeper mechanics of how objects are stored this notion may very well be true in some aspects, but today we're focusing on the simplicity that makes it beautiful and the principles that makes it incredibly powerful.
The solution
The solution we'll set up in this article is based on a two conclusions:
- A git remote does not have to be remote. You can clone and push to a repository on your local machine:
git clone /projects/my-original-repo
. Thus,git
contains everything needed to act as a remote repository. - SSH is just a means to invoke
git
. By default, when you rungit clone [email protected]:Username/RepoName.git
you are logging in as thegit
user ongithub.com
via SSH.
Creating the git user
- Create a
git
user that clients will be able tossh
into.sudo useradd -m git mkdir -p /home/git/.ssh touch /home/git/.ssh/authorized_keys
- Configure which shell is used for the
git
user. For the default Git experience you would usegit-shell
, but because we want to write a custom script to override what happens during an incoming push we'll runbash
:sudo chsh -s /bin/bash git
- Add any public keys you're going to use as a line in
/home/git/.ssh/authorized_keys
, but disable features we don't expect we'll need. It should look something like this:no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAAA JOAKIM-PC
where
AAAAAA
is your public key.
Setting up the custom push functionality
-
Create a
/home/git/git-receive-pack.sh
script where we'll register the function Git calls after connecting via SSH:function git-receive-pack() { # Use /srv/git as our base directory if [ ! -d /srv/git ]; then mkdir -p /srv/git || exit $? fi cd /srv/git # Automatically create repository being pushed if [ ! -d $1 ]; then echo "Initializing repository" >&2 mkdir -p $1 && cd $1 && git init --quiet && git config receive.denyCurrentBranch ignore && git config advice.detachedHead false && cd $OLDPWD fi # Let native git do its thing command git-receive-pack $@ # Checkout the latest commit regardless of branch cd $1 git checkout "$(git log --all -1 --format=%H)" # make sure HEAD and worktree are in sync git reset --hard }
You may notice that the repository isn't initialized using
git init --bare
which is often the recommended approach if you just want to push and pull to your remote. I, however, want to be able to interact with the worktree. In the last section of the script you'll see it checks out the latest commit, regardless of branch, so whenever a push is received the server will checkout whatever branch was last updated. - Finally, in
/home/git/.bashrc
, register our function somewhere at the top of the file:source /home/git/git-receive-pack.sh
Trying it out
On your local machine:
# set it up locally
mkdir potato-repo
cd potato-repo
git init
git remote add laboratory git@my-home-server:potato-repo
echo "Hello world" > README.md
git add README.md
git commit -m 'My first commit!'
git push laboratory
Your output should look something like this:
Initializing repository
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 233 bytes | 233.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
HEAD is now at 2d29073 My first commit!
To my-home-server:potato-repo
* [new branch] main -> main
branch 'main' set up to track 'laboratory/main'.
Related reading
I found these links incredibly helpful:
Git push to a new repository on remote, initializing on the fly