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.
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]
.
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:
bugFix
bugFix
branch with git checkout bugFix
main
with git checkout
bugFix
into main
with git merge
Remember, you can always re-display this dialog with "objective"!
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
bugFix
Good luck!
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 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.
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:
^
~<num>
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!
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.
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.
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
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.
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.
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:
First and foremost, remotes serve as a great backup! Local git repositories have the ability to restore files to a previous state (as you know), but all that information is stored locally. By having copies of your git repository on other computers, you can lose all your local data and still pick up where you left off.
More importantly, remotes make coding social! Now that a copy of your project is hosted elsewhere, your friends can contribute to your project (or pull in your latest changes) very easily.
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!
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.
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.
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:
<remote name>/<branch name>
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.
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.
git fetch
performs two main steps, and two main steps only. It:
o/main
)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://
).
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!
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:
git cherry-pick o/main
git rebase o/main
git merge o/main
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
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!
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!
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:
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.)
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.
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.
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:
main
, andLet'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:
side1
side2
and side3
:O intense! good luck, completing this level is a big step.
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.
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:
o/main
and then merged into the main
branch. The implied target of the merge is determined from this connection.main
branch was pushed onto the remote's main
branch (which was then represented by o/main
locally). The destination of the push is determined from the connection between main
and o/main
.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"
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.
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.
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.
<place>
argument detailsRemember 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>
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...
<place>
parameterIf 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!
<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:
git push origin :side
git fetch origin :bugFix
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!
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
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.
The first command in this series is called git cherry-pick
. It takes on the following form:
git cherry-pick <Commit1> <Commit2> <...>
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.
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:
pick
button next to it being active. To drop a commit, toggle off its pick
button.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
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:
git rebase -i
git cherry-pick
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.
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:
git rebase -i
git commit --amend
to make the slight modificationgit rebase -i
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.
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.
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.
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
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!
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.
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!
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
.