Posted on • Edited on • Originally published atriedmann.dev
Learn git concepts, not commands
An interactive git tutorial meant to teach you how git works, not just which commands to execute.
So, you want to use git right?
But you don't just want to learn commands, you want to understand what you're using?
Then this is meant for you!
Let's get started!
Based on the general concept from Rachel M. Carmena's blog post onHow to teach Git.
While I find many git tutorials on the internet to be too focused on what to do instead of how things work, the most invaluable resource for both (and source for this tutorial!) are thegit Book andReference page.
So if you're still interested when you're done here, go check those out! I do hope the somewhat different concept of this tutorial will aid you in understanding all the other git features detailed there.
- Overview
- Getting aRemote Repository
- Making changes
- Branching
- Merging
- Rebasing
- Updating theDev Environment with remote changes
- Cherry-picking
- Rewriting history
- Reading history
Overview
In the picture below you see four boxes. One of them stands alone, while the other three are grouped together in what I'll call yourDevelopment Environment.
We'll start with the one that's on it's own though. TheRemote Repository is where you send your changes when you want to share them with other people, and where you get their changes from. If you've used other version control systems there's nothing interesting about that.
TheDevelopment Environment is what you have on your local machine.
The three parts of it are yourWorking Directory, theStaging Area and theLocal Repository. We'll learn more about those as we start using git.
Choose a place in which you want to put yourDevelopment Environment.
Just go to your home folder, or where ever you like to put your projects. You don't need to create a new folder for yourDev Environment though.
Getting aRemote Repository
Now we want to grab aRemote Repository and put what's in it onto your machine.
I'd suggest we use this one (https://github.com/UnseenWizzard/git_training.git if you're not already reading this on github).
To do that I can use
git clone https://github.com/UnseenWizzard/git_training.git
But as following this tutorial will need you to get the changes you make in yourDev Environment back to theRemote Repository, and github doesn't just allow anyone to do that to anyone's repo, you'll best create afork of it right now. There's a button to do that on the top right of this page.
Now that you have a copy of myRemote Repository of your own, it's time to get that onto your machine.
For that we usegit clone https://github.com/{YOUR USERNAME}/git_training.git
As you can see in the diagram below, this copies theRemote Repository into two places, yourWorking Directory and theLocal Repository.
Now you see how git isdistributed version control. TheLocal Repository is a copy of theRemote one, and acts just like it. The only difference is that you don't share it with anyone.
Whatgit clone
also does, is create a new folder wherever you called it. There should be agit_training
folder now. Open it.
Adding new things
Someone already put a file into theRemote Repository. It'sAlice.txt
, and kind of lonely there. Let's create a new file and call itBob.txt
.
What you've just done is add the file to yourWorking Directory.
There's two kinds of files in yourWorking Directory:tracked files that git knows about anduntracked files that git doesn't know about (yet).
To see what's going on in yourWorking Directory rungit status
, which will tell you what branch you're on, whether yourLocal Repository is different from theRemote and the state oftracked anduntracked files.
You'll see thatBob.txt
is untracked, andgit status
even tells you how to change that.
In the picture below you can see what happens when you follow the advice and executegit add Bob.txt
: You've added the file to theStaging Area, in which you collect all the changes you wish to put intoRepository
When you have added all your changes (which right now is only adding Bob), you're ready tocommit what you just did to theLocal Repository.
The collected changes that youcommit are some meaningful chunk of work, so when you now rungit commit
a text editor will open and allow you to write a message telling everything what you just did. When you save and close the message file, yourcommit is added to theLocal Repository.
You can also add yourcommit message right there in the command line if you callgit commit
like this:git commit -m "Add Bob"
. But because you want to writegood commit messages you really should take your time and use the editor.
Now your changes are in your local repository, which is a good place for the to be as long as no one else needs them or you're not yet ready to share them.
In order to share your commits with theRemote Repository you need topush
them.
Once you rungit push
the changes will be sent to theRemote Repository. In the diagram below you see the state after yourpush
.
Making changes
So far we've only added a new file. Obviously the more interesting part of version control is changing files.
Have a look atAlice.txt
.
It actually contains some text, butBob.txt
doesn't, so lets change that and putHi!! I'm Bob. I'm new here.
in there.
If you rungit status
now, you'll see thatBob.txt
ismodified.
In that state the changes are only in yourWorking Directory.
If you want to see what has changed in yourWorking Directory you can rungit diff
, and right now see this:
diff --git a/Bob.txt b/Bob.txtindex e69de29..3ed0e1b 100644--- a/Bob.txt+++ b/Bob.txt@@ -0,0 +1 @@+Hi!! I'm Bob. I'm new here.
Go ahead andgit add Bob.txt
like you've done before. As we know, this moves your changes to theStaging Area.
I want to see the changes we juststaged, so let's show thegit diff
again! You'll notice that this time the output is empty. This happens becausegit diff
operates on the changes in yourWorking Directory only.
To show what changes arestaged already, we can usegit diff --staged
and we'll see the same diff output as before.
I just noticed that we put two exclamation marks after the 'Hi'. I don't like that, so lets changeBob.txt
again, so that it's just 'Hi!'
If we now rungit status
we'll see that there's two changes, the one we alreadystaged where we added text, and the one we just made, which is still only in the working directory.
We can have a look at thegit diff
between theWorking Directory and what we've already moved to theStaging Area, to show what has changed since we last felt ready tostage our changes for acommit.
diff --git a/Bob.txt b/Bob.txtindex 8eb57c4..3ed0e1b 100644--- a/Bob.txt+++ b/Bob.txt@@ -1 +1 @@-Hi!! I'm Bob. I'm new here.+Hi! I'm Bob. I'm new here.
As the change is what we wanted, let'sgit add Bob.txt
to stage the current state of the file.
Now we're ready tocommit
what we just did. I went withgit commit -m "Add text to Bob"
because I felt for such a small change writing one line would be enough.
As we know, the changes are now in theLocal Repository.
We might still want to know what change we justcommitted and what was there before.
We can do that by comparing commits.
Every commit in git has a unique hash by which it is referenced.
If we have a look at thegit log
we'll not only see a list of all the commits with theirhash as well asAuthor andDate, we also see the state of ourLocal Repository and the latest local information aboutremote branches.
Right now thegit log
looks something like this:
commit 87a4ad48d55e5280aa608cd79e8bce5e13f318dc (HEAD -> master)Author: {YOU} <{YOUR EMAIL}>Date: Sun Jan 27 14:02:48 2019 +0100 Add text to Bobcommit 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e (origin/master, origin/HEAD)Author: {YOU} <{YOUR EMAIL}>Date: Sun Jan 27 13:35:41 2019 +0100 Add Bobcommit 71a6a9b299b21e68f9b0c61247379432a0b6007c Author: UnseenWizzard <nicola.riedmann@live.de>Date: Fri Jan 25 20:06:57 2019 +0100 Add Alicecommit ddb869a0c154f6798f0caae567074aecdfa58c46Author: Nico Riedmann <UnseenWizzard@users.noreply.github.com>Date: Fri Jan 25 19:25:23 2019 +0100 Add Tutorial Text Changes to the tutorial are all squashed into this commit on master, to keep the log free of clutter that distracts from the tutorial See the tutorial_wip branch for the actual commit history
In there we see a few interesting things:
- The first two commits are made by me.
- Your initial commit to add Bob is the currentHEAD of themaster branch on theRemote Repository. We'll look at this again when we talk about branches and getting remote changes.
- The latest commit in theLocal Repository is the one we just made, and now we know its hash.
Note that the actual commit hashes will be different for you. If you want to know how exactly git arrives at those revision IDs have a look atthis interesting article.
To compare that commit and the one one before we can dogit diff <commit>^!
, where the^!
tells git to compare to the commit one before. So in this case I rungit diff 87a4ad48d55e5280aa608cd79e8bce5e13f318dc^!
We can also dogit diff 8af2ff2a8f7c51e2e52402ecb7332aec39ed540e 87a4ad48d55e5280aa608cd79e8bce5e13f318dc
for the same result and in general to compare any two commits. Note that the format here isgit diff <from commit> <to commit>
, so our new commit comes second.
In the diagram below you again see the different stages of a change, and the diff commands that apply to where a file currently is.
Now that we're sure we made the change we wanted, go ahead andgit push
.
Branching
Another thing that makes git great, is the fact that working with branches is really easy and integral part of how you work with git.
In fact we've been working on a branch since we've started.
When youclone
theRemote Repository yourDev Environment automatically starts on the repositories main ormaster branch.
Most work-flows with git include making your changes on abranch, before youmerge
them back intomaster.
Usually you'll be working on your ownbranch, until you're done and confident in your changes which can then be merged into themaster.
Many git repository managers likeGitLab andGitHub also allow for branches to beprotected, which means that not everyone is allowed to just
push
changes there. There themaster is usually protected by default.
Don't worry, we'll get back to all of these things in more detail when we need them.
Right now we want to create a branch to make some changes there. Maybe you just want to try something on your own and not mess with the working state on yourmaster branch, or you're not allowed topush
tomaster.
Branches live in theLocal andRemote Repository. When you create a new branch, the branches contents will be a copy of the currently committed state of whatever branch you are currently working on.
Let's make some change toAlice.txt
! How about we put some text on the second line?
We want to share that change, but not put it onmaster right away, so let's create a branch for it usinggit branch <branch name>
.
To create a new branch calledchange_alice
you can rungit branch change_alice
.
This adds the new branch to theLocal Repository.
While yourWorking Directory andStaging Area don't really care about branches, you alwayscommit
to the branch you are currently on.
You can think ofbranches in git as pointers, pointing to a series of commits. When youcommit
, you add to whatever you're currently pointing to.
Just adding a branch, doesn't directly take you there, it just creates such a pointer.
In fact the state yourLocal Repository is currently at, can be viewed as another pointer, calledHEAD, which points to what branch and commit you are currently at.
If that sounds complicated the diagrams below will hopefully help to clear things up a bit:
To switch to our new branch you will have to usegit checkout change_alice
. What this does is simply to move theHEAD to the branch you specify.
As you'll usually want switch to a branch right after creating it, there is the convenient
-b
option available for thecheckout
command, which allows you to just directlycheckout
anew branch, so you don't have to create it beforehand.So to create and switch to our
change_alice
branch, we could also just have calledgit checkout -b change_alice
.
You'll notice that yourWorking Directory hasn't changed. That we'vemodifiedAlice.txt
is not related to the branch we're on yet.
Now you canadd
andcommit
the change toAlice.txt
just like we did on themaster before, which willstage (at which point it's still unrelated to the branch) and finallycommit your change to thechange_alice
branch.
There's just one thing you can't do yet. Try togit push
your changes to theRemote Repository.
You'll see the following error and - as git is always ready to help - a suggestion how to resolve the issue:
fatal: The current branch change_alice has no upstream branch.To push the current branch and set the remote as upstream, use git push --set-upstream origin change_alice
But we don't just want to blindly do that. We're here to understand what's actually going on. So what areupstream branches andremotes?
Remember when wecloned
theRemote Repository a while ago? At that point it didn't only contain this tutorial andAlice.txt
but actually two branches.
Themaster we just went ahead and started working on, and one I called "tutorial_wip" on which I commit all the changes I make to this tutorial.
When we copied the things in theRemote Repository into yourDev Environment a few extra steps happened under the hood.
Git setup theremote of yourLocal Repository to be theRemote Repository you cloned and gave it the default nameorigin
.
YourLocal Repository can track severalremotes and they can have different names, but we'll stick to the
origin
and nothing else for this tutorial.
Then it copied the two remote branches into yourLocal Repository and finally itchecked out
master for you.
When doing that another implicit step happens. When youcheckout
a branch name that has an exact match in the remote branches, you will get a newlocal branch that is linked to theremote branch. Theremote branch is theupstream branch of yourlocal one.
In the diagrams above you can see just the local branches you have. You can see that list of local branches by runninggit branch
.
If you want to also see theremote branches yourLocal Repository knows, you can usegit branch -a
to list all of them.
Now we can call the suggestedgit push --set-upstream origin change_alice
, andpush
the changes on our branch to a newremote. This will create achange_alice
branch on theRemote Repository and set ourlocalchange_alice
to track that new branch.
There is another option if we actually want our branch to track something that already exists on theRemote Repository. Maybe a colleague has already pushed some changes, while we were working on something related on our local branch, and we'd like to integrate the two. Then we could just set theupstream for our
change_alice
branch to a newremote by usinggit branch --set-upstream-to=origin/change_alice
and from there on track theremote branch.
After that went through have a look at yourRemote Repository on github, your branch will be there, ready for other people to see and work with.
We'll get to how you can get other people's changes into yourDev Environment soon, but first we'll work a bit more with branches, to introduce all the concepts that also come into play when we get new things from theRemote Repository.
Merging
As you and everyone else will generally be working on branches, we need to talk about how to get changes from one branch into the other bymerging them.
We've just changedAlice.txt
on thechange_alice
branch, and I'd say we're happy with the changes we made.
If you go andgit checkout master
, thecommit
we made on the other branch will not be there. To get the changes into master we need tomerge
thechange_alice
branchinto master.
Note that you alwaysmerge
some branchinto the one you're currently at.
Fast-Forward merging
As we've alreadychecked out
master, we can nowgit merge change_alice
.
As there are no otherconflicting changes toAlice.txt
, and we've changed nothing onmaster, this will go through without a hitch in what is called afast forward merge.
In the diagrams below, you can see that this just means that themaster pointer can simply be advanced to where thechange_alice one already is.
The first diagram shows the state before ourmerge
,master is still at the commit it was, and on the other branch we've made one more commit.
The second diagram shows what has changed with ourmerge
.
Merging divergent branches
Let's try something more complex.
Add some text on a new line toBob.txt
onmaster and commit it.
Thengit checkout change_alice
, changeAlice.txt
and commit.
In the diagram below you see how our commit history now looks. Bothmaster andchange_alice
originated from the same commit, but since then theydiverged, each having their own additional commit.
If you nowgit merge change_alice
a fast-forward merge is not possible. Instead your favorite text editor will open and allow you to change the message of themerge commit
git is about to make in order to get the two branches back together. You can just go with the default message right now. The diagram below shows the state of our git history after we themerge
.
The new commit introduces the changes that we've made on thechange_alice
branch into master.
As you'll remember from before, revisions in git, aren't only a snapshot of your files but also contain information on where they came from from. Eachcommit
has one or more parent commits. Our newmerge
commit, has both the last commit frommaster and the commit we made on the other branch as it's parents.
Resolving conflicts
So far our changes haven't interfered with each other.
Let's introduce aconflict and thenresolve it.
Create andcheckout
a new branch. You know how, but maybe try usinggit checkout -b
to make your live easier.
I've called minebobby_branch
.
On the branch we'll make a change toBob.txt
.
The first line should still beHi!! I'm Bob. I'm new here.
. Change that toHi!! I'm Bobby. I'm new here.
Stage and thencommit
your change, before youcheckout
master again. Here we'll change that same line toHi!! I'm Bob. I've been here for a while now.
andcommit
your change.
Now it's time tomerge
the new branch intomaster.
When you try that, you'll see the following output
Auto-merging Bob.txt CONFLICT (content): Merge conflict in Bob.txt Automatic merge failed; fix conflicts and then commit the result.
The same line has changed on both of the branches, and git can't handle this on it's own.
If you rungit status
you'll get all the usual helpful instructions on how to continue.
First we have to resolve the conflict by hand.
For an easy conflict like this one your favorite text editor will do fine. For merging large files with lots of changes a more powerful tool will make your life much easier, and I'd assume your favorite IDE comes with version control tools and a nice view for merging.
If you openBob.txt
you'll see something similar to this (I've truncated whatever we might have put on the second line before):
<<<<<<< HEAD Hi! I'm Bob. I've been here for a while now. ======= Hi! I'm Bobby. I'm new here. >>>>>>> bobby_branch [... whatever you've put on line 2]
On top you see what has changed inBob.txt
on the current HEAD, below you see what has changed in the branch we're merging in.
To resolve the conflict by hand, you'll just need to make sure that you end up with some reasonable content and without the special lines git has introduced to the file.
So go ahead and change the file to something like this:
Hi! I'm Bobby. I've been here for a while now. [...]
From here what we're doing is exactly what we'd do for any changes.
Westage them when weadd Bob.txt
, and then wecommit
.
We already know the commit for the changes we've made to resolve the conflict. It's themerge commit that is always present when merging.
Should you ever realize in the middle of resolving conflicts that you actually don't want to follow through with themerge
, you can justabort
it by runninggit merge --abort
.
Rebasing
Git has another clean way to integrate changes between two branches, which is calledrebase
.
We still recall that a branch is always based on another. When you create it, youbranch away from somewhere.
In our simple merging example we branched frommaster at a specific commit, then committed some changes on bothmaster and thechange_alice
branch.
When a branch is diverging from the one it's based on and you want to integrate the latest changes back into your current branch,rebase
offers a cleaner way of doing that than amerge
would.
As we've seen, amerge
introduces amerge commit in which the two histories get integrated again.
Viewed simply, rebasing just changes the point in history (the commit) your branch is based on.
To try that out, let's first checkout themaster branch again, then create/checkout a new branch based on it.
I called mineadd_patrick
and I added a newPatrick.txt
file and committed that with the message 'Add Patrick'.
When you've added a commit to the branch, get back tomaster, make a change and commit it. I added some more text toAlice.txt
.
Like in our merging example the history of these two branches diverges at a common ancestor as you can see in the diagram below.
Now let'scheckout add_patrick
again, and get that change that was made onmaster into the branch we work on!
When wegit rebase master
, we re-base ouradd_patrick
branch on the current state of themaster branch.
The output of that command gives us a nice hint at what is happening in it:
First, rewinding head to replay your work on top of it... Applying: Add Patrick
As we rememberHEAD is the pointer to the current commit we're at in ourDev Environment.
It's pointing to the same place asadd_patrick
before the rebase starts. For the rebase, it then first moves back to the common ancestor, before moving to the current head of the branch we want to re-base ours on.
SoHEAD moves from the0cfc1d2 commit, to the7639f4b commit that is at the head ofmaster.
Then rebase applies every single commit we made on ouradd_patrick
branch to that.
To be more exact whatgit does after movingHEAD back to the common ancestor of the branches, is to store parts of every single commit you've made on the branch (thediff
of changes, and the commit text, author, etc.).
After that it does acheckout
of the latest commit of the branch you're rebasing on, and then applies each of the stored changedas a new commit on top of that.
So in our original simplified view, we'd assume that after therebase
the0cfc1d2 commit doesn't point to the common ancestor anymore in it's history, but points to the head of master.
In fact the0cfc1d2 commit is gone, and theadd_patrick
branch starts with a new0ccaba8 commit, that has the latest commit ofmaster as its ancestor.
We made it look, like ouradd_patrick
was based on the currentmaster not an older version of it, but in doing so we re-wrote the history of the branch.
At the end of this tutorial we'll learn a bit more about re-writing history and when it's appropriate and inappropriate to do so.
Rebase
is an incredibly powerful tool when you're working on your own development branch which is based on a shared branch, e.g. themaster.
Using rebase you can make sure that you frequently integrate the changes other people make and push tomaster, while keeping a clean linear history that allows you to do afast-forward merge
when it's time to get your work into the shared branch.
Keeping a linear history also makes reading or looking at (try outgit log --graph
or take a look at the branch view ofGitHub orGitLab) commit logs much more useful than having a history littered withmerge commits, usually just using the default text.
Resolving conflicts
Just like for amerge
you may run into conflicts, if you run into two commits changing the same parts of a file.
However when you encounter a conflict during arebase
you don't fix it in an extramerge commit, but can simply resolve it in the commit that is currently being applied.
Again, basing your changes directly on the current state of the original branch.
Actually resolving conflicts while yourebase
is very similar to how you would for amerge
so refer back to that section if you're not sure anymore how to do it.
The only distinction is, that as you're not introducing amerge commit there is no need tocommit
your resolution. Simplyadd
the changes to theStaging Environment and thengit rebase --continue
. The conflict will be resolved in the commit that was just being applied.
As when merging, you can always stop and drop everything you've done so far when yougit rebase --abort
.
Updating theDev Environment with remote changes
So far we've only learned how to make and share changes.
That fits what you'll do if you're just working on your own, but usually there'll be a lot of people that do just the same, and we're gonna want to get their changes from theRemote Repository into ourDev Environment somehow.
Because it has been a while, lets have another look at the components of git:
Just like yourDev Environment everyone else working on the same source code has theirs.
All of theseDev Environments have their ownworking andstaged changes, that are at some pointcommitted
to theLocal Repository and finallypushed
to theRemote.
For our example, we'll use the online tools offered byGitHub, to simulate someone else making changes to theremote while we work.
Go to yourfork
of this repo ongithub.com and open theAlice.txt
file.
Find the edit button and make and commit a change via the website.
In this repository I have added a remote change toAlice.txt
on a branch calledfetching_changes_sample
, but in your version of the repository you can of course just change the file onmaster
.
Fetching Changes
We still remember that when yougit push
, you synchronize changes made to theLocal Repository into theRemote Repository.
To get changes made to theRemote into yourLocal Repository you usegit fetch
.
This gets any changes on the remote - so commits as well as branches - into yourLocal Repository.
Note that at this point, changes aren't integrated into the local branches and thus theWorking Directory andStaging Area yet.
If you rungit status
now, you'll see another great example of git commands telling you exactly what is going on:
git status On branch fetching_changes_sample Your branch is behind 'origin/fetching_changes_sample' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch)
Pulling Changes
As we have noworking orstaged changes, we could just executegit pull
now to get the changes from theRepository all the way into our working area.
Pulling will implicitly also
fetch
theRemote Repository, but sometimes it is a good idea to do afetch
on it's own.
For example when you want to synchronize any newremote branches, or when you want to make sure yourLocal Repository is up to date before you do agit rebase
on something likeorigin/master
.
Before wepull
, lets change a file locally to see what happens.
Lets also changeAlice.txt
in ourWorking Directory now!
If you now try to do agit pull
you'll see the following error:
git pull Updating df3ad1d..418e6f0 error: Your local changes to the following files would be overwritten by merge: Alice.txt Please commit your changes or stash them before you merge. Aborting
You can notpull
in any changes, while there are modifications to files in theWorking Directory that are also changed by the commits you'repull
ing in.
While one way around this is, to just get your changes to a point where you're confident in them,add
them to theStaging Environment, before you finallycommit
them, this is a good moment to learn about another great tool, thegit stash
.
Stashing changes
If at any point you have local changes that you do not yet want to put into a commit, or want to store somewhere while you try some different angle to solve a problem, you canstash
those changes away.
Agit stash
is basically a stack of changes on which you store any changes to theWorking Directory.
The commands you'll mostly use aregit stash
which places any modifications to theWorking Directory on the stash, andgit stash pop
which takes the latest change that was stashed and applies it the to theWorking Directory again.
Just like the stack commands it's named aftergit stash pop
removes the latest stashed change before applying it again.
If you want to keep the stashed changes, you can usegit stash apply
, which doesn't remove them from the stash before applying them.
To inspect you currentstash
you can usegit stash list
to list the individual entries, andgit stash show
to show the changes in the latest entry on thestash
.
Another nice convenience command is
git stash branch {BRANCH NAME}
, which creates a branch, starting from the HEAD at the moment you've stashed the changes, and applies the stashed changes to that branch.
Now that we know aboutgit stash
, lets run it to remove our local changes toAlice.txt
from theWorking Directory, so that we can go ahead andgit pull
the changes we've made via the website.
After that, let'sgit stash pop
to get the changes back.
As both the commit wepull
ed in and thestash
ed change modifiedAlice.txt
you wil have to resolve the conflict, just how you would in amerge
orrebase
.
When you're doneadd
andcommit
the change.
Pulling with Conflicts
Now that we've understood how tofetch
andpull
Remote Changes into ourDev Environment, it's time to create some conflicts!
Do notpush
the commit that changedAlice.txt
and head back to yourRemote Repository ongithub.com.
There we're also again going to changeAlice.txt
and commit the change.
Now there's actually two conflicts between ourLocal andRemote Repositories.
Don't forget to rungit fetch
to see the remote change withoutpull
ing it in right away.
If you now rungit status
you will see, that both branches have one commit on them that differs from the other.
git status On branch fetching_changes_sample Your branch and 'origin/fetching_changes_sample' have diverged, and have 1 and 1 different commits each, respectively. (use "git pull" to merge the remote branch into yours)
In addition we've changed the same file in both of those commits, to introduce amerge
conflict we'll have to resolve.
When yougit pull
while there is a difference between theLocal andRemote Repository the exact same thing happens as when youmerge
two branches.
Additionally, you can think of the relationship between branches on theRemote and the one in theLocal Repository as a special case of creating a branch based on another.
A local branch is based on a branches state on theRemote from the time you lastfetched
it.
Thinking that way, the two options you have to getremote changes make a lot of sense:
When yougit pull
theLocal andRemote version of a branch will bemerged
. Just likemerging
branches, this will introduce a _merge commit.
As anylocal branch is based on it's respectiveremote version, we can alsorebase
it, so that any changes we may have made locally, appear as if they were based on the latest version that is available in the _Remote Repository.
To do that, we can usegit pull --rebase
(or the shorthandgit pull -r
).
As detailed in the section onRebasing, there is a benefit in keeping a clean linear history, which is why I would strongly recommend that whenever yougit pull
you do agit pull -r
.
You can also tell git to use
rebase
instead ofmerge
as it's default strategy when yourgit pull
, by setting thepull.rebase
flag with a command like thisgit config --global pull.rebase true
.
If you haven't already rungit pull
when I first mentioned it a few paragraphs ago, let's now rungit pull -r
to get the remote changes while making it look like our new commit just happened after them.
Of course like with a normalrebase
(ormerge
) you'll have to resolve the conflict we introduced for thegit pull
to be done.
Cherry-picking
Congratulations! You've made it to the more advanced features!
By now you understand how to use all the typical git commands and more importantly how they work.
This will hopefully make the following concepts much simpler to understand than if I just told you what commands to type in.
So let's head right in an learn how to
cherry-pick
commits!
From earlier sections you still remember roughly what acommit
is made off, right?
And how when yourebase
a branch your commits are applied as new commits with the samechange set andmessage?
Whenever you want to just take a few choice changes from one branch and apply them to another branch, you want tocherry-pick
these commits and put them on your branch.
That is exactly whatgit cherry-pick
allows you to do with either single commits or a range of commits.
Just like during arebase
this will actually put the changes from these commits into a new commit on your current branch.
Lets have a look at an example each forcherry-pick
ing one or more commits:
The figure below shows three branches before we have done anything. Let's assume we really want to get some changes from theadd_patrick
branch into thechange_alice
branch. Sadly they haven't made it into master yet, so we can't justrebase
onto master to get those changes (along with any other changes on the other branch, that we might not even want).
So let's justgit cherry-pick
the commit63fc421.
The figure below visualizes what happens when we rungit cherry-pick 63fc421
As you can see, a new commit with the changes we wanted shows up on branch.
At this point note that like with any other kind of getting changes onto a branch that we've seen before, any conflicts that arise during a
cherry-pick
will have to beresolved by us, before the command can go through.Also like all other commands you can either
--continue
acherry-pick
when you've resolved conflicts, or decide to--abort
the command entirely.
The figure below visualizescherry-pick
ing a range of commits instead of a single one. You can simply do that by calling the command in the formgit cherry-pick <from>..<to>
or in our example below asgit cherry-pick 0cfc1d2..41fbfa7
.
Rewriting history
I'm repeating myself now, but you still remember
rebase
well enough right? Else quickly jump back to that section, before continuing here, as we'll use what we already know when learning about how change history!
As you know acommit
basically contains your changes, a message and few other things.
The 'history' of a branch is made up of all it's commits.
But lets say you've just made acommit
and then notice, that you've forgotten to add a file, or you made a typo and the change leaves you with broken code.
We'll briefly look at two things we could do to fix that, and make it look like it never happened.
Let's switch to a new branch withgit checkout -b rewrite_history
.
Now make some changes to bothAlice.txt
andBob.txt
, and thengit add Alice.txt
.
Thengit commit
using a message like "This is history" and you're done.
Wait, did I say we're done? No, you'll clearly see that we've made some mistakes here:
- We forgot to add the changes to
Bob.txt
- We didn't write agood commit message
Amending the last Commit
One way to fix both of these in one go would be toamend
the commit we've just made.
Amend
ing the latest commit basically works just like making a new one.
Before we do anything take a look at your latest commit, withgit show {COMMIT}
. Put either the commit hash (which you'll probably still see in your command line from thegit commit
call, or in thegit log
), or justHEAD.
Just like in thegit log
you'll see the message, author, date and of course changes.
Now let'samend
what we've done in that commit.
git add Bob.txt
to get the changes to theStaging Area, and thengit commit --amend
.
What happens next is your latest commit being unrolled, the new changes from theStaging Area added to the existing one, and the editor for the commit message opening.
In the editor you'll see the previous commit message.
Feel free to change it to something better.
After you're done, take another look at the latest commit withgit show HEAD
.
As you've certainly expected by now, the commit hash is different. The original commit is gone, and in it's place there is a new one, with the combined changes and new commit message.
Note how the other commit data like author and date are unchanged from the original commit. You can mess with those too, if you really want, by using the extra
--author={AUTHOR}
and--date={DATE}
flags when amending.
Congratulations! You've just successfully re-written history for the first time!
Interactive Rebase
Generally when wegit rebase
, werebase
onto a branch. When we do something likegit rebase origin/master
, what actually happens, is a rebase onto theHEAD of that branch.
In fact if we felt like it, we couldrebase
onto any commit.
Remember that a commit contains information about the history that came before it
Like many other commandsgit rebase
has aninteractive mode.
Unlike most others, theinteractiverebase
is something you'll probably be using a lot, as it allows you to change history as much as you want.
Especially if you follow a work-flow of making many small commits of your changes, which allow you to easily jump back if you made a mistake,interactiverebase
will be your closest ally.
Enough talk! Lets do something!
Switch back to yourmaster branch andgit checkout
a new branch to work on.
As before, we'll make some changes to bothAlice.txt
andBob.txt
, and thengit add Alice.txt
.
Then wegit commit
using a message like "Add text to Alice".
Now instead of changing that commit, we'llgit add Bob.txt
andgit commit
that change as well. As message I used "Add Bob.txt".
And to make things more interesting, we'll make another change toAlice.txt
which we'llgit add
andgit commit
. As a message I used "Add more text to Alice".
If we now have a look at the branch's history withgit log
(or for just a quick look preferably withgit log --oneline
), we'll see our three commits on top of whatever was on yourmaster.
For me it looks like this:
git log --oneline0b22064 (HEAD -> interactiveRebase) Add more text to Alice062ef13 Add Bob.txt9e06fca Add text to Alicedf3ad1d (origin/master, origin/HEAD, master) Add Alice800a947 Add Tutorial Text
There's two things we'd like to fix about this, which for the sake of learning different things, will be a bit different than in the previous section onamend
:
- Put both changes to
Alice.txt
in a single commit - Consistently name things, and remove the.txt from the message about
Bob.txt
To change the three new commits, we'll want to rebase onto the commit just before them. That commit for me isdf3ad1d
, but we can also reference it as the third commit from the currentHEAD asHEAD~3
To start aninteractiverebase
we usegit rebase -i {COMMIT}
, so let's rungit rebase -i HEAD~3
What you'll see is your editor of choice showing something like this:
pick 9e06fca Add text to Alice pick 062ef13 Add Bob.txt pick 0b22064 Add more text to Alice # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
Note as always howgit
explains everything you can do right there when you call the command.
TheCommands you'll probably be using most arereword
,squash
anddrop
. (Andpick
but that one's there by default)
Take a moment to think about what you see and what we're going to use to achieve our two goals from above. I'll wait.
Got a plan? Perfect!
Before we start making changes, take note of the fact, that the commits are listed from oldest to newest, and thus in the opposite direction of thegit log
output.
I'll start off with the easy change and make it so we get to change the commit message of the middle commit.
pick 9e06fca Add text to Alice reword 062ef13 Add Bob.txt pick 0b22064 Add more text to Alice # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands) [...]
Now to getting the two changes ofAlice.txt
into one commit.
Obviously what we want to do is tosquash
the later of the two into the first one, so let's put that command in place of thepick
on the second commit changingAlice.txt
. For me in the example that's0b22064.
pick 9e06fca Add text to Alice reword 062ef13 Add Bob.txt squash 0b22064 Add more text to Alice # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands) [...]
Are we done? Will that do what we want?
It wont right? As the comments in the file tell us:
# s, squash = use commit, but meld into previous commit
So what we've done so far, will merge the changes of the second Alice commit, with the Bob commit. That's not what we want.
Another powerful thing we can do in aninteractiverebase
is changing the order of commits.
If you've read what the comments told you carefully, you already know how: Simply move the lines!
Thankfully you're in your favorite text editor, so go ahead and move the second Alice commit after the first.
pick 9e06fca Add text to Alice squash 0b22064 Add more text to Alice reword 062ef13 Add Bob.txt # Rebase df3ad1d..0b22064 onto df3ad1d (3 commands) [...]
That should do the trick, so close the editor to tellgit
to start executing the commands.
What happens next is just like a normalrebase
: starting with the commit you've referenced when starting it, each of the commits you have listed will be applied one after the other.
Right now it won't happen, but when you re-order actual code changes, it may happen, that you run into conflicts during the
rebase
. After all you've possibly mixed up changes that were building on each other.Justresolve them, as you would usually.
After applying the first commit, the editor will open and allow you to put a new message for the commit combining the changes toAlice.txt
. I've thrown away the text of both commits and put "Add a lot of very important text to Alice".
After you close the editor to finish that commit, it will open again to allow you to change the message of theAdd Bob.txt
commit. Remove the ".txt" and continue by closing the editor.
That's it! You've rewritten history again. This time a lot more substantially than whenamend
ing!
If you look at thegit log
again, you'll see that there's two new commits in place of the three that we had previously. But by now you're used to whatrebase
does to commits and have expected that.
git log --oneline105177b (HEAD -> interactiveRebase) Add Bobed78fa1 Add a lot very important text to Alicedf3ad1d (origin/master, origin/HEAD, master) Add Alice800a947 Add Tutorial Text
Public History, why you shouldn't rewrite it, and how to still do it safely
As noted before, changing history is a incredibly useful part of any work-flow that involves making a lot of small commits while you work.
While all the small atomic changes make it very easy for you to e.g. verify that with each change your test-suite still passes and if it doesn't, remove or amend just these specific changes, the 100 commits you've made to writeHelloWorld.java
are probably not something you want to share with people.
Most likely what you want to share with them, are a few well formed changes with nice commit messages telling your colleagues what you did for which reason.
As long as all those small commits only exist in yourDev Environment, you're perfectly save to do agit rebase -i
and change history to your hearts content.
Things get problematic when it comes to changingPublic History. That means anything that has already made it to theRemote Repository.
At this point is has becomepublic and other people's branches might be based on that history. That really makes it something you generally don't want to mess with.
The usual advice is to "Never rewrite public history!" and while I repeat that here, I've got to admit, that there is a decent amount of cases in which you might still want to rewritepublic history.
In all of theses cases that history isn't 'really'public though. You most certainly don't want to go rewriting history on themaster branch of an open source project, or something like your company'srelease branch.
Where you might want to rewrite history are branches that you'vepush
ed just to share with some colleagues.
You might be doing trunk-based development, but want to share something that doesn't even compile yet, so you obviously don't want to put that on the main branch knowingly.
Or you might have a work-flow in which you share feature branches.
Especially with feature branches you hopefullyrebase
them onto the currentmaster frequently. But as we know, agit rebase
adds our branch's commits asnew commits on top of the thing we're basing them on. This rewrites history. And in the case of a shared feature branch it rewritespublic history.
So what should we do if we follow the "Never rewrite public history" mantra?
Never rebase our branch and hope it still merges intomaster in the end?
Not use shared feature branches?
Admittedly that second one is actually a reasonable answer, but you might still not be able to do that. So the only thing you can do, is to accept rewriting thepublic history andpush
the changed history to theRemote Repository.
If you just do agit push
you'll be notified that you're not allowed to do that, as yourlocal branch has diverged from theremote one.
You will need toforce
pushing the changes, and overwrite the remote with your local version.
As I've highlighted that so suggestively, you're probably ready to trygit push --force
right now. You really shouldn't do that if you want to rewritepublic history safely though!
You're much better off using--force
's more careful sibling--force-with-lease
!
--force-with-lease
will check if yourlocal version of theremote branch and the actualremote match, beforepush
ing.
By that you can ensure that you don't accidentally wipe any changes someone else may havepush
ed while you where rewriting history!
And on that note I'll leave you with a slightly changed mantra:
Don't rewrite public history unless you're really sure about what you're doing. And if you do, be safe and force-with-lease.
Reading history
Knowing about the differences between the areas in yourDev Environment - especially theLocal Repository - and how commits and the history work, doing arebase
should not be scary to you.
Still sometimes things go wrong. You may have done arebase
and accidentally accepted the wrong version of file when resolving a conflict.
Now instead of the feature you've added, there's just your colleagues added line of logging in a file.
Luckilygit
has your back, by having a built in safety feature called theReference Logs AKAreflog
.
Whenever anyreference like the tip of a branch is updated in yourLocal Repository aReference Log entry is added.
So theres a record of any time you make acommit
, but also of when youreset
or otherwise move theHEAD
etc.
Having read this tutorial so far, you see how this might come in handy when we've messed up arebase
right?
We know that arebase
moves theHEAD
of our branch to the point we're basing it on and the applies our changes. An interactiverebase
works similarly, but might do things to those commits likesquashing orrewording them.
If you're not still on the branch on which we practicedinteractive rebase, switch to it again, as we're about to practice some more there.
Lets have a look at thereflog
of the things we've done on that branch by - you've guessed it - runninggit reflog
.
You'll probably see a lot of output, but the first few lines on the top should be similar to this:
git reflog105177b (HEAD -> interactiveRebase) HEAD@{0}: rebase -i (finish): returning to refs/heads/interactiveRebase105177b (HEAD -> interactiveRebase) HEAD@{1}: rebase -i (reword): Add Bobed78fa1 HEAD@{2}: rebase -i (squash): Add a lot very important text to Alice9e06fca HEAD@{3}: rebase -i (start): checkout HEAD~30b22064 HEAD@{4}: commit: Add more text to Alice062ef13 HEAD@{5}: commit: Add Bob.txt9e06fca HEAD@{6}: commit: Add text to Alicedf3ad1d (origin/master, origin/HEAD, master) HEAD@{7}: checkout: moving from master to interactiveRebase
There it is. Every single thing we've done, from switching to the branch to doing therebase
.
Quite cool to see the things we've done, but useless on it's own if we messed up somewhere, if it wasn't for the references at the start of each line.
If you compare thereflog
output to when we looked at thelog
the last time, you'll see those points relate to commit references, and we can use them just like that.
Let's say we actually didn't want to do the rebase. How do we get rid of the changes it made?
We moveHEAD
to the point before therebase
started with agit reset 0b22064
.
0b22064
is the commit before therebase
in my case. More generally you can also reference it asHEAD four changes ago viaHEAD@{4}
. Note that should you have switched branches in between or done any other thing that creates a log entry, you might have a higher number there.
If you take a look at thelog
now, you'll see the original state with three individual commits restored.
But let's say we now realize that's not what we wanted. Therebase
is fine, we just don't like how we changed the message of the Bob commit.
We could just do anotherrebase -i
in the current state, just like we did originally.
Or we use the reflog and jump back to after the rebase andamend
the commit from there.
But by now you know how to do either of that, so I'll let you try that on your own. And in addition you also know that there's thereflog
allowing you to undo most things you might end up doing by mistake.
Top comments(102)

- Email
- LocationNY
- EducationMount Allison University
- PronounsHe/him
- WorkCo-founder at Forem
- Joined
Learnx
concepts, notx
commands. Probably a reusable statement across many technologies.

- LocationSalzburg
- WorkFull Stack Developer at Self Employed
- Joined
How about just:
Learn concepts, not commands.

- LocationFranca, SP - Brazil
- EducationComputer Science at Uni-FACEF
- Pronounshe/him
- WorkFullstack web developer
- Joined
teste

- Email
- LocationNY
- EducationMount Allison University
- PronounsHe/him
- WorkCo-founder at Forem
- Joined
Toast

Nice post, thanks.
For another perspective I could also suggest the "Git from the Bottom Up"jwiegley.github.io/git-from-the-bo...
which starts from how the repo is built inside (blobs and trees). Opened my eyes at some point - and also allowed me to explain Git to others better :-)

- LocationAustria
- WorkSoftware Engineer at dynatrace
- Joined
Thanks, did not know that one yet! Added to my reading list

- LocationKigali, Rwanda
- WorkSoftware Engineer
- Joined
One of the best articles about Git. You've really put a lot of effort in producing this awesome article. Thank you so much for your contribution to the developer community. Actually, This is the best and intensive article that I have ever encountered on the internet. Nice job!

Great write up on crucial concepts to understanding how/why to use various git commands. There is one semantic distinction that I find helpful when talking aboutrebase
to those unfamiliar with it. Rather than saying:
We re-base our
add_patrick
branchon the current state of themaster
branch.
It might be clearer what is happening if you say:
We re-base our
add_patrick
branchfrom the current state of themaster
branch.
That is to say, we get a new base set of commits for our branchfrom another branch. So, as you describe, when we run the commandgit rebase master [add_patrick]
, we are taking all commitsfrommaster
thatadd_patrick
does not yet have, and rewinding to apply them to HEAD before replaying our commits inadd_patrick
.

- LocationAustria
- WorkSoftware Engineer at dynatrace
- Joined
For me personally understanding theon
oronto
wording for rebase as it's also used in the git reference actually helped me when I learned about it originally.
I take my changes, which where originally based on some branch HEAD, and I put themonto
some other branch's current state (or state of the same branch)

Like I said, it is semantics, but I've found that slight change in wording useful for some when helping them understandrebase
.

Agreed - I completely misunderstood this at first because “rewind and rebase onto” sounds like “take my work from ‘add_patrick’, add all those commits “onto” ‘master’ (which doesn’t happen & wouldn’t really make sense) before moving the divergence point & continuing on the current branch.
The key point to understand is that you get all new commitsfrom ‘master’ so your current branch is up to date with it (kinda like a git pull), then reapply the commits from ‘add_patrick’ again from that new point of divergence from master, but stillon ‘add_patrick’ itself.
That confusion on my part aside, I found this to be a fantastic overview! Thanks!

- Email
- LocationRaleigh, North Carolina
- EducationB.S.Ed. Western Carolina University
- WorkStudent at Udemy.com
- Joined
Hey! I am so new to GitHub...where am I supposed to be typing these commands...at the command prompt, possibly? I am trying to follow this tutorial. I ran into a 'GitHub Desktop.' Now I'm confused as to whether I use this or do it some other way.
Sorry!
Angie

- LocationAustria
- WorkSoftware Engineer at dynatrace
- Joined
Hi
Yes, those go in the command line!
There a few graphical git clients that I hear are nice. The github desktop one, tower git and a lot of my colleagues use what comes with their IDE (we use intellij idea for java)
But for understanding what is going on I think you'll learn more using git from the commandline.
The tools abstract a lot of things away trying to make things easier to use

- Email
- LocationRaleigh, North Carolina
- EducationB.S.Ed. Western Carolina University
- WorkStudent at Udemy.com
- Joined
I think I prefer the command line anyway. Thanks so much!

- LocationKinda GTA
- EducationSoftware Development
- Joined
What an effort you have put on this post. It's almost felt bad to call it as a post. It seems like a book or wiki very least.
I was just getting there by doing it and you helped me greatly. I also really loved the useful tips. Thank you so much.

- LocationPhoenix, AZ
- EducationUNCW
- WorkSenior Product Engineer at Podia
- Joined
This is crazy good. Super informative and the visual aids definitely help. Thanks for sharing! 👏🏼

- Email
- LocationIndia
- EducationMCA, MA (Econ)
- PronounsHe/Him
- WorkProgrammer at Freelance
- Joined
One caveat you should mention is that "git push" doesn't always work on some git installations, especially POSIX ones like Linux. You may have to qualify with the remote repository for it to work:
git push origin master
But otherwise, its super informative and well written article.

- LocationAustria
- WorkSoftware Engineer at dynatrace
- Joined
With git 2.0 introducing thesimple
push strategy as default setting, I was under the impression that you generally wont need to qualify the remote you're pushing to, as long as it's set as upstream and has the same name as your local branch (which it is if you don't go out of your way to have it differently).
Or am I wrong about something there?

- Email
- LocationIndia
- EducationMCA, MA (Econ)
- PronounsHe/Him
- WorkProgrammer at Freelance
- Joined
Yep,it considers the current branch (origin/master) as the default if gitconfig --global push.default
setting is set tocurrent
. This is usually set by default on windows and ios, so simply doing "git push" might work but on some linux distros, this setting isn't set tocurrent
but set tonothing
instead (which means you'll have to explicitly add the branch).
Especially, the last time when I'd worked on Ubuntu, simply doing agit push
had not worked.

- LocationAustria
- WorkSoftware Engineer at dynatrace
- Joined
As far as I understood it, the "new" (git 2.0 is from 2014) default is simple.
From the git doc:
When neither the command-line nor the configuration specify what to push, the default behavior is used, which corresponds to the simple value for push.default: the current branch is pushed to the corresponding upstream branch, but as a safety measure, the push is aborted if the upstream branch does not have the same name as the local one.
Of course it may still be that some distro installations either install older versions, or install with a non-default configuration. Somewhat recently having set-up my work laptop on Ubuntu 18.04 I do not recall having to set the push configuration

- Location🇫🇷 Caen, Normandy
- EducationMaster's degree at University of Caen
- Workdeveloper at Orange
- Joined

- LocationHyderabad India
- WorkApplication Developer at Oracle
- Joined
One of the best tech articles I have ever read. Thanks for the effort
For further actions, you may consider blocking this person and/orreporting abuse