Logo
Logo

Programming by Stealth

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

PBS 115 of X — The Push & Pull of Git

Starting with instalment 112 we’ve been discovering how Git establishes peer-to-peer relationships between repositories. We started with the theory and the jargon, then in instalment 113 we moved on to create our first relationship between repositories, and finally in instalment 114 we introduced the concept of establishing relationships between local and remote branches. Or, to use the jargon, we’ve learned that we establish relationships between our current repository and other repositories by adding remotes, and that we can configure local branches to track remote branches.

We learned that different Git commands are used to establish tracking relationships in different scenarios. We used git branch -u to set the current local branch to track an existing remote branch, and we used git push -u to publish a local branch to a remote and track it. We also put a pin in a third scenario — creating a new local tracking branch from a remote branch. We didn’t learn how to do that, just that it was possible. That will be the most important thing we learn in this instalment, but we’ll learn it within a broader context.

In the previous instalment we introduced the scenario of a lone developer who has a NAS for storing the authoritative copy of their repository, and working copies of the repository on their laptop and desktop computers for doing their development work. It’s entirely normal and to be expected for our imagined developer to start work on one of their machines, then continue on another before switching back to the first. With tracking branches this proves very easy to do!

Matching Podcast Episode

Listen along to this instalment on episode 684 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 pbs115-init.sh as well as a bundle file name pbs115a.bundle.

This script automates the steps bring us back to where we left off last time, two working repositories now named pbs115a-desktop & pbs115a-laptop, and one pretend-NAS-repository now named pbs115a-nas.git. If you open the script in your favourite text editor you’ll see that it does the following:

  1. clone the bundle into a new bare repository named pbs115a-nas.git
  2. clone the new NAS repository into a regular new repository named pbs115a-desktop
  3. clone the NAS repository again into a regular new repository named pbs115a-laptop

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

./pbs115-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 three new folders have been created named pbs114a-nas.git, pbs115a-desktop, and pbs114a-laptop, these are our repositories.

Worked Example — a New Feature Developed Across Two Computers

It turns out I’ve forgotten to include another language in my history of Hello Worlds — in around 2019 I started to program in Zsh. Let’s add that feature to our example page using our standard process. We’ll start on our imaginary desktop, then get interrupted and need to switch to our imaginary laptop and continue work from there. In the process of switching from one to the other we’ll learn how to create a new tracking local branch from a remote branch.

Let’s get started by changing into our desktop repository:

cd pbs115a-desktop

Next, we’ll create a dev branch to work on:

git checkout -b dev-zsh

We now start work by first adding the carrousel item. You’ll find a copy of index.html with this code added in the folder pbs115a-2 in the instalment’s ZIP. Copy this file into your working tree.

Something’s come up and we need to leave the house, so let’s commit, and push (with tracking):

bart-imac2018:pbs115a-desktop bart% git commit -am "WIP: made a start on adding Zsh to Hello World Carousel"
[dev-zsh e5552ed] WIP: made a start on adding Zsh to Hello World Carousel
 1 file changed, 15 insertions(+)
bart-imac2018:pbs115a-desktop bart% git push -u origin dev-zsh
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), 439 bytes | 439.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To /Users/bart/Documents/Temp/pbs115/pbs115a-nas.git
 * [new branch]      dev-zsh -> dev-zsh
Branch 'dev-zsh' set up to track remote branch 'dev-zsh' from 'origin'.
bart-imac2018:pbs115a-desktop bart% 

We’re now ready to switch to our imaginary laptop and pull and track the new branch from there. Let’s change into that repo before we continue:

cd ../pbs115a-laptop

Tracking a NEW Remote Branch

Having switched to our imaginary laptop we can now fetch the latest information from the bare repository on the NAS with a simple git fetch:

bart-imac2018:pbs115a-laptop bart% git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /Users/bart/Documents/Temp/pbs115/pbs115a-nas
 * [new branch]      dev-zsh    -> origin/dev-zsh
bart-imac2018:pbs115a-laptop bart%

Remember that because we cloned the laptop repository from the NAS repository it automatically contains a remote named origin pointing at the NAS repo.

We can see in the output form the fetch that it found a new remote branch named dev-zsh. Had we not paid attention to that output, remember that we can always get a full listing of remote branches with git branch -r:

bart-imac2018:pbs115a-laptop bart% git  branch -r
  origin/HEAD -> origin/main
  origin/dev-zsh
  origin/main
bart-imac2018:pbs115a-laptop bart%

Note that the special remote reference HEAD points to the remote’s default branch, in the case of our origin that’s main.

To continue our work we need to checkout and track the remote branch origin/dev-zsh which does not exist locally yet (remember, you can see the local branches that exist with just git branch, or with git branch -vv if you want to see tracking relationships in the output).

The generic command to checkout and track a remote branch is:

git checkout -b LOCAL_BRANCH REMOTE/REMOTE_BRANCH

Where LOCAL_BRANCH is the name of a local branch (it can be a new branch), REMOTE is the name of the remote, and REMOTE_BRANCH is the name of the remote branch.

Note that if you like, you can use different names for the local and remote branches. As a general rule, I advise against that, it usually just leads to confusion, but there are times when it can be useful, so it’s good to know it can be done. The only real world situation where I routines use different remote and local branch names is when collaboration with another person or organisation who’s naming scheme is utterly different to mine/ours.

Because you almost always keep the two names the same, Git offers a convenient shortcut to help reduce repetition in commands, the --track flag. The following two commands mean exactly the same thing:

git checkout -b main origin/main
git checkout --track origin/main

Let’s now go ahead and check out and track our new remote branch:

bart-imac2018:pbs115a-laptop bart% git checkout --track origin/dev-zsh
Branch 'dev-zsh' set up to track remote branch 'dev-zsh' from 'origin'.
Switched to a new branch 'dev-zsh'
bart-imac2018:pbs115a-laptop bart% 

As you can see, the tracking relationship was created.

We can now finish off our new feature. You’ll find a final version of index.html in the pbs115a-3 folder in the instalment ZIP, copy that file into your working tree.

We can now commit our change with:

git commit -am 'Feat: added Zsh to Hello World History'

And we should push our change to the NAS with git push:

bart-imac2018:pbs115a-laptop bart% git push
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), 358 bytes | 358.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To /Users/bart/Documents/Temp/pbs115/pbs115a-nas.git
   e5552ed..aa8a792  dev-zsh -> dev-zsh
bart-imac2018:pbs115a-laptop bart% 

After a little testing to make sure we’re happy that our new feature is finished we can merge it into main by switching to the main branch and then merging in our dev branch:

bart-imac2018:pbs115a-laptop bart% git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
bart-imac2018:pbs115a-laptop bart% git merge dev-zsh
Updating 2df67ab..aa8a792
Fast-forward
 index.html | 8 ++++++++
 1 file changed, 8 insertions(+)
bart-imac2018:pbs115a-laptop bart% 

In keeping with our standard process we should tag this commit with a new version number:

git tag v2.12.0 -m 'Feat: added Zsh to Hello World history'

Remember, we are using an annotated tag (one with a message) so our tag can be automatically pushed if we’ve configured Git to do so.

We can now push our updated main branch to the NAS with a simple git push:

bart-imac2018:pbs115a-laptop bart% git push
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 192 bytes | 192.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To /Users/bart/Documents/Temp/pbs115/pbs115a-nas.git
   2df67ab..aa8a792  main -> main
 * [new tag]         v2.12.0 -> v2.12.0
bart-imac2018:pbs115a-laptop bart%

Note that the tag was automatically pushed because I followed the advice in the previous instalment and set the global config variable push.followTags to true on my machine.

Our dev branch has now done its work, so we should clean it up both locally and remotely. As we learned last time, we always have to do a local and remote delete as two separate commands:

bart-imac2018:pbs115a-laptop bart% git branch -d dev-zsh
Deleted branch dev-zsh (was aa8a792).
bart-imac2018:pbs115a-laptop bart% git push origin --delete dev-zsh
To /Users/bart/Documents/Temp/pbs115/pbs115a-nas.git
 - [deleted]         dev-zsh
bart-imac2018:pbs115a-laptop bart%

Pulling & Fetching Re-visited

At some time in the future we’ll want to do some more work from our desktop, so let’s change back to that repo:

cd ../pbs115a-desktop

If we were the thinking kind of person we’d do a git fetch before we did anything else, and then we’d take a moment to think about where we are in this repo, we might do a git status to remind ourselves what branch we’re on, and then make an informed decision about what to do, but in reality, most of us will just sit down and blindly pull without looking around, so let’s do that:

bart-imac2018:pbs115a-desktop bart% git pull
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From /Users/bart/Documents/Temp/pbs115/pbs115a-nas
   2df67ab..aa8a792  main       -> origin/main
 * [new tag]         v2.12.0    -> v2.12.0
Your configuration specifies to merge with the ref 'refs/heads/dev-zsh'
from the remote, but no such ref was fetched.
bart-imac2018:pbs115a-desktop bart%

This output bears some closer inspection — we’ve not yet said it explicitly, but git pull will do a git fetch before it tries to merge any changes it may find on the relevant remote branch into your local branch.

The first 8 lines in the output above are actually from the implicit fetch triggered by pulling:

remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From /Users/bart/Documents/Temp/pbs115/pbs115a-nas
   2df67ab..aa8a792  main       -> origin/main
 * [new tag]         v2.12.0    -> v2.12.0

As you can see, we fetched the updates to the main branch, and we got our new version tag.

Only with that implicit fetch completed, does Git try to actually pull updates from the remote branch our currently checked out brach is configured to track, and that does not go so well:

Your configuration specifies to merge with the ref 'refs/heads/dev-zsh'
from the remote, but no such ref was fetched.

What’s going on?

For any of this to make sense we need to do what we should have done before we started, and checked where we were with a quick git status:

bart-imac2018:pbs115a-desktop bart% git status
On branch dev-zsh
Your branch is up to date with 'origin/dev-zsh'.

nothing to commit, working tree clean
bart-imac2018:pbs115a-desktop bart% 

So we’re on the local branch dev-zsh which is configured to track a remote branch named origin/dev-zsh. Now the output from the attempted pull makes perfect sense, it’s basically ‘err — I can’t pull from the remote branch dev-zsh because it doesn’t exist dummy!’

Oh yea — we finished our work on that new feature, so we cleaned up its dev branch. Now what? Well, the first step is definitely to move off the obsolete dev branch and back to our default branch, main:

bart-imac2018:pbs115a-desktop bart% git checkout main
Switched to branch 'main'
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
bart-imac2018:pbs115a-desktop bart% 

Our main branch doesn’t have any of the changes we merged into main on our laptop yet, so as Git very kindly tells us, we need to pull main (now our current branch):

bart-imac2018:pbs115a-desktop bart% git pull
Updating 2df67ab..aa8a792
Fast-forward
 index.html | 8 ++++++++
 1 file changed, 8 insertions(+)
bart-imac2018:pbs115a-desktop bart% 

We now have all the work that was carried out remotely safely merged into our local repo, but we have some detritus to clean up — the obsolete dev-zsh branch is gone from our laptop, and gone form our NAS, but we’ve still go it:

bart-imac2018:pbs115a-desktop bart% git branch
  dev-zsh
* main
bart-imac2018:pbs115a-desktop bart%

Pruning our Local Repository

To properly clean up from our current situation we actually need to do two things.

The obvious thing is to delete our local branches, but there is a second thing we need to clean up — the local cache of the now deleted remote branch. In Git jargon, we need to prune our remote.

We can do this by simply doing a git fetch with the --prune flag:

bart-imac2018:pbs115a-desktop bart% git fetch --prune
From /Users/bart/Documents/Temp/pbs115/pbs115a-nas
 - [deleted]         (none)     -> origin/dev-zsh
bart-imac2018:pbs115a-desktop bart%

This has only delete the cache, it has not actually deleted our local branch named dev-zsh. If we look at all our branches we can see it still exists:

bart-imac2018:pbs115a-desktop bart% git branch -vv
  dev-zsh e5552ed [origin/dev-zsh: gone] WIP: made a start on adding Zsh to Hello World Carousel
* main    aa8a792 [origin/main] Feat: added Zsh to Hello World History
bart-imac2018:pbs115a-desktop bart%

Notice that Git is now telling us our local branch is tracking a non-existent remote branch that it doesn’t even have a cache of by marking the tracking branches as gone.

We can now safely delete the local branch with git branch -d:

bart-imac2018:pbs115a-desktop bart% git branch -d dev-zsh
Deleted branch dev-zsh (was e5552ed).
bart-imac2018:pbs115a-desktop bart% 

Auto-pruning

It’s very easy to forget to prune the local caches of deleted remote branches. This is why I suggest setting the fetch.prune config variable to true globally:

git config --global fetch.prune true

Final Thoughts

At the very start this series-within-a-series in instalment 101 I described our journey into version control as having three phases:

  1. Local Repositories
  2. A Server Just for Us
  3. Collaborating with Others

We wrapped up the first phase with instalment 111, and we’ve just wrapped up the second phase!

We now have all the skills we need to dip our toe into the wider pool, first with team mates, then with the wider community. In terms of core Git functionality we’ve covered all I plan to cover. What remains is examining ways of using what we’ve learned to help us collaborate smoothly and efficiently, and, some GitHub-specific tools and technologies that are built on top of Git.

Whether you’re working alone, as part of a team, or out in the open source community, you’ll be doing the very same things — creating commits, branches, and tags, merging branches, and fetching, pushing, and pulling between remotes. The more people you have working together the more likely you are to bump into the odd merge conflict, but don’t panic when that does happen, as we learned in instalment 110, Git gives us the tools we need to resolve them.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack