PBS 114 — Git: Tracking Branches
In the previous instalment we implemented a simple scenario — a lone developer with one development computer, and a desire to back their work up to a NAS drive mounted on their computer’s file system. We learned how to create a bare repository on the NAS, and then push the work in our local working repository to that remote repository to keep it safe.
We’ll start this instalment by learning how tracking branches can greatly simplify a 2-repository workflow, but we’ll do so while preparing for the next logical leap — a three-repository workflow.
We’ll be implementing a slightly more real-world scenario, a lone developer with a NAS, a desktop computer, and a laptop computer. What was our backup repository on the imagined NAS now becomes the authoritative or primary copy of our repository, and we’ll have two working repositories, one on our imagined desktop, and one on our imagined laptop. The NAS repo now takes on two roles — it remains a backup, but it now also acts as an intermediary between the laptop and desktop repos, allowing our imagined developer to move between computers without risk of losing work.
Git facilitates this workflow, but like any powerful tool, we need to learn how to use it before we let it loose in the real world! It’s not complicated, but it does have the potential to be very confusing, so we need to build up our knowledge carefully. To that end, we’ll be splitting our move to this new scenario over two instalments. We’ll start the process in this instalment by learning about tracking branches. Tracking makes the two-repository workflow much more efficient, and, without it, a three-repository workflow would be possible, but utterly impractical.
We’ll finish this instalment by creating our third repository, then we’ll pick it up from there next time.
Matching Podcast Episode
Listen along to this instalment on episode 681 of the Chit Chat Across the Pond Podcast.
You can also Download the MP3
- The instalment ZIP file — pbs114.zip.
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
pbs114-init.sh as well as a bundle file name
This script automates the steps bring us back to where we left off last time, one working repository now named
pbs114a-desktop, and one pretend-NAS-repository now named
pbs114a-nas. If you open the script in your favourite text editor you’ll see that it does the following:
- create a folder named
- change into that folder
- initialise it as a Git repo
- if needed, change the default branch from
- import all branches and tags from the bundle
- change back to the zip folder
- created a folder named
pbs114a-nas.gitas a bare Git repo
- change into the
pbs114a-nas.gitas a remote named
origin(the prentend-NAS repo)
- push all branches to
- push all tags to
- change into
pbs114a-nas.gitand force it’s default branch to be
main(only needed because we’re injecting content into the bare repository in an unusual way to stop Git being too helpful and ruining the examples)
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 *.sh
Now we can execute it with:
If you view the contents of the folder (in your file manager or on the Terminal with the
ls command) you’ll see that two new folders have been created named
pbs114a-nas, these are our repositories.
A Little House-Keeping — Deleting an Obsolete Fetched Branch from a non-Existent Remote
When we imported everything from the bundle file, we actually imported a little more than we had intended to.
The bundle was created from the repository as it stood at the end of the previous instalment. At that time there was a remote named
backup which had a branch named
main, and we’d fetched that remote, so the repository contained a cache of that remote branch.
Remotes don’t restore from bundles, and, our init script did not create a new remote named
backup, instead, it created a remote named
origin. We can see that our desktop repository has exactly one remote named
origin by changing into that repository (
cd pbs114a-desktop) and listing all remotes (
bart-imac2018:pbs114 bart% cd pbs114a-desktop bart-imac2018:pbs114a-desktop bart% git remote origin bart-imac2018:pbs114a-desktop bart%
However, if we list all cached remote branches (with
git branch -r) we find more than we were expecting:
bart-imac2018:pbs114a-desktop bart% git branch -r backup/main origin/main bart-imac2018:pbs114a-desktop bart%
Since there is no remote named
backup any more, this cached remote branch is useless, and should be removed. We can do this by combining the
-d for delete &
-r for remote flags with the
git branch command:
git branch -dr backup/main
Listing all cached remote branches now shows what we expect, just one remote branch,
bart-imac2018:pbs114a-desktop bart% git branch -r origin/main bart-imac2018:pbs114a-desktop bart%
To allow Git to push and pull without needing explicit branch names all the time we need to tell it which local branches match to which remote branches.
The commands to set up tracking relationships vary a little depending on your exact situation. There are three situations we’ll cover:
- Establishing a tracking relationship between a local and a remote branch that already exist.
- Publishing a local branch that does not previously exist on a remote to that remote.
- Creating a new local branch from a pre-existing remote branch. Put a pin in this scenario until later.
Tracking Existing Branches
The simplest scenario is where both the local and remote branches already exist, and you simply want to tell Git to have the local branch track the remote one. You do this with the
git branch sub-command, and you have to run the command with the desired local branch checked out. The full version of the command is:
git branch --set-upstream-to=REMOTE/REMOTE_BRANCH
REMOTE is the name of a remote, and
REMOTE_BRANCH the name of a branch that exists in the specified remote. Thankfully there is a shorter version of this command:
git branch -u REMOTE/REMOTE_BRANCH
Let’s use this command to connect our local
main branch to the
main branch on our NAS repository which we have named
origin. Before we start, we need to be sure we are on our local main branch with a quick
git status (if you’re not, check out
git checkout main). We’re now ready to create the link:
git branch -u origin/main
We can see all the tracking relationships that exist with a very verbose branch listing, i.e. with the
-vv flag on the
git branch command:
bart-imac2018:pbs114a-desktop bart% git branch -vv * main b029d25 [origin/main] chore: bumped instalment numbers bart-imac2018:pbs114a-desktop bart%
We only have one branch ATM, so this is not a very interesting list, but it is worth describing in detail. Firstly, it’s one local branch per line, so if we had more local branches we’d have more lines. The
* at the start of a line shows which local branch is currently checked out. The first real column shows the name of the local branch (
main above), the second column the short hash of the current most recent commit on that branch, then, if there is a tracking relationship defined, the remote branch enclosed within square brackets, so
[origin/main] above, and finally the most recent commit’s commit message.
Also notice that the
git status command now gives us information about the tracked remote branch, and our status relative to it:
bart-imac2018:pbs114a-desktop bart% git status On branch main Your branch is up to date with 'origin/main'. nothing to commit, working tree clean bart-imac2018:pbs114a-desktop bart%
In this case, it’s telling us we’re up to date with the remote branch
Publishing a New Local Branch to a Remote
The next common scenario is creating a new branch locally and then publishing it to a remote. You could push the branch with
git push and then track the branch with
git branch -u, but because this is such a common thing to want to do, Git provides us with a convenient short-cut to push and track with one command — the
--set-upstream flag for
git push .
Before we go any further, let’s create a new local branch that we can then push and track.
It turns out my history of Hello Worlds is incomplete — there should be an entry for PowerShell dated 2020, so let’s do that.
We’ll start by creating a local dev branch:
git checkout -b dev-powershell
You’ll find an updated version of
index.html in the folder
pbs114a-2 in the instalment ZIP. Replace
index.html in your working tree (
pbs114a-desktop) with this file.
As we can see, we now have some changes to commit:
bart-imac2018:pbs114a-desktop bart% git status On branch dev-powershell Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: index.html no changes added to commit (use "git add" and/or "git commit -a") bart-imac2018:pbs114a-desktop bart%
Let’s commit them:
git commit -am 'feat: added PowerShell in 2020 to Hello World History'
We now have a local branch named
dev-powershell with one commit that does not exist on our remote at all. We want to push it to the remote, and, track the new remote copy.
We could do that in two steps like so:
git push origin dev-powershell git branch -u origin/dev-powershell
But, we can actually do it in with thanks to the
--set-upstream flag for
git push --set-upstream origin dev-powershell
In fact, we can make things even simpler, the
--set-upstream flag has the much shorter alias
-u, so the command we actually want to execute is simply:
git push -u origin dev-powershell
We can now see that we have two tracking branches with
git branch -vv:
bart-imac2018:pbs114a-desktop bart% git branch -vv * dev-powershell 9db968c [origin/dev-powershell] feat: added PowerShell in 2020 to Hello World History main b029d25 [origin/main] chore: bumped instalment numbers
Pushing with Tracking
It turns out there’s a bug in my code — I copied and pasted the entry for PowerShell from the one for Node.js, and while I changed the code snippet, I forgot to update the URL to learn more. Let’s correct that now!
You’ll find an updated version of
index.html in the folder
pbs114a-3 in the instalment ZIP. Replace
index.html in your working tree (
pbs114a-desktop) with this file.
Now let’s commit our fix:
git commit -am 'Fix: corrected PowerShell URL'
At this point we have a commit on our local
dev-powershell branch that doesn’t exist on the remote
dev-powershell branch. Because we’ve established a tracking relationship,
git status will tell us this:
bart-imac2018:pbs114a-desktop bart% git status On branch dev-powershell Your branch is ahead of 'origin/dev-powershell' by 1 commit. (use "git push" to publish your local commits) nothing to commit, working tree clean bart-imac2018:pbs114a-desktop bart%
As Git so kindly tells us, and because we have a tracking relationship, we can push our change to
origin with the nice simple command:
That’s it, we’ve now pushed our changes to our NAS repository where they are safe.
At this stage our new feature is done, so it’s ready to be merged into
main, let’s do that in our new remote-aware way. The process will be to:
- Merge our local
- Push the now updated
bart-imac2018:pbs114a-desktop bart% git checkout main Switched to branch 'main' Your branch is up to date with 'origin/main'. bart-imac2018:pbs114a-desktop bart% git merge dev-powershell Updating b029d25..8351b9c Fast-forward index.html | 8 ++++++++ 1 file changed, 8 insertions(+) bart-imac2018:pbs114a-desktop bart% git push Total 0 (delta 0), reused 0 (delta 0) To ../pbs114a-nas.git b029d25..8351b9c main -> main bart-imac2018:pbs114a-desktop bart%
Automatically Pushing Tags
We’ve now created a new feature, so we should tag it. For reasons that will become obvious later, I am going to use an annotated tag (one that has both a name and a message):
git tag v2.10.0 -m 'Feat: Added PowerShell to the Hello World History'
As we learned in the previous instalment, we could now manually push the tag with
git push v2.10.0, but I’d much rather have Git push tags automatically for me. As a personality profile one taught me, I prefer economy of effort after all 🙂
The key to having tags automatically pushed is the
--follow-tags flag on the
git push command. This flag will automatically push some tags (often the ones you want), but not all, so it’s important to understand the caveats:
- Only Annotated Tags get pushed
- Only tags on commits on the current branch get pushed
What this means is that you need to get into the habit of annotating all important and/or long-term tags, and using un-annotated tags only for temporary local labels. I like to think of un-annotated tags as digital post-it notes — they’re purely for my personal benefit, not for sharing, and they’re ephemeral, that is to say, they’re private and temporary.
So, to push any annotated tags that exist on the local branch but not on the tracking remote branch while pushing any other un-pushed changes, simply use:
git push --follow-tags
Having to remember this cumbersome flag hardly seems much of an improvement over having to remember to push each tag as you created it. Surely we can do better?
Thankfully we can — Git provides a config variable
push.followTags which, if set, will cause
push to always assume
--follow-tags, unless you explicitly use the
--no-follow-tags flag to override the new default.
You can set this option on your account, or, on a per-repository basis:
# set on just the current repository git config --local push.followTags true # set on your account git config --global push.followTags true
Which ever option you choose to use, you can now push the new annotated
v2.10.0 tag with a simple
bart-imac2018:pbs114a-desktop bart% git push Enumerating objects: 1, done. Counting objects: 100% (1/1), done. Writing objects: 100% (1/1), 199 bytes | 199.00 KiB/s, done. Total 1 (delta 0), reused 0 (delta 0) To ../pbs114a-nas.git * [new tag] v2.10.0 -> v2.10.0 bart-imac2018:pbs114a-desktop bart%
Deleting a Tracking Branch
We’ve now finished our new feature — it’s merged into
main, and has been tagged with a version number. We should now clean up after ourselves and delete the
dev-powershell branch, and we should do that both locally and on the
Even if you have a tracking relationship established, Git will never automatically delete a remote branch, if you want to delete a remote branch, you always have to be explicit about it.
We already know we can delete local branches with the
-d flags on the
git branch command, so we can delete our local dev branch easily:
git branch -d dev-powershell
Now, we need to delete the remote branch, and perhaps confusingly, we do this using the
git push command (not the
git branch command like you might expect) with the
-d flags. The command takes the following form:
git push REMOTE --delete BRANCH
REMOTE is the name of a remote, and
BRANCH the name of a branch on that remote. So, in our case we want:
git push origin --delete dev-powershell
Cloning a Repository
We’re now ready to bring a third repository into the mix. Up this point we’ve got a bare repository we’re pretending is on a NAS drive that’s locally mounted on a desktop machine (
pbs114a-nas.git), and a working repository in that imagined desktop machine we’ve been using to do our work (
We now want a repository we’re going to pretend is on our laptop, and to keep things simple, we’re going to assume the NAS drive is mounted on the laptop too. We now need to create a new working copy from the contents of the bare repo on the NAS. The Git command for doing this is
git clone, and it’s a very powerful command that does a lot of things for you automatically, including establishing tracking relationships.
To create our third repository, open a second terminal window, and change into the folder that contains the init script, the bundle file, and our two existing repos. We’re going to clone
pbs114a-nas.git into a new repo named
pbs114a-laptop. The basic structure of the command is:
git clone REMOTE_URL FOLDER
REMOTE_URL is the URL to the Git repository to clone, and
FOLDER is the local folder to use as the new repository. Note that specifying a folder is optional — if you don’t, a new folder in the current directory with the same name as the remote repository is used.
We’d like to clone
pbs114a-nas.git into a new folder named
pbs114a-laptop, so the command we need is:
git clone pbs114a-nas.git pbs114a-laptop
Now, change into the newly created repository:
Not only has the clone initialised our repository with all our commits, branches, and tags:
bart-imac2018:pbs114a-laptop bart% git --no-pager log --oneline 8351b9c (HEAD -> main, tag: v2.10.0, origin/main, origin/HEAD) Fix: corrected PowerShell URL 9db968c feat: added PowerShell in 2020 to Hello World History b029d25 (tag: v2.9.0) chore: bumped instalment numbers a007430 (tag: v2.8.0) feat: added Back to the Future Day trivia box d86b2e3 (tag: v2.7.0) chore: bumped instalment number to 113 05970fb (tag: v2.6.1) chore: bumped instalment numbers to 112 0df6358 (tag: v2.6.0) feat: added About the Authors section to README 4c09611 feat: added About the Authors section to README b70fb8d (tag: v2.5.2) fix: updated licensing paragraph in README 728ce55 feat: Added an About the Authors section to the README 3e29f1f (tag: v2.5.1) chore: bumped instalment numbers from 110 to 111 and fixed a typo 74d6dd0 (tag: v2.5.0) feat: added an alert with the age of the series 9890fb8 feat: added PBS age dismissible alert 03e7d10 chore: added MomentJS b45a1e4 (tag: v2.4.1) fix: expanded Open Source paragraph to include jQuery & Popper.js 275f564 (tag: v2.4.0) feat: added 'Powered by Open Source' section 4793fd7 (tag: v2.3.0) feat: bumped instalment numbers from 109 to 110 8f4e695 (tag: v2.2.0) feat: carousel links 26b67ea feat: added more info links for each programming language 069204c (tag: v2.1.1) fix: descriptions adc5fb3 wip: added captions to the first fice slides 4f085d3 (tag: v2.1.0) Merge branch 'dev' into main 5f12dcd feat: bumped instalment number to 109 42b42d6 (tag: v2.0.0) feat: replaced static programming history in Hello Worlds with carousel c66d9bf wip: converted programming history from single pre to carousel fe1492f wip: added Bootstrap JS support 235b86f chore: bumped instalment to 108 8b5791c fixed typo 4be9147 Bumped instalment number to 107 cf47673 Added Easter Egg into HTML file 46e114a retrieved the Easter Egg PNG from the past cef53ce updated instalment references to 106 9e0df06 Removed the Easter Eggs 43bca0f (tag: v1.1.0) Added Easter Egg into HTML file c50e09c added a little Easter Egg 27d4f35 Updated PBS instalment number to 105 in all files ff15d67 Fixed Markdown link in HTML file ff8bc62 (tag: v1.0.0) Added my history of programming in Hello World programs 2e3a0ce Correct semantic markup by moving title and lead into header and adding a footer 80025b1 Added link to Wikipedia article on the Hello World meme a6d4bfd Added Bootstrap licensing info to README 7cf6b0b Moved from plain HTML to Bootstrap c88546e Fixed silly typo in README d58f072 initial version bart-imac2018:pbs114a-laptop bart%
Git has also automatically created a remote named
origin which points to the URL we cloned from:
bart-imac2018:pbs114a-laptop bart% git remote -v origin /Users/bart/Documents/Temp/pbs114/pbs114a-nas.git (fetch) origin /Users/bart/Documents/Temp/pbs114/pbs114a-nas.git (push) bart-imac2018:pbs114a-laptop bart%
What’s more, Git has also automatically checked out the default branch specified in the cloned repository, which is
main in our case, and, it has enabled tracking:
bart-imac2018:pbs114a-laptop bart% git status On branch main Your branch is up to date with 'origin/main'. nothing to commit, working tree clean bart-imac2018:pbs114a-laptop bart%
git clone has provided us with a new working copy of our repository that’s ready to use in just a single command!
Now that we understand branch-level relationships between repositories we can very efficiently work with two repositories, and, we’ve learned how to quickly and easily add more repositories into the mix with
Adding more repositories complicates our workflow a little. When you only have two repositories, one you do all your work in, and one you archive your work to, you only need to push data from one repo to the other, you never need to pull. Once you add even one more repo into the mix, you need to start pulling as well as pushing, and that’s what we’ll learn to do in the next instalment.
Join the Community
Find us in the PBS channel on the Podfeet Slack.