Learn Git Branching - All Levels Documentation

Level Sequence: Introduction Sequence

A nicely paced introduction to the majority of git commands

Level: Introduction to Git Commits

Git Commits

A commit in a git repository records a snapshot of all the (tracked) files in your directory. It's like a giant copy and paste, but even better!

Git wants to keep commits as lightweight as possible though, so it doesn't just blindly copy the entire directory every time you commit. It can (when possible) compress a commit as a set of changes, or a "delta", from one version of the repository to the next.

Git also maintains a history of which commits were made when. That's why most commits have ancestor commits above them -- we designate this with arrows in our visualization. Maintaining history is great for everyone working on the project!

It's a lot to take in, but for now you can think of commits as snapshots of the project. Commits are very lightweight and switching between them is wicked fast!

Let's see what this looks like in practice. On the right we have a visualization of a (small) git repository. There are two commits right now -- the first initial commit, C0, and one commit after that C1 that might have some meaningful changes.

Hit the button below to make a new commit.

git commit

There we go! Awesome. We just made changes to the repository and saved them as a commit. The commit we just made has a parent, C1, which references which commit it was based off of.

Go ahead and try it out on your own! After this window closes, make two commits to complete the level.

Level: Branching in Git

Git Branches

Branches in Git are incredibly lightweight as well. They are simply pointers to a specific commit -- nothing more. This is why many Git enthusiasts chant the mantra:

branch early, and branch often

Because there is no storage / memory overhead with making many branches, it's easier to logically divide up your work than have big beefy branches.

When we start mixing branches and commits, we will see how these two features combine. For now though, just remember that a branch essentially says "I want to include the work of this commit and all parent commits."

Let's see what branches look like in practice.

Here we will create a new branch named newImage.

git branch newImage

There, that's all there is to branching! The branch newImage now refers to commit C1.

Let's try to put some work on this new branch. Hit the button below.

git commit

Oh no! The main branch moved but the newImage branch didn't! That's because we weren't "on" the new branch, which is why the asterisk (*) was on main.

Let's tell git we want to checkout the branch with

git checkout <name>

This will put us on the new branch before committing our changes.

git checkout newImage; git commit

There we go! Our changes were recorded on the new branch.

Note: In Git version 2.23, a new command called git switch was introduced to eventually replace git checkout, which is somewhat overloaded (it does a bunch of different things depending on the arguments). The lessons here will still use checkout instead of switch because the switch command is still considered experimental and the syntax may change in the future. However you can still try out the new switch command in this application, and also learn more here.

Ok! You are all ready to get branching. Once this window closes, make a new branch named bugFix and switch to that branch.

By the way, here's a shortcut: if you want to create a new branch AND check it out at the same time, you can simply type git checkout -b [yourbranchname].

Level: Merging in Git

Branches and Merging

Great! We now know how to commit and branch. Now we need to learn some kind of way of combining the work from two different branches together. This will allow us to branch off, develop a new feature, and then combine it back in.

The first method to combine work that we will examine is git merge. Merging in Git creates a special commit that has two unique parents. A commit with two parents essentially means "I want to include all the work from this parent over here and this one over here, and the set of all their parents."

It's easier with visuals, let's check it out in the next view.

Here we have two branches; each has one commit that's unique. This means that neither branch includes the entire set of "work" in the repository that we have done. Let's fix that with merge.

We will merge the branch bugFix into main.

git merge bugFix

Woah! See that? First of all, main now points to a commit that has two parents. If you follow the arrows up the commit tree from main, you will hit every commit along the way to the root. This means that main contains all the work in the repository now.

Also, see how the colors of the commits changed? To help with learning, I have included some color coordination. Each branch has a unique color. Each commit turns a color that is the blended combination of all the branches that contain that commit.

So here we see that the main branch color is blended into all the commits, but the bugFix color is not. Let's fix that...

Let's merge main into bugFix:

git checkout bugFix; git merge main

Since bugFix was an ancestor of main, git didn't have to do any work; it simply just moved bugFix to the same commit main was attached to.

Now all the commits are the same color, which means each branch contains all the work in the repository! Woohoo!

To complete this level, do the following steps:

Remember, you can always re-display this dialog with "objective"!

Level: Rebase Introduction

Git Rebase

The second way of combining work between branches is rebasing. Rebasing essentially takes a set of commits, "copies" them, and plops them down somewhere else.

While this sounds confusing, the advantage of rebasing is that it can be used to make a nice linear sequence of commits. The commit log / history of the repository will be a lot cleaner if only rebasing is allowed.

Let's see it in action...

Here we have two branches yet again; note that the bugFix branch is currently selected (note the asterisk)

We would like to move our work from bugFix directly onto the work from main. That way it would look like these two features were developed sequentially, when in reality they were developed in parallel.

Let's do that with the git rebase command.

git rebase main

Awesome! Now the work from our bugFix branch is stacked "on top of main", since it points to main. In our visualization though, its shown below main since our commit trees flow downwards.

Note that the commit C3 still exists somewhere (it has a faded appearance in the tree), and C3' is the "copy" that we rebased onto main.

The only problem is that main hasn't been updated either, let's do that now...

Now we are checked out on the main branch. Let's go ahead and rebase onto bugFix...

git rebase bugFix

There! Since main was an ancestor of bugFix, git simply moved the main branch reference forward in history.

To complete this level, do the following

Good luck!

Level Sequence: Ramping Up

The next serving of 100% git awesomes-ness. Hope you're hungry

Level: Detach yo' HEAD

Moving around in Git

Before we get to some of the more advanced features of Git, it's important to understand different ways to move through the commit tree that represents your project.

Once you're comfortable moving around, your powers with other git commands will be amplified!

First we have to talk about "HEAD". HEAD is the symbolic name for the currently checked out commit -- it's essentially what commit you're working on top of.

HEAD always points to the most recent commit which is reflected in the working tree. Most git commands which make changes to the working tree will start by changing HEAD.

Normally HEAD points to a branch name (like bugFix). When you commit, the status of bugFix is altered and this change is visible through HEAD.

Let's see this in action. Here we will reveal HEAD before and after a commit.

git checkout C1; git checkout main; git commit; git checkout C2

See! HEAD was hiding underneath our main branch all along.

Detaching HEAD

Detaching HEAD just means attaching it to a commit instead of a branch. This is what it looks like beforehand:

HEAD -> main -> C1

git checkout C1

And now it's

HEAD -> C1

To complete this level, let's detach HEAD from bugFix and attach it to the commit instead.

Specify this commit by its hash. The hash for each commit is displayed on the circle that represents the commit.

Level: Relative Refs (^)

Relative Refs

Moving around in Git by specifying commit hashes can get a bit tedious. In the real world you won't have a nice commit tree visualization next to your terminal, so you'll have to use git log to see hashes.

Furthermore, hashes are usually a lot longer in the real Git world as well. For instance, the hash of the commit that introduced the previous level is fed2da64c0efc5293610bdd892f82a58e8cbc5d8. Doesn't exactly roll off the tongue...

The upside is that Git is smart about hashes. It only requires you to specify enough characters of the hash until it uniquely identifies the commit. So I can type fed2 instead of the long string above.

Like I said, specifying commits by their hash isn't the most convenient thing ever, which is why Git has relative refs. They are awesome!

With relative refs, you can start somewhere memorable (like the branch bugFix or HEAD) and work from there.

Relative commits are powerful, but we will introduce two simple ones here:

Let's look at the Caret (^) operator first. Each time you append that to a ref name, you are telling Git to find the parent of the specified commit.

So saying main^ is equivalent to "the first parent of main".

main^^ is the grandparent (second-generation ancestor) of main

Let's check out the commit above main here.

git checkout main^

Boom! Done. Way easier than typing the commit hash.

You can also reference HEAD as a relative ref. Let's use that a couple of times to move upwards in the commit tree.

git checkout C3; git checkout HEAD^; git checkout HEAD^; git checkout HEAD^

Easy! We can travel backwards in time with HEAD^

To complete this level, check out the parent commit of bugFix. This will detach HEAD.

You can specify the hash if you want, but try using relative refs instead!

Level: Relative Refs #2 (~)

The "~" operator

Say you want to move a lot of levels up in the commit tree. It might be tedious to type ^ several times, so Git also has the tilde (~) operator.

The tilde operator (optionally) takes in a trailing number that specifies the number of parents you would like to ascend. Let's see it in action.

Let's specify a number of commits back with ~.

git checkout HEAD~4

Boom! So concise -- relative refs are great.

Branch forcing

You're an expert on relative refs now, so let's actually use them for something.

One of the most common ways I use relative refs is to move branches around. You can directly reassign a branch to a commit with the -f option. So something like:

git branch -f main HEAD~3

moves (by force) the main branch to three parents behind HEAD.

Note: In a real git environment git branch -f command is not allowed for your current branch.

Let's see that previous command in action.

git branch -f main HEAD~3

There we go! Relative refs gave us a concise way to refer to C1 and branch forcing (-f) gave us a way to quickly move a branch to that location.

Now that you have seen relative refs and branch forcing in combination, let's use them to solve the next level.

To complete this level, move HEAD, main, and bugFix to their goal destinations shown.

Level: Reversing Changes in Git

Reversing Changes in Git

There are many ways to reverse changes in Git. And just like committing, reversing changes in Git has both a low-level component (staging individual files or chunks) and a high-level component (how the changes are actually reversed). Our application will focus on the latter.

There are two primary ways to undo changes in Git -- one is using git reset and the other is using git revert. We will look at each of these in the next dialog

Git Reset

git reset reverses changes by moving a branch reference backwards in time to an older commit. In this sense you can think of it as "rewriting history;" git reset will move a branch backwards as if the commit had never been made in the first place.

Let's see what that looks like:

git reset HEAD~1

Nice! Git moved the main branch reference back to C1; now our local repository is in a state as if C2 had never happened.

Git Revert

While resetting works great for local branches on your own machine, its method of "rewriting history" doesn't work for remote branches that others are using.

In order to reverse changes and share those reversed changes with others, we need to use git revert. Let's see it in action.

git revert HEAD

Weird, a new commit plopped down below the commit we wanted to reverse. That's because this new commit C2' introduces changes -- it just happens to introduce changes that exactly reverses the commit of C2.

With reverting, you can push out your changes to share with others.

To complete this level, reverse the most recent commit on both local and pushed. You will revert two commits total (one per branch).

Keep in mind that pushed is a remote branch and local is a local branch -- that should help you choose your methods.

Level Sequence: Push & Pull -- Git Remotes!

Time to share your 1's and 0's kids; coding just got social

Level: Clone Intro

Git Remotes

Remote repositories aren't actually that complicated. In today's world of cloud computing it's easy to think that there's a lot of magic behind git remotes, but they are actually just copies of your repository on another computer. You can typically talk to this other computer through the Internet, which allows you to transfer commits back and forth.

That being said, remote repositories have a bunch of great properties:

It's become very popular to use websites that visualize activity around remote repos (like GitHub), but remote repositories always serve as the underlying backbone for these tools. So it's important to understand them!

Our Command to create remotes

Up until this point, Learn Git Branching has focused on teaching the basics of local repository work (branching, merging, rebasing, etc). However now that we want to learn about remote repository work, we need a command to set up the environment for those lessons. git clone will be that command.

Technically, git clone in the real world is the command you'll use to create local copies of remote repositories (from github for example). We use this command a bit differently in Learn Git Branching though -- git clone actually makes a remote repository out of your local one. Sure it's technically the opposite meaning of the real command, but it helps build the connection between cloning and remote repository work, so let's just run with it for now.

Lets start slow and just look at what a remote repository looks like (in our visualization).

git clone

There it is! Now we have a remote repository of our project. It looks pretty similar except for some visual changes to make the distinction apparent -- in later levels you'll get to see how we share work across these repositories.

To finish this level, simply git clone your existing repository. The real learning will come in following lessons.

Level: Remote Branches

Git Remote Branches

Now that you've seen git clone in action, let's dive into what actually changed.

The first thing you may have noticed is that a new branch appeared in our local repository called o/main. This type of branch is called a remote branch; remote branches have special properties because they serve a unique purpose.

Remote branches reflect the state of remote repositories (since you last talked to those remote repositories). They help you understand the difference between your local work and what work is public -- a critical step to take before sharing your work with others.

Remote branches have the special property that when you check them out, you are put into detached HEAD mode. Git does this on purpose because you can't work on these branches directly; you have to work elsewhere and then share your work with the remote (after which your remote branches will be updated).

To be clear: Remote branches are on your local repository, not on the remote repository.

What is o/?

You may be wondering what the leading o/ is for on these remote branches. Well, remote branches also have a (required) naming convention -- they are displayed in the format of:

Hence, if you look at a branch named o/main, the branch name is main and the name of the remote is o.

Most developers actually name their main remote origin, not o. This is so common that git actually sets up your remote to be named origin when you git clone a repository.

Unfortunately the full name of origin does not fit in our UI, so we use o as shorthand :( Just remember when you're using real git, your remote is probably going to be named origin!

That's a lot to take in, so let's see all this in action.

Lets check out a remote branch and see what happens.

git checkout o/main; git commit

As you can see, git put us into detached HEAD mode and then did not update o/main when we added a new commit. This is because o/main will only update when the remote updates.

To finish this level, commit once off of main and once after checking out o/main. This will help drive home how remote branches behave differently, and they only update to reflect the state of the remote.

Level: Git Fetchin'

Git Fetch

Working with git remotes really just boils down to transferring data to and from other repositories. As long as we can send commits back and forth, we can share any type of update that is tracked by git (and thus share work, new files, new ideas, love letters, etc.).

In this lesson we will learn how to fetch data from a remote repository -- the command for this is conveniently named git fetch.

You'll notice that as we update our representation of the remote repository, our remote branches will update to reflect that new representation. This ties into the previous lesson on remote branches.

Before getting into the details of git fetch, let's see it in action! Here we have a remote repository that contains two commits that our local repository does not have.

git fetch

There we go! Commits C2 and C3 were downloaded to our local repository, and our remote branch o/main was updated to reflect this.

What fetch does

git fetch performs two main steps, and two main steps only. It:

git fetch essentially brings our local representation of the remote repository into synchronization with what the actual remote repository looks like (right now).

If you remember from the previous lesson, we said that remote branches reflect the state of the remote repositories since you last talked to those remotes. git fetch is the way you talk to these remotes! Hopefully the connection between remote branches and git fetch is apparent now.

git fetch usually talks to the remote repository through the Internet (via a protocol like http:// or git://).

What fetch doesn't do

git fetch, however, does not change anything about your local state. It will not update your main branch or change anything about how your file system looks right now.

This is important to understand because a lot of developers think that running git fetch will make their local work reflect the state of the remote. It may download all the necessary data to do that, but it does not actually change any of your local files. We will learn commands in later lessons to do just that :D

So at the end of the day, you can think of running git fetch as a download step.

To finish the level, simply git fetch and download all the commits!

Level: Git Pullin'

Git Pull

Now that we've seen how to fetch data from a remote repository with git fetch, let's update our work to reflect those changes!

There are actually many ways to do this -- once you have new commits available locally, you can incorporate them as if they were just normal commits on other branches. This means you could execute commands like:

In fact, the workflow of fetching remote changes and then merging them is so common that git actually provides a command that does both at once! That command is git pull.

Let's first see a fetch and a merge executed sequentially.

git fetch; git merge o/main

Boom -- we downloaded C3 with a fetch and then merged in that work with git merge o/main. Now our main branch reflects the new work from the remote (in this case, named origin)

What would happen if we used git pull instead?

git pull

The same thing! That should make it very clear that git pull is essentially shorthand for a git fetch followed by a merge of whatever branch was just fetched.

We will explore the details of git pull later (including options and arguments), but for now let's try it out in the level.

Remember -- you can actually solve this level with just fetch and merge, but it will cost you an extra command :P

Level: Faking Teamwork

Simulating collaboration

So here is the tricky thing -- for some of these upcoming lessons, we need to teach you how to pull down changes that were introduced in the remote.

That means we need to essentially "pretend" that the remote was updated by one of your coworkers / friends / collaborators, sometimes on a specific branch or a certain number of commits.

In order to do this, we introduced the aptly-named command git fakeTeamwork! It's pretty self explanatory, let's see a demo...

The default behavior of fakeTeamwork is to simply plop down a commit on main.

git fakeTeamwork

There we go -- the remote was updated with a new commit, and we haven't downloaded that commit yet because we haven't run git fetch.

You can also specify the number of commits or the branch by appending them to the command.

git fakeTeamwork foo 3

With one command we simulated a teammate pushing three commits to the foo branch on our remote.

The upcoming levels are going to be pretty difficult, so we're asking more of you for this level.

Go ahead and make a remote (with git clone), fake some changes on that remote, commit yourself, and then pull down those changes. It's like a few lessons in one!

Level: Git Pushin'

Git Push

Ok, so I've fetched changes from remote and incorporated them into my work locally. That's great and all... but how do I share my awesome work with everyone else?

Well, the way to upload shared work is the opposite of downloading shared work. And what's the opposite of git pull? git push!

git push is responsible for uploading your changes to a specified remote and updating that remote to incorporate your new commits. Once git push completes, all your friends can then download your work from the remote.

You can think of git push as a command to "publish" your work. It has a bunch of subtleties that we will get into shortly, but let's start with baby steps...

note -- the behavior of git push with no arguments varies depending on one of git's settings called push.default. The default value for this setting depends on the version of git you're using, but we are going to use the upstream value in our lessons. This isn't a huge deal, but it's worth checking your settings before pushing in your own projects.

Here we have some changes that the remote does not have. Let's upload them!

git push

There we go -- the remote received commit C2, the branch main on the remote was updated to point at C2, and our own reflection of the remote (o/main) was updated as well. Everything is in sync!

To finish this level, simply share two new commits with the remote. Strap in though, because these lessons are about to get a lot harder!

Level: Diverged History

Diverged Work

So far we've seen how to pull down commits from others and how to push up our own changes. It seems pretty simple, so how can people get so confused?

The difficulty comes in when the history of the repository diverges. Before discussing the details of this, let's see an example...

Imagine you clone a repository on Monday and start dabbling on a side feature. By Friday you are ready to publish your feature -- but oh no! Your coworkers have written a bunch of code during the week that's made your feature out of date (and obsolete). They've also published these commits to the shared remote repository, so now your work is based on an old version of the project that's no longer relevant.

In this case, the command git push is ambiguous. If you run git push, should git change the remote repository back to what it was on Monday? Should it try to add your code in while not removing the new code? Or should it totally ignore your changes since they are totally out of date?

Because there is so much ambiguity in this situation (where history has diverged), git doesn't allow you to push your changes. It actually forces you to incorporate the latest state of the remote before being able to share your work.

So much talking! Let's see this situation in action.

git push

See? Nothing happened because the command fails. git push fails because your most recent commit C3 is based off of the remote at C1. The remote has since been updated to C2 though, so git rejects your push.

How do you resolve this situation? It's easy, all you need to do is base your work off of the most recent version of the remote branch.

There are a few ways to do this, but the most straightforward is to move your work via rebasing. Let's go ahead and see what that looks like.

Now if we rebase before pushing instead...

git fetch; git rebase o/main; git push

Boom! We updated our local representation of the remote with git fetch, rebased our work to reflect the new changes in the remote, and then pushed them with git push.

Are there other ways to update my work when the remote repository has been updated? Of course! Let's check out the same thing but with merge instead.

Although git merge doesn't move your work (and instead just creates a merge commit), it's a way to tell git that you have incorporated all the changes from the remote. This is because the remote branch is now an ancestor of your own branch, meaning your commit reflects all commits in the remote branch.

Lets see this demonstrated...

Now if we merge instead of rebasing...

git fetch; git merge o/main; git push

Boom! We updated our local representation of the remote with git fetch, merged the new work into our work (to reflect the new changes in the remote), and then pushed them with git push.

Awesome! Is there any way I can do this without typing so many commands?

Of course -- you already know git pull is just shorthand for a fetch and a merge. Conveniently enough, git pull --rebase is shorthand for a fetch and a rebase!

Let's see these shorthand commands at work.

First with --rebase...

git pull --rebase; git push

Same as before! Just a lot shorter.

And now with regular pull.

git pull; git push

Again, exact same as before!

This workflow of fetching, rebase/merging, and pushing is quite common. In future lessons we will examine more complicated versions of these workflows, but for now let's try this out.

In order to solve this level, take the following steps:

Level: Locked Main

Remote Rejected!

If you work on a large collaborative team it's likely that main is locked and requires some Pull Request process to merge changes. If you commit directly to main locally and try pushing you will be greeted with a message similar to this:

 ! [remote rejected] main -> main (TF402455: Pushes to this branch are not permitted; you must use a pull request to update this branch.)

Why was it rejected?

The remote rejected the push of commits directly to main because of the policy on main requiring pull requests to instead be used.

You meant to follow the process creating a branch then pushing that branch and doing a pull request, but you forgot and committed directly to main. Now you are stuck and cannot push your changes.

The solution

Create another branch called feature and push that to the remote. Also reset your main back to be in sync with the remote otherwise you may have issues next time you do a pull and someone else's commit conflicts with yours.

Level Sequence: To Origin And Beyond -- Advanced Git Remotes!

And you thought being a benevolent dictator would be fun...

Level: Push Main!

Merging feature branches

Now that you're comfortable with fetching, pulling, and pushing, let's put these skills to the test with a new workflow.

It's common for developers on big projects to do all their work on feature branches (off of main) and then integrate that work only once it's ready. This is similar to the previous lesson (where side branches get pushed to the remote), but here we introduce one more step.

Some developers only push and pull when on the main branch -- that way main always stays updated to what is on the remote (o/main).

So for this workflow we combine two things:

Let's see a refresher real quick of how to update main and push work.

git pull --rebase; git push

We executed two commands here that:

This level is pretty hefty -- here is the general outline to solve:

:O intense! good luck, completing this level is a big step.

Level: Merging with remotes

Why not merge?

In order to push new updates to the remote, all you need to do is incorporate the latest changes from the remote. That means you can either rebase or merge in the remote branch (e.g. o/main).

So if you can do either method, why have the lessons focused on rebasing so far? Why is there no love for merge when working with remotes?

There's a lot of debate about the tradeoffs between merging and rebasing in the development community. Here are the general pros / cons of rebasing:

Pros:

Cons:

For example, commit C1 can be rebased past C3. It then appears that the work for C1' came after C3 when in reality it was completed beforehand.

Some developers love to preserve history and thus prefer merging. Others (like myself) prefer having a clean commit tree and prefer rebasing. It all comes down to preferences :D

For this level, let's try to solve the previous level but with merging instead. It may get a bit hairy but it illustrates the point well.

Level: Remote Tracking

Remote-Tracking branches

One thing that might have seemed "magical" about the last few lessons is that git knew the main branch was related to o/main. Sure these branches have similar names and it might make logical sense to connect the main branch on the remote to the local main branch, but this connection is demonstrated clearly in two scenarios:

Remote tracking

Long story short, this connection between main and o/main is explained simply by the "remote tracking" property of branches. The main branch is set to track o/main -- this means there is an implied merge target and implied push destination for the main branch.

You may be wondering how this property got set on the main branch when you didn't run any commands to specify it. Well, when you clone a repository with git, this property is actually set for you automatically.

During a clone, git creates a remote branch for every branch on the remote (aka branches like o/main). It then creates a local branch that tracks the currently active branch on the remote, which is main in most cases.

Once git clone is complete, you only have one local branch (so you aren't overwhelmed) but you can see all the different branches on the remote (if you happen to be very curious). It's the best of both worlds!

This also explains why you may see the following command output when cloning:

local branch "main" set to track remote branch "o/main"

Can I specify this myself?

Yes you can! You can make any arbitrary branch track o/main, and if you do so, that branch will have the same implied push destination and merge target as main. This means you can run git push on a branch named totallyNotMain and have your work pushed to the main branch on the remote!

There are two ways to set this property. The first is to checkout a new branch by using a remote branch as the specified ref. Running

git checkout -b totallyNotMain o/main

Creates a new branch named totallyNotMain and sets it to track o/main.

Enough talking, let's see a demonstration! We will checkout a new branch named foo and set it to track main on the remote.

git checkout -b foo o/main; git pull

As you can see, we used the implied merge target of o/main to update the foo branch. Note how main doesn't get updated!!

This also applies for git push.

git checkout -b foo o/main; git commit; git push

Boom. We pushed our work to the main on the remote even though our branch was named something totally different.

Way #2

Another way to set remote tracking on a branch is to simply use the git branch -u option. Running

git branch -u o/main foo

will set the foo branch to track o/main. If foo is currently checked out you can even leave it off:

git branch -u o/main

Let's see this other way of specifying remote tracking real quick...

git branch -u o/main foo; git commit; git push

Same as before, just a more explicit command. Sweet!

Ok! For this level let's push work onto the main branch on remote while not checked out on main locally. You should instead create a branch named side which the goal diagram will show.

Level: Git push arguments

Push arguments

Great! Now that you know about remote tracking branches we can start to uncover some of the mystery behind how git push, fetch, and pull work. We're going to tackle one command at a time but the concepts between them are very similar.

First we'll look at git push. You learned in the remote tracking lesson that git figured out the remote and the branch to push to by looking at the properties of the currently checked out branch (the remote that it "tracks"). This is the behavior with no arguments specified, but git push can optionally take arguments in the form of:

git push <remote> <place>

What is a <place> parameter you say? We'll dive into the specifics soon, but first an example. Issuing the command:

git push origin main

translates to this in English:

Go to the branch named "main" in my repository, grab all the commits, and then go to the branch "main" on the remote named "origin". Place whatever commits are missing on that branch and then tell me when you're done.

By specifying main as the "place" argument, we told git where the commits will come from and where the commits will go. It's essentially the "place" or "location" to synchronize between the two repositories.

Keep in mind that since we told git everything it needs to know (by specifying both arguments), it totally ignores where we are checked out!

Let's see an example of specifying the arguments. Note the location where we are checked out in this example.

git checkout C0; git push origin main

There we go! main got updated on the remote since we specified those arguments.

What if we hadn't specified the arguments? What would happen?

git checkout C0; git push

The command fails (as you can see), since HEAD is not checked out on a remote-tracking branch.

Ok, for this level let's update both foo and main on the remote. The twist is that git checkout is disabled for this level!

Note: The remote branches are labeled with o/ prefixes because the full origin/ label does not fit in our UI. Don't worry about this... simply use origin as the name of the remote like normal.

Level: Git push arguments -- Expanded!

<place> argument details

Remember from the previous lesson that when we specified main as the place argument for git push, we specified both the source of where the commits would come from and the destination of where the commits would go.

You might then be wondering -- what if we wanted the source and destination to be different? What if you wanted to push commits from the foo branch locally onto the bar branch on remote?

Well unfortunately that's impossible in git... just kidding! Of course it's possible :)... git has tons and tons of flexibility (almost too much).

Let's see how in the next slide...

In order to specify both the source and the destination of <place>, simply join the two together with a colon:

git push origin <source>:<destination>

This is commonly referred to as a colon refspec. Refspec is just a fancy name for a location that git can figure out (like the branch foo or even just HEAD~1).

Once you are specifying both the source and destination independently, you can get quite fancy and precise with remote commands. Let's see a demo!

Remember, source is any location that git will understand:

git push origin foo^:main

Woah! That's a pretty trippy command but it makes sense -- git resolved foo^ into a location, uploaded whatever commits that weren't present yet on the remote, and then updated destination.

What if the destination you want to push doesn't exist? No problem! Just give a branch name and git will create the branch on the remote for you.

git push origin main:newBranch

Sweet, that's pretty slick :D

For this level, try to get to the end goal state shown in the visualization, and remember the format of:

<source>:<destination>

Level: Fetch arguments

Git fetch arguments

So we've just learned all about git push arguments, this cool <place> parameter, and even colon refspecs (<source>:<destination>). Can we use all this knowledge for git fetch as well?

You betcha! The arguments for git fetch are actually very, very similar to those for git push. It's the same type of concepts but just applied in the opposite direction (since now you are downloading commits rather than uploading).

Let's go over the concepts one at a time...

The <place> parameter

If you specify a place with git fetch like in the following command:

git fetch origin foo

Git will go to the foo branch on the remote, grab all the commits that aren't present locally, and then plop them down onto the o/foo branch locally.

Let's see this in action (just as a refresher).

By specifying a place...

git fetch origin foo

We download only the commits from foo and place them on o/foo.

You might be wondering -- why did git plop those commits onto the o/foo remote branch rather than just plopping them onto my local foo branch? I thought the <place> parameter is a place that exists both locally and on the remote?

Well git makes a special exception in this case because you might have work on the foo branch that you don't want to mess up!! This ties into the earlier lesson on git fetch -- it doesn't update your local non-remote branches, it only downloads the commits (so you can inspect / merge them later).

"Well in that case, what happens if I explicitly define both the source and destination with <source>:<destination>?"

If you feel passionate enough to fetch commits directly onto a local branch, then yes you can specify that with a colon refspec. You can't fetch commits onto a branch that is checked out, but otherwise git will allow this.

Here is the only catch though -- <source> is now a place on the remote and <destination> is a local place to put those commits. It's the exact opposite of git push, and that makes sense since we are transferring data in the opposite direction!

That being said, developers rarely do this in practice. I'm introducing it mainly as a way to conceptualize how fetch and push are quite similar, just in opposite directions.

Let's see this craziness in action:

git fetch origin C2:bar

Wow! See, git resolved C2 as a place on the origin and then downloaded those commits to bar (which was a local branch).

What if the destination doesn't exist before I run the command? Let's see the last slide but without bar existing beforehand.

git fetch origin C2:bar

See, it's JUST like git push. Git made the destination locally before fetching, just like git will make the destination on remote before pushing (if it doesn't exist).

No args?

If git fetch receives no arguments, it just downloads all the commits from the remote onto all the remote branches...

git fetch

Pretty simple, but worth going over just once.

Ok, enough talking! To finish this level, fetch just the specified commits in the goal visualization. Get fancy with those commands!

You will have to specify the source and destination for both fetch commands. Pay attention to the goal visualization since the IDs may be switched around!

Level: Source of nothing

Oddities of <source>

Git abuses the <source> parameter in two weird ways. These two abuses come from the fact that you can technically specify "nothing" as a valid source for both git push and git fetch. The way you specify nothing is via an empty argument:

Let's see what these do...

What does pushing "nothing" to a remote branch do? It deletes it!

git push origin :foo

There, we successfully deleted the foo branch on remote by pushing the concept of "nothing" to it. That kinda makes sense...

Finally, fetching "nothing" to a place locally actually makes a new branch.

git fetch origin :bar

Very odd / bizarre, but whatever. That's git for you!

This is a quick level -- just delete one remote branch and create a new branch with git fetch to finish!

Level: Pull arguments

Git pull arguments

Now that you know pretty much everything there is to know about arguments for git fetch and git push, there's almost really nothing left to cover for git pull :)

That's because git pull at the end of the day is really just shorthand for a fetch followed by merging in whatever was just fetched. You can think of it as running git fetch with the same arguments specified and then merging in where those commits ended up.

This applies even when you use crazy-complicated arguments as well. Let's see some examples:

Here are some equivalent commands in git:

git pull origin foo is equal to:

git fetch origin foo; git merge o/foo

And...

git pull origin bar:bugFix is equal to:

git fetch origin bar:bugFix; git merge bugFix

See? git pull is really just shorthand for fetch + merge, and all git pull cares about is where the commits ended up (the destination argument that it figures out during fetch).

Lets see a demo:

If we specify the place to fetch, everything happens as before with fetch but we merge in whatever was just fetched.

git pull origin main

See! by specifying main we downloaded commits onto o/main just as normal. Then we merged o/main to our currently checked out location which is not the local branch main. For this reason it can actually make sense to run git pull multiple times (with the same args) from different locations in order to update multiple branches.

Does it work with source and destination too? You bet! Let's see that:

git pull origin main:foo

Wow, that's a TON in one command. We created a new branch locally named foo, downloaded commits from remote's main onto that branch foo, and then merged that branch into our currently checked out branch bar. It's over 9000!!!

Ok to finish up, attain the state of the goal visualization. You'll need to download some commits, make some new branches, and merge those branches into other branches, but it shouldn't take many commands :P

Level Sequence: Moving Work Around

"Git" comfortable with modifying the source tree :P

Level: Cherry-pick Intro

Moving Work Around

So far we've covered the basics of git -- committing, branching, and moving around in the source tree. Just these concepts are enough to leverage 90% of the power of git repositories and cover the main needs of developers.

That remaining 10%, however, can be quite useful during complex workflows (or when you've gotten yourself into a bind). The next concept we're going to cover is "moving work around" -- in other words, it's a way for developers to say "I want this work here and that work there" in precise, eloquent, flexible ways.

This may seem like a lot, but it's a simple concept.

Git Cherry-pick

The first command in this series is called git cherry-pick. It takes on the following form:

It's a very straightforward way of saying that you would like to copy a series of commits below your current location (HEAD). I personally love cherry-pick because there is very little magic involved and it's easy to understand.

Let's see a demo!

Here's a repository where we have some work in branch side that we want to copy to main. This could be accomplished through a rebase (which we have already learned), but let's see how cherry-pick performs.

git cherry-pick C2 C4

That's it! We wanted commits C2 and C4 and git plopped them down right below us. Simple as that!

To complete this level, simply copy some work from the three branches shown into main. You can see which commits we want by looking at the goal visualization.

Level: Interactive Rebase Intro

Git Interactive Rebase

Git cherry-pick is great when you know which commits you want (and you know their corresponding hashes) -- it's hard to beat the simplicity it provides.

But what about the situation where you don't know what commits you want? Thankfully git has you covered there as well! We can use interactive rebasing for this -- it's the best way to review a series of commits you're about to rebase.

Let's dive into the details...

All interactive rebase means Git is using the rebase command with the -i option.

If you include this option, git will open up a UI to show you which commits are about to be copied below the target of the rebase. It also shows their commit hashes and messages, which is great for getting a bearing on what's what.

For "real" git, the UI window means opening up a file in a text editor like vim. For our purposes, I've built a small dialog window that behaves the same way.

When the interactive rebase dialog opens, you have the ability to do two things in our educational application:

It is worth mentioning that in the real git interactive rebase you can do many more things like squashing (combining) commits, amending commit messages, and even editing the commits themselves. For our purposes though we will focus on these two operations above.

Great! Let's see an example.

When you hit the button, an interactive rebase window will appear. Reorder some commits around (or feel free to unpick some) and see the result!

git rebase -i HEAD~4 --aboveAll

Boom! Git copied down commits in the exact same way you specified through the UI.

To finish this level, do an interactive rebase and achieve the order shown in the goal visualization. Remember you can always undo or reset to fix mistakes :D

Level Sequence: A Mixed Bag

A mixed bag of Git techniques, tricks, and tips

Level: Grabbing Just 1 Commit

Locally stacked commits

Here's a development situation that often happens: I'm trying to track down a bug but it is quite elusive. In order to aid in my detective work, I put in a few debug commands and a few print statements.

All of these debugging / print statements are in their own commits. Finally I track down the bug, fix it, and rejoice!

Only problem is that I now need to get my bugFix back into the main branch. If I simply fast-forwarded main, then main would get all my debug statements which is undesirable. There has to be another way...

We need to tell git to copy only one of the commits over. This is just like the levels earlier on moving work around -- we can use the same commands:

To achieve this goal.

This is a later level so we will leave it up to you to decide which command you want to use, but in order to complete the level, make sure main receives the commit that bugFix references.

Level: Juggling Commits

Juggling Commits

Here's another situation that happens quite commonly. You have some changes (newImage) and another set of changes (caption) that are related, so they are stacked on top of each other in your repository (aka one after another).

The tricky thing is that sometimes you need to make a small modification to an earlier commit. In this case, design wants us to change the dimensions of newImage slightly, even though that commit is way back in our history!!

We will overcome this difficulty by doing the following:

There are many ways to accomplish this overall goal (I see you eye-ing cherry-pick), and we will see more of them later, but for now let's focus on this technique. Lastly, pay attention to the goal state here -- since we move the commits twice, they both get an apostrophe appended. One more apostrophe is added for the commit we amend, which gives us the final form of the tree

That being said, I can compare levels now based on structure and relative apostrophe differences. As long as your tree's main branch has the same structure and relative apostrophe differences, I'll give full credit.

Level: Juggling Commits #2

Juggling Commits #2

If you haven't completed Juggling Commits #1 (the previous level), please do so before continuing

As you saw in the last level, we used rebase -i to reorder the commits. Once the commit we wanted to change was on top, we could easily --amend it and re-order back to our preferred order.

The only issue here is that there is a lot of reordering going on, which can introduce rebase conflicts. Let's look at another method with git cherry-pick.

Remember that git cherry-pick will plop down a commit from anywhere in the tree onto HEAD (as long as that commit isn't an ancestor of HEAD).

Here's a small refresher demo:

git cherry-pick C2

Nice! Let's move on.

So in this level, let's accomplish the same objective of amending C2 once but avoid using rebase -i. I'll leave it up to you to figure it out! :D

Remember, the exact number of apostrophe's (') on the commit are not important, only the relative differences. For example, I will give credit to a tree that matches the goal tree but has one extra apostrophe everywhere.

Level: Git Tags

Git Tags

As you have learned from previous lessons, branches are easy to move around and often refer to different commits as work is completed on them. Branches are easily mutated, often temporary, and always changing.

If that's the case, you may be wondering if there's a way to permanently mark historical points in your project's history. For things like major releases and big merges, is there any way to mark these commits with something more permanent than a branch?

You bet there is! Git tags support this exact use case -- they (somewhat) permanently mark certain commits as "milestones" that you can then reference like a branch.

More importantly though, they never move as more commits are created. You can't "check out" a tag and then complete work on that tag -- tags exist as anchors in the commit tree that designate certain spots.

Let's see what tags look like in practice.

Let's try making a tag at C1 which is our version 1 prototype.

git tag v1 C1

There! Quite easy. We named the tag v1 and referenced the commit C1 explicitly. If you leave the commit off, git will just use whatever HEAD is at.

For this level just create the tags in the goal visualization and then check v1 out. Notice how you go into detached HEAD state -- this is because you can't commit directly onto the v1 tag.

In the next level we'll examine a more interesting use case for tags.

Level: Git Describe

Git Describe

Because tags serve as such great "anchors" in the codebase, git has a command to describe where you are relative to the closest "anchor" (aka tag). And that command is called git describe!

Git describe can help you get your bearings after you've moved many commits backwards or forwards in history; this can happen after you've completed a git bisect (a debugging search) or when sitting down at the computer of a coworker who just got back from vacation.

Git describe takes the form of:

git describe <ref>

Where <ref> is anything git can resolve into a commit. If you don't specify a ref, git just uses where you're checked out right now (HEAD).

The output of the command looks like:

<tag>_<numCommits>_g<hash>

Where tag is the closest ancestor tag in history, numCommits is how many commits away that tag is, and <hash> is the hash of the commit being described.

Let's look at a quick example. For this tree below:

git tag v2 C3

The command git describe main would output:

v1_2_gC2

Whereas git describe side would output:

v2_1_gC4

That's pretty much all there is to git describe! Try describing a few of the locations in this level to get a feel for the command.

Once you're ready, just go ahead and commit once to finish the level. We're giving you a freebie :P

Level Sequence: Advanced Topics

For the truly brave!

Level: Rebasing over 9000 times

Rebasing Multiple Branches

Man, we have a lot of branches going on here! Let's rebase all the work from these branches onto main.

Upper management is making this a bit trickier though -- they want the commits to all be in sequential order. So this means that our final tree should have C7' at the bottom, C6' above that, and so on, all in order.

If you mess up along the way, feel free to use reset to start over again. Be sure to check out our solution and see if you can do it in fewer commands!

Level: Multiple parents

Specifying Parents

Like the ~ modifier, the ^ modifier also accepts an optional number after it.

Rather than specifying the number of generations to go back (what ~ takes), the modifier on ^ specifies which parent reference to follow from a merge commit. Remember that merge commits have multiple parents, so the path to choose is ambiguous.

Git will normally follow the "first" parent upwards from a merge commit, but specifying a number with ^ changes this default behavior.

Enough talking, let's see it in action.

Here we have a merge commit. If we checkout main^ without the modifier, we will follow the first parent after the merge commit.

(In our visuals, the first parent is positioned directly above the merge commit.)

git checkout main^

Easy -- this is what we are all used to.

Now let's try specifying the second parent instead...

git checkout main^2

See? We followed the other parent upwards.

The ^ and ~ modifiers can make moving around a commit tree very powerful:

git checkout HEAD~; git checkout HEAD^2; git checkout HEAD~2

Lightning fast!

Even crazier, these modifiers can be chained together! Check this out:

git checkout HEAD~^2~2

The same movement as before, but all in one command.

Put it to practice

To complete this level, create a new branch at the specified destination.

Obviously it would be easy to specify the commit directly (with something like C6), but I challenge you to use the modifiers we talked about instead!

Level: Branch Spaghetti

Branch Spaghetti

WOAHHHhhh Nelly! We have quite the goal to reach in this level.

Here we have main that is a few commits ahead of branches one two and three. For whatever reason, we need to update these three other branches with modified versions of the last few commits on main.

Branch one needs a re-ordering of those commits and an exclusion/drop of C5. Branch two just needs a pure reordering of the commits, and three only needs one commit transferred!

We will let you figure out how to solve this one -- make sure to check out our solution afterwards with show solution.