Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 113 of X — My First Git Remote

In the previous instalment we introduced the concepts and jargon around git repositories interacting with each other. Or, to put it into Git jargon, we learned about remotes. We introduced the topic with some simplified example scenarios where you would use Git Remotes (basically all the time), and we introduced the concept of bare repositories.

We also learned that Git Remotes are accessed via URLs, and that there are multiple possible URL schemes for accessing them, but that we would only be using two, local access when the remote is on the same file system as the repository connecting to it, and HTTPS when we’re dealing with cloud-hosted remotes.

In this instalment we’ll get practical by making a start on implementing the first scenario from the previous instalment, the lone developer with a NAS who wishes to keep a copy of their work on their NAS both as a backup, and, to facilitate switching between computers. We’ll focus on just the first of those motivations in this instalment — using a remote repository for backup. We’ll leave facilitating working from multiple computers until next time.

Matching Podcast Episode

Listen along to this instalment on episode 679 of the Chit Chat Across the Pond Podcast.

You can also Download the MP3

Instalment Resources

Playing Along

If you’d like to play along with the examples you’ll need to download this instalment’s ZIP file and unzip it. Open a terminal and change to the folder into which you extracted the ZIP . You’ll find a bash script named pbs113-init.sh as well as a bundle file name pbs113a.bundle.

This script automates the steps we’ve been doing manually in the past few instalments to convert bundles into repositories:

  1. create a folder named pbs113a
  2. change into that folder
  3. initialise it as a Git repo
  4. if needed, change the default branch from master to main
  5. import all branches and tags from the bundle

One you have a terminal open in the folder the ZIP extracted to you’re ready to run the script. We’ll start by making sure the script is executable:

chmod 755 pbs113-init.sh

Now we can execute it with:

./pbs113-init.sh

If you view the contents of the folder (in your file manager or on the Terminal with the ls command) you’ll see that a new folder has been created named pbs113a, this is our repository.

Bonus Extra — Echoing Commands in a Shell Script

You may have noticed that as the shell script executed it showed each command executed pre-fixed with a + symbol. This is not Bash’s default behaviour — it normally only shows the outputs generated by the commands the commands the script executes. It can often be helpful to watch what a script does, so Bash provides an optional -x flag to enable command echoing.

You’ll notice that the so-called shebang line at the very top of the script specifies that the OS should execute the script with bash with the -x flag:

#! /bin/bash -x

Note that Zsh also supports the -x flag.

Creating a Bare Repository

Up until this point we’ve not created any Git repositories we’ve not done coding work directly within. In Git speak, we’ve used the working tree in all our repositories to do our development work. Think of all our repositories so far as working repositories.

As we start to use remote repositories as backups or canonical sources, that assumption falls apart. You’re never going to actively write code in the backup repository on your NAS, or, on the canonical copy of your team’s repository on the server in work. I think if these as storage repositories, and they have no need of a working tree.

Git supports repositories without working trees, and it has a special name for them — bare repositories.

Our new backup repository should be a bare repository, so let’s start by creating one.

In a real-work scenario you’d probably create this bare repository on some kind of NAS or shared drive, but we’ll keep things simple and simply create our in the folder we’re working in (the one with the init script, bundle file, and the pbs113a repo). We’ll name our bare repository pbs113a-backup.git.

To create our bare repository we first create a folder with the desired name, then we initialise it as a bare Git repository using the git init command with the --bare flag:

mkdir pbs113a-backup.git
git init --bare pbs113a-backup.git

Adding a Remote to an Existing Repository

Let’s now enter our primary repository, and add the bare backup repository to it as a remote.

The first step is to change into our repository and check the proverbial lay of the land:

bart-imac2018:pbs113 bart% cd pbs113a 
bart-imac2018:pbs113a bart% git status
On branch main
nothing to commit, working tree clean
bart-imac2018:pbs113a bart%

As you can see we’re on the main branch with a clean working tree.

We can also see that this repository has no remotes configured yet by running the git remote command without any sub-commands:

bart-imac2018:pbs113a bart% git remote
bart-imac2018:pbs113a bart% 

Let’s use the add sub-command of git remote to define a new remote with the name backup that’s linked to our newly created bare repository.

The git remote add command takes two arguments, a name for the remote, and the URL to access the remote repository. In this case the remote repository is on the local file system, so we’ll use a simple file path as the URL, specifically, we’ll use the relative file path ../pbs113a-backup.git (.. means my parent folder on the terminal).

git remote add backup ../pbs113a-backup.git

We can now see that the remote exists by invoking git remote without any sub-commands or flags:

bart-imac2018:pbs113a bart% git remote                                
backup
bart-imac2018:pbs113a bart% 

Or, more usefully, we can see the details of the defined remotes by adding the -v (for verbose) flag:

bart-imac2018:pbs113a bart% git remote -v
backup	../pbs113a-backup.git (fetch)
backup	../pbs113a-backup.git (push)
bart-imac2018:pbs113a bart%

Fetching & Viewing Remote Branches & Tags

For Git to learn the contents of a remote repository, we have to ask it to retrieve the needed information with the git fetch command. We can pass the command one or more remote names, or the flag --all. Let’s fetch the details from our backup repository:

git fetch backup

We can now list all the remote branches that our repository knows about by passing the -r flag to the git branch command:

git branch -r

We get no output because our only remote contains absolutely nothing.

Note that this command lists all fetched branches on all configured remotes, it does not read the data from the remote itself. This is why it’s important to fetch before trying to work interact remote branches.

Viewing remote tags does not work quite as you might expect — since you view local branches with git branch, and remote branches with git branch -r, you might assume that since you view local tags with git tag you could view remote tags with git tag -r, but alas not 🙁

The reason for this is very simple — remote tags are not fetched by the git fetch command, so they can’t be read from the local cache. To view remote branches you need to read the data directly from the remote repository.

The command for reading information directly from a remote repository of git ls-remote. By default it will list both branches and tags, but you can limit it to just tags with the --tags flag.

So, the command to read all branches and tags directly from our backup remote is:

git ls-remote backup

And the command to see just the tags is:

git ls-remote --tags backup

Again, since our remote repository is completely empty, both of these command return nothing at all.

Put a mental pin in these commands for a moment — we’ll use them shortly to verify that our backup has been properly initialised.

Pushing Everything to the Remote

We want our remote to be a full backup of everything in our repository, so we want to send all branches and tags to the remote. We can do this with the --all and --tags flags and the git push command:

First, push all the branches:

git push backup --all

Then push all the tags:

git push backup --tags

Viewing Remote Branches & Tags Revisited

We can verify that we have pushed all the data to our backup using the git ls-remote command:

bart-imac2018:pbs113a bart% git ls-remote backup
d86b2e36f75ddf958c2a9eddd9868e8ce9f1474b	refs/heads/main
ff8bc623afaabb9c2bcde5bc1bc492dc685a2f3e	refs/tags/v1.0.0
43bca0f7cc7befa88172f217f4d74b3ec58dffc6	refs/tags/v1.1.0
42b42d674716c55d25e4728a0fc8b25823a8e0ed	refs/tags/v2.0.0
4f085d37edf75bb8b157793ccf71d48057a9fe27	refs/tags/v2.1.0
069204cbe50a068c512f152ce37dac9e575c76d5	refs/tags/v2.1.1
8f4e695bd09b458c1c09960cfa73fe37fc853282	refs/tags/v2.2.0
4793fd78939e78f3416e6b966e290651589d817b	refs/tags/v2.3.0
275f564428b0aa21b50ab40a4fd4a674a444f2b3	refs/tags/v2.4.0
b45a1e487e4f7f09480de3e5fb2ee4f21737ecae	refs/tags/v2.4.1
74d6dd0f450097c5c6a792ee8841776cc7f88739	refs/tags/v2.5.0
3e29f1fe0cab9488439bca2c115b012998e45ab3	refs/tags/v2.5.1
b70fb8d6001bbe689786466006f6649242b0c55e	refs/tags/v2.5.2
0df6358fbdc40be7037659fcc5c9db6b6a607cc5	refs/tags/v2.6.0
05970fbccd7583bda297487d3276b9034c000e22	refs/tags/v2.6.1
d86b2e36f75ddf958c2a9eddd9868e8ce9f1474b	refs/tags/v2.7.0
bart-imac2018:pbs113a bart%

The output of this command is not as clear as it could be. We’ve said before that branches are just references to commits which act as the head of a branch, and tags are just references to commits. When you bear that in mind the output begins to make sense.

The first column in the output lists commit hashes, and the second column the thing referencing those commits. Anything starting with refs/heads/ is a remote branch, and anything starting with refs/tags/ is a remote tag.

Pushing Changes to a Remote Branch

We have now succeeded in creating a backup of all our work up to this point, but that’s not the end of the story, we now need to update this backup as we create new commits.

Let’s make a change and commit it so we have something to work with.

You’ll find an updated version of index.html in the pbs113a-2 folder in the instalment zip, copy that into your working copy. You’ll see this has added a new trivia box about Back to the Future Day.

Because this is a new feature let’s commit it with an appropriate Conventional-Commit-style message:

git commit -am 'feat: added Back to the Future Day trivia box'

Since this is a new feature, let’s also tag it with a new SemVer-style version number. As the git tag command shows us, the current version number is 2.7.0, since this is a non-breaking feature update, we need to bump the middle number, so the new version should be 2.8.0:

git tag v2.8.0

We have made these changes in our local repository, and only in our local repository. Let’s prove that to ourselves by looking at the hash of the commit at the head of our local main branch (using the git log command):

bart-imac2018:pbs113a bart% git log --pretty -1
commit a007430fcb621de92cf877cf865281ef4b89f797 (HEAD -> main, tag: v2.8.0)
Author: Bart Busschots <opensource@bartificer.net>
Date:   Fri Apr 2 17:19:54 2021 +0100

    feat: added Back to the Future Day trivia box
bart-imac2018:pbs113a bart% 

So our local version of main is at a007430fcb621de92cf877cf865281ef4b89f797, and that commit is pointed to by the local tag v2.8.0.

What’s the state of the backup remote?

bart-imac2018:pbs113a bart% git ls-remote backup
d86b2e36f75ddf958c2a9eddd9868e8ce9f1474b	refs/heads/main
ff8bc623afaabb9c2bcde5bc1bc492dc685a2f3e	refs/tags/v1.0.0
43bca0f7cc7befa88172f217f4d74b3ec58dffc6	refs/tags/v1.1.0
42b42d674716c55d25e4728a0fc8b25823a8e0ed	refs/tags/v2.0.0
4f085d37edf75bb8b157793ccf71d48057a9fe27	refs/tags/v2.1.0
069204cbe50a068c512f152ce37dac9e575c76d5	refs/tags/v2.1.1
8f4e695bd09b458c1c09960cfa73fe37fc853282	refs/tags/v2.2.0
4793fd78939e78f3416e6b966e290651589d817b	refs/tags/v2.3.0
275f564428b0aa21b50ab40a4fd4a674a444f2b3	refs/tags/v2.4.0
b45a1e487e4f7f09480de3e5fb2ee4f21737ecae	refs/tags/v2.4.1
74d6dd0f450097c5c6a792ee8841776cc7f88739	refs/tags/v2.5.0
3e29f1fe0cab9488439bca2c115b012998e45ab3	refs/tags/v2.5.1
b70fb8d6001bbe689786466006f6649242b0c55e	refs/tags/v2.5.2
0df6358fbdc40be7037659fcc5c9db6b6a607cc5	refs/tags/v2.6.0
05970fbccd7583bda297487d3276b9034c000e22	refs/tags/v2.6.1
d86b2e36f75ddf958c2a9eddd9868e8ce9f1474b	refs/tags/v2.7.0
bart-imac2018:pbs113a bart% 

The remote main branch is not at the same commit as our local one, and, there is no remote v2.8.0 tag.

Let’s push these changes to our backup.

First, let’s push the changes on our current branch (main) to the branch with the same name in the remote repository named backup:

bart-imac2018:pbs113a bart% git push backup main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 673 bytes | 673.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To ../pbs113a-backup.git
   d86b2e3..a007430  main -> main
bart-imac2018:pbs113a bart%

Next, let’s push the v2.8.0 tag to the remote repository named backup:

bart-imac2018:pbs113a bart% git push backup v2.8.0
Total 0 (delta 0), reused 0 (delta 0)
To ../pbs113a-backup.git
 * [new tag]         v2.8.0 -> v2.8.0
bart-imac2018:pbs113a bart%

You can see that first argument to the git push command is the remote we’d like to push to, followed by what we’d like to push. The output shows the changes made in the remote repository.

We can now use the git ls-remote command to verify that we have indeed pushed our changes to our backup repo:

bart-imac2018:pbs113a bart% git ls-remote backup     
a007430fcb621de92cf877cf865281ef4b89f797	refs/heads/main
ff8bc623afaabb9c2bcde5bc1bc492dc685a2f3e	refs/tags/v1.0.0
43bca0f7cc7befa88172f217f4d74b3ec58dffc6	refs/tags/v1.1.0
42b42d674716c55d25e4728a0fc8b25823a8e0ed	refs/tags/v2.0.0
4f085d37edf75bb8b157793ccf71d48057a9fe27	refs/tags/v2.1.0
069204cbe50a068c512f152ce37dac9e575c76d5	refs/tags/v2.1.1
8f4e695bd09b458c1c09960cfa73fe37fc853282	refs/tags/v2.2.0
4793fd78939e78f3416e6b966e290651589d817b	refs/tags/v2.3.0
275f564428b0aa21b50ab40a4fd4a674a444f2b3	refs/tags/v2.4.0
b45a1e487e4f7f09480de3e5fb2ee4f21737ecae	refs/tags/v2.4.1
74d6dd0f450097c5c6a792ee8841776cc7f88739	refs/tags/v2.5.0
3e29f1fe0cab9488439bca2c115b012998e45ab3	refs/tags/v2.5.1
b70fb8d6001bbe689786466006f6649242b0c55e	refs/tags/v2.5.2
0df6358fbdc40be7037659fcc5c9db6b6a607cc5	refs/tags/v2.6.0
05970fbccd7583bda297487d3276b9034c000e22	refs/tags/v2.6.1
d86b2e36f75ddf958c2a9eddd9868e8ce9f1474b	refs/tags/v2.7.0
a007430fcb621de92cf877cf865281ef4b89f797	refs/tags/v2.8.0
bart-imac2018:pbs113a bart%

Note that the remote main branch is now also at commit a007430fcb621de92cf877cf865281ef4b89f797, and there’s now a remote tag named v2.8.0 pointing at that same commit.

Final Thoughts

We’ve now seen how we can create bare Git repositories and push commits, branches, and tags to them. This is an important first step in utilising the power of remote repos, but it really is just the first step. At this point we’ve not established any kind of relationship between specific local and remote branches, and as a result, we have to explicitly push our changes at targeted remote branches. We also have to explicitly push our tags. This works, but it’s both error prone and tedious, so of course there must be a better way, and there is 🙂. In the next instalment we’ll discover the power of tracking remote branches, how that simplifies our use of a remote as a backup, and how it enables our use of a remote as a transfer-hub between multiple computers.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack