Logo
Logo

Programming by Stealth

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

PBS 114 of X: Tracking Branches (Git)

17 Apr 2021

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

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 pbs114-init.sh as well as a bundle file name pbs114a.bundle.

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:

  1. create a folder named pbs114a-desktop
  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
  6. change back to the zip folder
  7. created a folder named pbs114a-nas.git
  8. initialise pbs114a-nas.git as a bare Git repo
  9. change into the pbs114a-desktop folder/repo
  10. add pbs114a-nas.git as a remote named origin
  11. fetch origin (the prentend-NAS repo)
  12. push all branches to origin
  13. push all tags to origin
  14. change into pbs114a-nas.git and 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:

./pbs114-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 two new folders have been created named pbs114a-desktop, and 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 (git remote):

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, origin/main:

bart-imac2018:pbs114a-desktop bart% git branch -r             
  origin/main
bart-imac2018:pbs114a-desktop bart%

Tracking Branches

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:

  1. Establishing a tracking relationship between a local and a remote branch that already exist.
  2. Publishing a local branch that does not previously exist on a remote to that remote.
  3. 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

Where 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 main with 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 origin/main.

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:

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:

git push

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:

  1. Checkout main
  2. Merge our local dev-powershell into main
  3. Push the now updated main to origin
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:

  1. Only Annotated Tags get pushed
  2. 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 git push:

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 origin remote.

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 --delete or -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 --delete or -d flags. The command takes the following form:

git push REMOTE --delete BRANCH

Where 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 (pbs114a-desktop).

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

Where 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:

cd pbs114a-laptop

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%

In short, git clone has provided us with a new working copy of our repository that’s ready to use in just a single command!

Final Thoughts

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 git clone.

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.

Podfeet Slack