Git Work Book
Git Work Book
Lorna Mitchell
This book is for sale at http://leanpub.com/gitworkbook
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook
using lightweight tools and many iterations to get reader feedback, pivot until you
have the right book and build traction once you do.
There is no easy way to pick up new skills, and git is no exception (and also theres
a lot of git skills!). In recognition of that, this workbook encapsulates everything you
will need for all the various areas, by explaining it, showing you, and then asking
you to try it yourself. There are some puzzles and quizzes along with the exercises -
these are there to hack your brain into remembering this stuff! Its tempting to skip
through or imagine what you would write, but please embrace the experience and
work through all the exercises and puzzles, I promise it will help to make everything
end up in your brain.
There are some supporting materials in git repositories on GitHub: you can find all of
them under the Git Workbook organisation here: https://github.com/gitworkbook.
If you have an comments or questions, then I would love to hear them: [email protected].
Have fun!
Each section has a box for you to tick so you can track your progress.
common to all platforms. I recommend that everyone gets to know a little bit on the
command line in git, and then goes on to choose a tool for everyday use - if you use
TextMate, Eclipse, or another IDE then there will be support for git in those tools.
To begin, theres some good documentation for each platform available at https://help.github.com
up-git/.
Assignment
Your first assignment is to install git.
Configure Git
The main point of version control is to have a safe place to keep the canonical version
of a project, and to have a record of all the changes that occurred in that project. Each
change is a commit (used as both a noun and a verb: you can commit a change
or create a commit), and will include a few key pieces of information:
To record this information, git will need to know who you are to begin with, so
well configure it with that information. In any directory, run the following (edit the
commands to include your actual details):
https://help.github.com/articles/set-up-git/
CONTENTS 3
In typical commandline style, git doesnt give any feedback, but we can check our
config settings with git config --list (it can be a very long list, depending on
your setup)
The username and email settings are the really important ones since this information
gets associated with each commit that you make. If you see issues such as your
avatar not appearing on GitHub, check these settings and make sure that your email
addresses are the same in your config and on GitHub.
Git can accept configuration at different levels. The three scopes for git config are:
The more specific settings override the more general ones. The example above sets
the global configuration that will apply to all your respositories.
Some other handy configuration options that you might like to set up at this point:
color.ui
Try setting this to true to get more colours in your terminal
core.editor
Sets which program youll be prompted to use to edit things in. Git will use
your system default editor or vi, but you can set whichever program youd
rather use.
core.autocrlf
Windows users, please set this to true. It will help you to avoid whitespace
issues in your projects.
Assignment
1) Configure your name, email address, and any of the other configura-
tion options that look useful.
2) Check your settings with git config --list
Make A Repository
Git is a distributed version control system, which means that you do your local work
on an actual repository, and there are other remotes which are also repositories in
exactly the same way. Well start by making a standalone repository and then well
discuss how to link it to other repositories or repos as they are known.
The command to create a repo is git init [dir]. If you run git init on its own,
your new repo will be in the current directory. If you run git init myproject then
youll get a new directory called myproject with a git repo inside it. You can check
if a directory is a git repo by running git status (if it says not a git repository,
check you changed into the directory you created the repository in) or by checking
that there is a hidden .git/ directory inside there.
Assignment
1) Create a new git repository in the directory project1.
2) Check that this directory exists and is a git repository
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: example.txt
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working d\
irectory)
#
# modified: README.md
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# other.txt
The left hand state is a file that has changes, and also includes the new/untracked
files. Changes here are in our file system, weve done the work, but we havent yet
begun to feed that information to git.
The middle section is the staging area. We use git add to put files into this area (we
CONTENTS 7
can remove them from here too without losing our changes, as youll see shortly).
Once we are happy with the collection of changes that we have added to the staging
area, we commit, which is the area on the right hand side of the diagram. A commit
is formed from the contents of the staging area, the current timestamp, the user
information you added earlier and the message you supply when you create the
commit. At this point, your new commit is quite literally history: it forms part of the
history of the project.
In real, just-a-day-at-the-office terms, gits staging area means that we dont need
to conduct our entire working life around the special needs of git. We can sit at our
computers, hack together a masterpiece, and then curate some good commits from
the work that we have done, by only adding the files we want to include in each
commit. It gets our workflow into a place which makes sense for developers and
away from working around a tool.
Assignment
1) Create a new text file in your repo; you can use your normal tools
for this, dont feel tied to the command line as the repo is just a normal
directory with a few superpowers.
2) Run git status and check you can see it under untracked files. Now
run git add [file] with the filename of your file.
3) Run git status again and check that you can see the file listed under
Changes to be committed:.
4) Now were going to actually commit the change. Run git commit, and
you will be prompted to edit a file. Write a message about the change
you made at the top of this file, then save and quit the file.
note
If you didnt update your editor settings in the config section, you may
now be in vim. If that doesnt seem like a good idea, try this:
press Esc. Now type (it will appear in the bottom left):
:wq and press Enter
now set your core.editor config value to something
you would rather use
CONTENTS 8
Weve discussed how amazing and powerful it is to have a staging area rather than
just snapshotting all our changes, but lets see how that could work in practice.
Assignment
1) Add multiple files to your project directory, and run git status
2) Do git add for just ONE of your files
3) Run git status, then commit your changes as before, and run git
status again (spotting a theme here?)
CONTENTS 9
I can create a commit with a subset of the changed files in it, leaving the other
changes intact in my working directory
Assignment
1) Run git log to see the commits you have already made.
2) Try the command again with the --stat switch to get information
about which files were affected in each commit
3) For a compact view, use git log --oneline - this is useful when
looking at a bigger picture
Command Shows
git diff unstaged changes
git diff --staged changes staged for commit
git diff HEAD all changes in this directory since the last
commit, both staged and not
Its useful to use git diff to check whats already been done, as well as to see what
is or is not included in your next commit. In git, its possible to stage part(s) of a file,
but not all of the changes in it, and so git diff is very handy there too!
For example if I change a file, and add it, then change it again, Ill see it listed as both
staged and unstaged in the output:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.md
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working d\
irectory)
#
# modified: README.md
#
We can check out whats going on here by running the three git diff commands
above on this very simple changeset.
CONTENTS 11
$ git diff
diff --git a/README.md b/README.md
index 67cda08..5cc0661 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
Read this
+then this
In the diff output, additions have a + at the start of the line (and are in green if you
have default colours enabled) and removals have - at the start of the line (and are in
red if you have default colours enabled).
Git status shows the file as staged, so she commits her work.
Will Mias changes be included? Any of them? Why?
Quiz answers can be found at the end of the workbook
Another very handy trick is to diff and add at the same time; this is one I use a lot.
Running git add -p shows each chunk of output from git diff in turn, with the
option to add it! Press ? if youre not sure which option you want; this produces a
more detailed help output. The other advantage of this approach is that it really helps
to spot things which should not have been included, such as debug statements!
Assignment
1) Make sure you have no outstanding changes in your repository by
running git diff and adding/committing changes as necessary (well
come back to undoing things in a moment)
2) Create a file with at least 10 lines of content in it, add and commit the
file
3) Now edit this file at the beginning and at the end. Stage only one
of these changes by doing git add -p and choosing y for one of the
changes and n for the other
4) Use git diff to check that only one of the changes will be committed,
and then commit it
5) Check that the other change is still in your working directory, then
add and commit that too
I know how to add some, but not all, changes from a file
with git add -p, but sadly she already added the whole thing
Enter the git reset command to rescue Anya and allow her to make well-curated
commits on her project. This command is destructive, so use with care! In this case
though, were only working on the staging area, to un-add either one or all files.
Command Outcome
git reset No changes to any project files, but any staged
changes are now unstaged
git reset [file] The named file will not be changed, but will no
longer be staged. No other staged changes are
affected
Warning
Theres a variation on this command, git reset --hard, which actually throws
away past commits as well. Only add the --hard if youre sure you know what
to do next! We will cover this thoroughly in later chapters, just be aware of the
danger until then
Assignment
1) Make changes to one or more files in your repo. Add the files and run
git status to check things look as you expect.
2) Pick a file and unstage it. Then run git status again.
3) Finally add and commit all your changes, or throw them away.
not the only person to have made this mistake, and in fact git has a command for
exactly this situation: git commit --amend.
This command will remove and re-stage your changes, allow you to change the
commit message, and then create a new commit with the new commit message.
Because its not actually the same commit, the commit identifier will change, so
dont use this trick if you have already pushed your changes to another repository.
(We havent covered pushing yet, so if that was an unfamiliar term, dont worry, all
will become clear!)
Assignment
1) Run git log to see the history of all the commits in this repository.
We want to alter the most recent commit message.
2) Run git commit --amend and you will be prompted by your editor
which will allow you to edit the commit message. Then save and close
the file.
3) Run git log again and see that both the message and the commit
identifier has changed.
We can create a new branch, feature, by doing git checkout -b feature. Running
git status shows us that were on the feature branch, but in fact our repo looks like
this:
Its only when we commit on the feature branch that the branches actually diverge:
CONTENTS 16
Each commit you make, by changing the files and then adding the changes, then
gets baked into something that becomes part of history. The branch pointer moves
along to point to the new commit, and the branch is made up by walking back up
the chain of commits, looking at the parent of each one.
Using branches is one of the most powerful features of git, and its useful in lots of
different situations. Usually we use branching to give us a safe space in which to
work. At its most basic level, using a branch means we can commit at logical points
throughout our work, rather than being unable to commit until the whole thing is
completely finished and tested.
Whether developing a new feature or fixing a bug, having a unit of work on a
separate branch means that we can very easily share our work with our team either
to collaborate with us or to review what we have done. The branch can be merged
and, if necessary, unmerged - but only if it was created separately in the first place.
Using branches gives us great flexibility and allows us to work quickly and commit
and share often; a vital ingredient into good development practices.
Now youve hopefully got a better idea of how the concepts fit together, lets have a
look at the commands youll need to work with branches:
With all these tools in the proverbial box, its time to try them out
Assignment
1) Make sure you have no changes in your working area, either commit
or undo them before you begin.
2) Create a branch called excellent-feature.
3) Make two commits to the branch excellent-feature. Run git log
to check that you see the commits as you expected.
4) Go back to the master branch. Run git status to double check
which branch you are on, and git log to check that you cannot see
the commits that you made to the other branch.
5) . do nothing. Well merge in the next section!
When we merge, were combining two sets of changes. Sometimes this results in a
conflict, well deal with those in a moment. Lets look at a very simple merge in terms
of what happens to the files at play (which is what we really care about). Here is a
repository with three files in it:
Now lets imagine we made a feature branch and made some changes to those files
- were working on a cool new feature. We finish, and by then there have been other
changes to the original master branch.
CONTENTS 19
At this point, we want to take our new feature, and put it into the codebase without
losing anything that anyone else has done. So we checkout the master branch (since
this is the one we want to work on), and type git merge feature.
What happens looks something like this:
CONTENTS 20
CONTENTS 21
The merge has taken all the changes from both branches, even where the same files
are affected, and combines them safely. Never again will you hear the question Who
last edited/uploaded the template files for site X? source control will handle this
for you. Especially if youre new to tools like this, its almost indistinguishable from
magic!
(we still didnt talk about conflicts yet, they get their own chapter later on in the
workbook)
In terms of commits, weve made this:
The master branch has a new commit that has two parents and no changeset since
this is a merge commit. You can see this also when you run git log as it shows both
commit identifiers rather than one parent:
$ git log
commit e3a7b5591cab5ca792c4b2a00cf5b2060dc2962e
Merge: b5bb230 3a39f06
Author: Lorna Mitchell <[email protected]>
Date: Tue Sep 9 17:39:27 2014 +0100
This shows that the merge occurred in this commit, and that there were 2 parent
commits. The result is that your project now contains the changes from both
branches.
CONTENTS 22
Assignment
This assignment follows on directly from the previous one about creating and
committing to a branch. If you didnt do that already, go there first and then come
back!
I can merge one branch to another in git, correctly and with confidence
Assignment
1) Try each of the following and write in the table what the switch does.
Use git help log if you want more information.
--all
--graph
--raw
--author
--grep
--oneline
--decorate
2) When you see an interesting commit in the list, try git show [com-
mittish] with the commit identifier to see more information about just
that commit.
CONTENTS 24
We move changes, deletions and new files into the staging area with git add. Your
local working copy and the staging area are not associated with a branch - they will
come with you if you switch branches, so it doesnt matter which branch you are on
until you commit.
The staging area shows file names, but it doesnt deal in whole files; it actually deals
in changesets. It is perfectly possible to show a file in both the staged and unstaged
sections of git status output. Try this:
How can a file be both staged and unstaged? What happened was that when you
staged the file, the current changes in it were staged. We can check exactly what will
be included in the next commit with git diff --staged.
When you edit the file again, the new changes arent staged. View unstaged changes
with git diff - and for avoiding committing debug messages or having to type lots
of long file paths, try staging your changes with git add -p, which shows you the
output of diff with the option of staging each chunk.
You can also unstage changes, using the git reset command - and in fact you can
selectively unstage parts of a file using git reset -p, which offers each changeset
for you to examine and reset or not.
The git reset command takes some switches that indicated how severe youd like
your reset to be. The default is --mixed, which will reset your staging area but not
affect your working directory at all. This book already mentioned --hard which
resets your staging area and your working directory, and is usually used when forcing
your branch to go back in history. The third option is --soft which doesnt change
either your staging or working directory areas.
The assignment will give you the change to try all of these commands, and feel free
to improvise as well to give yourself some practice.
Assignment
1) Edit any three existing files in the project. Add all your changes, but
dont commit
2) Now change your mind about which files should be staged. Use git
reset to unstage everything or git reset [filename] to just unstage
one file
3) Oh no, we shouldnt have changed that last file at all! Use git
checkout -- [filename] to restore it to how it was at the last commit.
4) Boss has changed his mind, none of those changes are needed. Go
back to a clean working directory using git reset --hard.
Health Warning Beware that git reset --hard is the best way I know to totally
lose work without any chance of recovery in git. You have been warned - use with
spades of caution!
I can stage some changes, and unstage some changes, and undo my changes
completely, either selectively or wholesale
What is important here is that your working directory and staging area are not
associated with any particular branch. You can make changes, and even stage them,
but they only get applied to a branch when you actually commit - it is fine to switch
branches with local and/or staged changes in order to get onto the correct branch
you want to commit to!
Assignment
Getting the correct change onto the right branch is a key skill, so this is a great time
to get some practice! Try this:
I can commit to the correct branch in git (this makes you in the top 32% of git
users *)
* statistic entirely invented
CONTENTS 28
Assignment
1) Make a change to a tracked file. Inspect it with git diff.
2) Add the change to the staging area, now git diff shows no output;
use git diff --staged to inspect exactly what will be in your next
commit.
3) Make the commit.
4) Run git log and pick a commit from the past. Run git diff
[committish] to see the differences between that and your current state.
5) Inspect the differences between the master branch and your experi-
ment branch from the previous example, with git diff experiment.
I can inspect the differences between versions, including branches, history and
my working directory
You can replace git diff with git difftool in any circumstance.
Using the git difftool command uses all the tricks we learned when using the git
diff command, but with a little bit more sparkle? force? meaning?
CONTENTS 29
Now in any situation where you would run git diff you can run git difftool
instead - magic!
Pro-tip: if you do choose meld, try git difftool -d to get meld to open all the files
and folders at once and let you browse the changes rather than opening pairs of files
in turn. Most other GUIs will do this by default but I had to look up how to get meld
to do this.
odt2txt that will render a LibreOffice Document as text so I can configure git to
use this tool whenever it would like to show me the difference between two .odt
documents.
To configure git, changes are needed to both the .gitattributes and .git/config
files. First: .gitattributes. This file is local to your repo and doesnt get shared with
other repositories when you push and pull. We will use this file to add various file
extensions to a specific file type. For the LibreOffice documents, Ill add the following:
*.odt diff=odt
Now well tell .git/config how to handle odt files by adding some configuration
there:
[diff "odt"]
binary = true
textconv = odt2txt
This says that when diffing .odt files, they are binary and therefore git should not
attempt to show me diffs of the actual file contents. The textconv setting tells git
how to get a textual representation of this document and git will use this to diff.
When I diff two LibreOffice files, Ill see the text-based changes without any of the
formatting or other features not understood by odt2txt - very neat!
MergeTool
Git can also use an alternative program to help understand and resolve merge
conflicts. The git mergetool command is to git merge what git difftool is
to git diff. You configure your chosen diff program by setting the merge.tool
configuration and then when you use git mergetool you will see three screens, the
two at the sides are one from each branch, and the one in the middle is the version
that will be used after this merge. Its a great way of seeing the original context of
each change when merging is complicated!
https://github.com/dstosberg/odt2txt/
CONTENTS 31
Assignment
1) Find or clone an active repository to your local machine - any project
that has some git history that you can play with is fine.
2) Decide which diff tool you want to use/try, perhaps starting with what
you already have installed, or my recommendations from above.
3) Run git log and choose a commit from the history, just 2 or 3
commits ago is fine. Now run git diff [rev] to see the diff since that
change.
4) Replace git diff [rev] with git difftool [rev] and see your
other program launch. What is different when you use this approach?
Assignment
This is a pretty simple feature so the easiest way to show you is to let you have a go.
1) Make a change to whatever branch you are on, but dont commit it.
Instead type git stash
2) Run git status and marvel at the absence of the work you just did
3) Make more changes and type git stash again
4) Look at the stashes by running git stash list
5) Apply the newest one by running git stash apply, then run git
status and then git stash list again. What happened?
I can safely put my work aside for a moment, and still get it back
Note that since the new commit has a different parent, its identifier changes despite
it being exactly the same changeset.
This is super-useful for when you fix something small in the process of working
on one thing, and then it becomes apparent that your small fix was actually quite
important and should be used immediately on another branch.
Assignment
1) Create a new branch from master called fixes. Make three commits
to this branch
2) Identify the revision number (or copy it into your clipboard) of the
first commit you made to fixes
3) Now checkout master (in git we usually checkout the branch that
were going to make changes to)
4) Apply the first change from the fixes branch to master by typing git
cherry-pick [committish]
5) Check that you have master branch plus that one commit using git
log
Git Revert
The revert command is the opposite of cherry-pick, because instead of applying
the changeset, it applies the inverse changeset.
CONTENTS 34
If you have a repository that looks like this, with a commit that shouldnt be there
somewhere along the line:
Sometimes this might be the newest commit, if you havent pushed your changes, you
can reset your branch to before the commit, but if you have pushed your changes, or
if there are more changes after the mistake, then you should use the revert technique.
The command is git revert 49c9 - and what actually happens is that a new commit
is generated, with the exact opposite diff to the named commit, so you get something
that looks like this:
This is a safe way to undo changes without erasing history, the revert will always
show what was done, and then what was undone, so you can look back and
understand what happened.
Assignment
1) Run git log and pick a commit from 3 or 4 commits in the past. The
situation is that we need to undo these changes, but we have already
pushed our changes.
2) Now use git revert to make a commit with exactly the opposite
changeset to the one in the commit you chose.
3) Check that the files changed in the way you expected.
UnMerge Changes
Merging feature branches in is a regular feature in git, but sometimes we need to
undo a merge. This chapter will show you how to proceed - but if youve already
pushed your changes, you probably want to read the previous chapter about git
revert instead!
The reset command with the --hard switch simply moves a branch pointer from one
commit to another, and updates both your staging area and your working directory
to contain what was in that specific commit. Weve seen some examples in earlier
chapters that made use of this technique, but lets examine it in detail now.
If we start with a master branch with a feature branch merged into it:
CONTENTS 36
We can set the master branch back to before the merge again (use git log to find
which commit was before the merge):
Since the branch is simply a pointer to the commit, the reset command can move
that pointer around in any way. Powerful!
CONTENTS 37
This chapters assigment will walk you through a bit more merging practice, followed
by use of the git unmerge feature.
Assignment
1) Make sure you have no uncommitted changes in your local repo (this
is a good place to start with almost everything)
2) Now we need something to merge. Create and switch to a new branch
called masterpiece, and add and commit some new feature here.
3) Go back to your master branch and add at least one more commit
here.
4) Merge your masterpiece into the master branch. Check that git log
shows what youd expect (use the --graph and --all options to see how
the commits relate to one another), and also that your filesystem now
has both sets of changes in the same place.
5) Oh no! We merged in too soon, we need to undo the merge. Dont
panic .
6) Run git log and look at the merge commit, then the one before it
(still on the master branch), and note its commit identifier.
7) Now well just move our master branch back in time to point to the
commit before the merge by running git reset --hard b5bb2 (use the
committish you got from the previous step)
8) Run git log again to check that the master and masterpiece branches
look as you would expect.
Health Warning This was already mentioned but git reset --hard is a really good
way to lose work in git. The next section covers what to do if it happens.
$ git reflog
b5bb230 HEAD@{0}: reset: moving to b5bb2
e3a7b55 HEAD@{1}: merge feature: Merge made by the 'recursive' strat\
egy.
b5bb230 HEAD@{2}: commit: bugfixes
083a068 HEAD@{3}: commit: refactoring
c08074a HEAD@{4}: checkout: moving from feature to master
3a39f06 HEAD@{5}: commit: great feature
c08074a HEAD@{6}: checkout: moving from master to feature
c08074a HEAD@{7}: commit (initial): this is the repo we'll work on
This shows all the commits that Ive visited during my work - importantly this
includes the merge commit that I went on to undo later on. It wont be possible
CONTENTS 39
to view this commit via git log because it no longer has a branch or tag pointing to
it, but the reflog lets us see it.
Once we see the commit we want, we have a few options:
Run git reset --hard e3a7 to get the current branch back to that
lost commit
Checkout that commit to see if it has what you need by doing
git checkout e3a7. This puts you in a detached head state but
dont worry! If you decide this is a useful piece of work that youd
like to keep then you can either use the reset command above,
or simply create a new branch at this point with git checkout
-b newbranch. If you are finished looking, you can git checkout
master at any time to back to reality.
Its all very well knowing that this exists, but lets practice using it before you really,
really need to know how.
Assignment
This follows on from the previous assignment; you had merged a branch and then
reset to undo the merge.
1) Look at the commits you have with git log --oneline and observe
that you cant see the merge commit you did before resetting
2) Run git reflog and identify that merge commit that you undid
before
3) Now reset back to that commit so that master has the merge again.
I tried to merge my improvement branch into the master branch but there were
overlapping changes and git needs me to go and work out what to do. At this point,
I am now in a merging state; you can check this by looking at the output of git
status, which will say something like:
CONTENTS 41
$ git status
# On branch master
# You have unmerged paths.
# (fix conflicts and run "git commit")
#
# Unmerged paths:
# (use "git add <file>..." to mark resolution)
#
# both modified: README.md
#
no changes added to commit (use "git add" and/or "git commit -a")
At this point you can pretend this never happened: run git merge --abort and your
repo will return to its pre-merge state. You can do this at any point during a merge
and its handy for getting your repository back to a clean state while you wait for
someone to come out of a meeting and explain the merge, or help you, or whatever.
Or while you wait for someone else on your team to sort out the mess?
Sometimes, we do need to handle the conflict. In this case, I have just one file with
conflicts (any other files that were changed in the merge will be added for me, so
youll see them listed but you dont need to do anything with them. If we look inside
that file to see what happened, we see both sets of changes with a bit of ascii art to
show which is which:
My Project
##########
Oh, look. The same line has been changed in two different ways in the two branches.
We can help git out by editing this file to make it look correct. Make sure that you
remove the lines starting with <<<<<, ====== and >>>>, fix the rest of the file so the
conflicted line(s) look correct, and save the file.
Running git status will still show the file as conflicted until you add it, then it will
suggest that you run git commit to complete the merge - so do that, and were done.
The basic process:
Oh no! A conflict.
Run git status to see which files are conflicted.
Fix the files.
Add the files.
Type git commit; the commit message will already be stored from when the
commit was intially attempted.
I also have two top tips to share with you about merge conflicts:
The main thing with a merge is to keep calm, and know what to do. Work through
the assignment and then youll be all set for this eventuality.
CONTENTS 43
Assignment
The hard bit is always contriving a conflict on a simple example, here goes.
1) Switch to the master branch and check you have no pending changes.
Create a file called trouble.txt, add a few lines, then add and commit
it.
2) Create a branch called awkward but do not switch to it, stay on (or
return to) the master branch.
3) Edit trouble.txt, and add/commit your changes.
4) Checkout the awkward branch, and edit trouble.txt again, changing
the same lines of the file in different ways
5) Checkout master, then try to merge the awkward branch in - this will
result in a conflict
6) Resolve the conflict, and complete the process by adding the affected
file (after you have fixed it), and committing.
Merge Types
In the previous examples, youve seen that a merge will take place and git reports
merge made by the recursive strategy. Sometimes, youll see instead Fast-for-
ward, which means that there were no changes on the target branch while the
incoming branch was in progress. So if a recursive merge, with changes on both
branches, looks like this:
CONTENTS 44
The feature branch is created and changes are committed to it, but there are no
changes in the meantime on the master branch, so there isnt anything to combine
when we come to do the merge. By default, git will do a fast-forward merge, just
moving the master branch pointer to the tip of the feature branch.
If this isnt what you want, and you want to create a merge commit as we have done
in the previous examples, then you can force git to do that by adding --no-ff to your
merge command. This can be useful if, for example, you want it to be clear which
branch work was done on, and where that branched off master and was merged
back in. Forcing git to make the merge commit gives a clearer paper trail of what
happened.
CONTENTS 45
Assignment
1) Checkout the master branch and check you have no pending changes
(this should seem familiar by this point!)
2) Checkout a new hotfix branch and apply a commit or two to it
3) Merge hotfix into master. Oh no! It did the fast-forward and we
didnt want it to
4) Undo the merge (look back at a previous exercise, you did do this
already)
5) Now repeat the merge but without allowing git to fast-forward.
I know what a fast-forward merge is and how to prevent it if thats not what
I wanted
Git narrates its progress pretty well, with the comment rewinding head to replay
your work giving a good clue. It will then read out each commit as it succesfully
applies it, or notifies you if theres a problem. The result is output that looks like this:
Once this is done, you can see (from the diagrams or from git log --all --graph
if you have tried this yourself) that weve set ourselves up to achieve a fast-forward
merge that we looked at in the previous section. Whether you choose to rebase
regularly or not is a feature of your chosen work process and branching strategy,
but its very useful to know HOW to - you can use this technique for example when
someone has branched off the wrong branch by mistake when creating a new feature.
CONTENTS 47
One thing to look out for is that the commit identifiers of all the commits on your
branch will change, because they depend on the parent and the original parent
commit has changed. As a result, it isnt advisable to push a branch after youve
rebased it if it was already shared. Instead a common pattern is to rename a featureX
branch to featureX.rebased or similar.
(Rebasing can also be used to rewrite history on a single branch, that bit is in the
next section).
Rebasing feels a bit like performing surgery on your repo, but there are a limited
number of things that can go wrong. They are:
You panic (valid response). Try the git rebase --abort command
if this eventuality occurs.
There is a conflict. See earlier sections on handling merge conflicts,
its the same for rebasing. Once your conflict is sorted, you tell git
to carry on with git rebase --continue
The key with rebase is to really understand whats happening, so in the practical
section, well do lots of checking on what is going on.
Assignment
1) Start with git status to check for being on the master branch with
no changes. This is a good habit to get into whenever you return to your
git project after a break.
2) Create a new branch called issue-42, but stay on the master branch.
3) Add some commits to the master branch.
4) Add some commits to the issue-42 branch. Its up to you whether you
edit the same line of the same file on both branches and try handling a
rebase conflict while youre here.
5) See that you have two branches with commits since the most recent
commit they have in common (I use git log for this)
CONTENTS 48
6) Run git status, check that you have no local changes and check that
you are on the issue-42 branch - because this is the one we want to alter
by rebasing it.
7) Now rebase! We want the commits on the issue-42 branch to remain
intact, but look as if the branch was created now from the master branch
and your amazing feature was created very quickly indeed.
8) Assuming everything is still okay, use git status to check that the
rebase is complete (if not, it will tell you, so its worth checking if
anything happened that youre not sure about) and git log to check
that the commits on all the branches look as you would expect, with the
master branch commits before the issue-42 ones.
9) Now checkout master, do a git merge issue-42 and admire your
fast-forward merge.
I can rebase a branch onto another branch in git, and I understand why you
might want to do that
Where you have already pushed really means someone has already pulled. If youve pushed a branch to your own
repo and you werent expecting anyone to pull or collaborate on it, then you can do as you please!
CONTENTS 49
Action Outcome
pick keep the commit (this is the default)
reword get the chance to change the commit message
edit edit the commit
fixup combine these changes into the previous commit. Useful for
the fixed the bug, followed by no really fixed it this time
situations
squash still combines commits but prompts you with an editor with
all the commit messages present in order for you to make
the correct commit
remove to pretend a commit never happened: simply delete the line
for it
Pretty powerful stuff. One thing that always confuses me is that the order of the
commits is in the order that they happened, with the oldest first. Which is exactly the
opposite way round from the way I usually see my history, via the git log command!
First of all: git log
$ git log
commit e11e694f62832dc84473350d838040aa94977280
Author: Lorna Mitchell <[email protected]>
Date: Mon Oct 20 10:49:47 2014 +0100
commit c8c923986d5df6e9281a77884af4f031e5355c15
Author: Lorna Mitchell <[email protected]>
Date: Mon Oct 20 10:49:29 2014 +0100
Oops! I think you can see the problem here. The interactive rebase is exactly the
right thing to iron out this history. Since I know I only want to operate on those last
2 commits, I can use this command rather than looking up which revision I want to
go back to: git rebase -i HEAD2.
When I do so, my editor opens a file that looks like this (the gotcha is that these
commits show in time order, oldest first, unlike git log. This confuses me every time):
CONTENTS 50
pick c8c9239 Fixed bug 123 that's been causing all the issues
pick e11e694 Bug definitely fixed by this commit
There are also some comments in the file, which is basically the documentation for
the various options we showed above. Each commit is listed with its revision number
and commit message, and starting with the word pick. Next, I will edit the file to
describe what should happen, and then git works through line by line to actually do
it. At each point where my input is needed, it will pause and let me either edit the
message or actually use git. git status will warn me that I am
still mid-rebase while thats the case, but I can type git rebase --abort to undo it
all at any time during the rebase.
If I dont change anything and just save and close the file that git prompted me with,
then git will simply take and reapply all the changes, one at a time. This is the default
behaviour and the edits that I make to the file tell git what to do differently this time
around.
For the situation above, Im simply going to squash that commit into the first one, so
it looks like I can actually fix a bug properly:
pick c8c9239 Fixed bug 123 that's been causing all the issues
fixup e11e694 Bug definitely fixed by this commit
When I save and close the file, git will make a new commit (with a new identifier)
containing all the changes needed to fix this bug. Since I chose the fixup option, I
dont need to supply any more information or write a new commit message.
Now my git log looks much tidier:
$ git log
commit 83347acaf9e9efeaf948fea8718cfa40af0fb069
Author: Lorna Mitchell <[email protected]>
Date: Mon Oct 20 10:49:29 2014 +0100
Exactly like when we merge or rebase a branch, conflicts can occur here too. Git will
stop, and give you the space you need to resolve the conflict exactly like you do for a
merge conflict: identify the conflict, edit the files, then add and commit. Theres no
difference in what you need to do and when you are finished, git will continue with
its rebase.
Assignment
1) Get onto the master branch and create three commits here.
2) Look at the history with git log, then use git rev-parse HEAD3
to check which revision well be rebasing back to.
3) Rebase all three: delete the first commit you made and combine the
other two using squash
4) Look at the history again to check that you now have one commit as
you expected
Git chatters away and then tells you that you have a new directory with a repository
in it. What just happened? Well, something like this:
http://github.com
CONTENTS 53
At this point you have a local repo and all the git skills you have learned until now
can be used!
Assignment
1) Log in to GitHub and then visit https://github.com/gitworkbook/accompaniments
2) Fork the repo and make sure that there is now a repo of this project
at a URL which has your username instead of gitworkbook in it.
3) On the page of your copy of the repo, look for the clone URL on
the right hand side bar. It comes in different flavours, the recommended
default is HTTPS but if you would rather set up SSH keys than confirm
your password regularly, then read https://help.github.com/articles/
generating-ssh-keys and then use the SSH URL. Whichever way you
choose, you should now copy that URL.
4) Put the URL into the git clone command. You will get new directory
with the same name as the default repo (you can override this by
supplying an additional argument for the directory name).
CONTENTS 54
5) Change into that directory and observe that there is a git repository
there.
6) Make some changes, and commit them.
7) Push your changes, and then visit your fork on the github website and
you should be able to see them there, too.
This is very important when collaborating because it means that our work can be
based (or rebased) onto the current version of the project, rather than sliding further
and further out of date as the clone gets older.
Assignment
Its difficult to arrange upstream changes on a github repo, so well do this exercise
with some local repositories. This also works to show you how multiple repositories
on the same machine could work, without GitHub or forks in the mix.
1) Get out of any existing git repositories, were going to create a few
at the same directory level as one another (you might like to create a
directory to put them all in).
2) Create the upstream repository by running git init upstream. Make
three commits of anything you like here, on the master branch.
3) Go up a level so youre outside the repo again. Clone this upstream
repo like this: git clone --bare upstream origin.git. The --bare-
- switch means that this repo doesnt have a working directory, if you
CONTENTS 56
cd into it you will see contents that would normally be found in the
.git directory of a repository. Since it is just a repo and not a working
directory, we use the .git suffix as a convention.
4) Make sure you are not inside a repo, then clone the origin.git repo
you just made into a directory called local
5) Confused yet?? You have three repositories, upstream, origin and
local - to reflect how things look when we fork an upstream repo. So
now were set up and ready to begin the exercise!
6) Next well make some changes in the upstream repo, there is a new
feature in our project. Make sure you are using the upstream repo and
add a couple of commits to it.
7) Switch back to your local repo and run git remote -v to check
which remotes it knows about. Add the path to the upstream repo as
a remote called upstream, then run git remote -v again and check
that this reflects what you expected (what did you expect?).
8) There are new changes upstream of our repo. Run git fetch up-
stream to get the changes from upstream into the current repo. This
brings in all the meta data and allows us to checkout those branches,
inspect the changes, and so on, but it does not affect any of the branches
on this repository. To apply the changes from the incoming master
branch to the local one, you need to merge: checkout the master branch
and run git merge upstream/master.
9) Its more common to use pull than fetch for master branches. pull
does both the fetch and the merge command in a single swoop. For
a master branch, then the changes made upstream arent negotiable,
but for most other branches that we collaborate on, you might like to
examine the changes first, in which case you would fetch and refer to
those branches by their repo/branchname qualified name.
10) So the changes are in our local repo, but what about our fork? This
is the final stage in the process and it goes like this: git push origin
master, run that command and see your changes go into the origin repo.
11) Since the master branch on your local repo is already set up to track
the master branch on the origin repo (check that this is true by running
CONTENTS 57
git branch -vv), you could achieve the same thing as the previous step
with the shorter command git push - git will assume you meant the
branch that is being tracked.
[branch "master"]
remote = origin
merge = refs/heads/master
[branch "cool-feature"]
remote = upstream
merge = refs/heads/cool-feature
If you want to stop a branch from tracking another, or change what it tracks, you
can edit this config file manually.
To set up a branch to track another branch in the first place, you have two main
options:
can fetch the upstream changes with git fetch upstream and
then do git checkout feature4. Git will then create the branch
and notify you that it is tracking the upstream one of the same
name.
if you created a branch locally, you can create the link when you
push it to a remote by using the -u switch. So if your new branch is
called fix-failures then you can do git push -u origin fix-
failures to link your branch with the one on the origin repo.
Once your tracking branches are set up, you can just use git push or git pull to
push and pull changes between the local and remote pairs of branches - and you can
still push and pull changes to other branches or remotes too just by specifying those
as you usually would.
Assignment
This assignment continues from the previous one and assumes you have a local and
remote origin repository
What happened? The local and remote versions of the branches have diverged. We
know that feature and origin/feature are different branches; usually we set them
up as tracking branches but they are just as different as two different-named branches
on a single repo.
From this point, there are two options:
1) Allow the git pull command to add a merge commit to the local
feature branch and then add an additional merge commit when adding
those changes back onto origin/feature.
2) Ask git to rebase the feature branch onto the tip of master. We can
do this very easily with the command git pull --rebase - this updates
your local branch with changes from upstream, then adds any local
commits on top of that. At this point we can git push our changes and
everything is as it should be.
The first solution is simpler, and git will try to create the merge commit in this
situation (remember that git pull is just git fetch with git merge attached) so
to avoid this you will need to empty the file when you are prompted to write the
commit message. Saving an empty file will abort the commit. Using git pull --
rebase however rewrites the history on the local branch to make it look as if it had
never got out of sync from the upstream one, making it a very useful trick in this
(common) situation.
CONTENTS 60
Assignment
You will need two repositories for this, to simulate the local/remote pattern.
I can put my new commits onto a branch that got out of date
Git Hooks
Git hooks are scripts that we can set up to occur in response to given events. They are
stored in the .git/hooks directory inside your repository. By default it will contain
sample placeholders for all the various kinds of hooks you can use.
Im sure you can think of places in your own process where it would be useful to have
an automated script run rather than someone having to remember to do something.
I mostly see this in a couple of key places:
One important thing to note is that the scripts are set on a per-repository basis, theyre
not shared when you push and pull to other repositories. So if you change something
in your script, or you want your colleagues to use the same ones, you need to agree
that via another channel because git wont pick it up by itself. On the plus side, it
means nobody needs to know that I need a syntax check to catch my silly mistakes!
To use or create a hook in your own repo, create a file with the name of the event
that will cause it to run - if you need a hook to do mulitple things then you will still
have one script that does them all. Put your script into the .git/hooks/ directory in
your repository, and check that it is executable. Thats it - the script will run next
time the event that it hooks into occurs.
Assignment
1) Theres a sample hook in the examples/ directory of the accompani-
ments repo called pre-commit. Copy it into the .git/hooks directory of
your repo
2) Edit the test.php file, then add and commit it.
3) Now edit the file again and break the PHP syntax (no need to be a PHP
expert, feel free to let your cat walk on your keyboard at this point). Now
go ahead and add and commit again - and see what happens
Submodules are where one project has another project as its dependency, either
because the two projects are maintained separately or sometimes because the
submodule is a dependency of many other projects too; a pattern we usually refer
to as Common Code. Often, the tools that we use already have ways of managing
dependencies and even tracking bleeding edges of dependencies - I work in web
scripting languages so npm, bundler, composer, pip - all those tools are one way
to solve this problem. Some of those tools play nicely with using another github repo
as a dependency also, and you many not find you need submodules at all. As a team,
youll need to decide what works for your situation.
Assuming you go with submodules, keep reading :)
The best way to describe submodules is one thing inside another. Imagine a big shed,
with a small shed inside it. From the outside of the big shed, it looks like a shed. When
you get inside there, you can see that there is a smaller shed as well as whatever else
is stored here. But when you get inside that small shed, it just looks like youre in a
small shed with nothing particularly special about it.
What have sheds got to do with it? Well, the repositories work more or less the same
way: the main repo just looks normal from the outside but has some special features
from the inside. The submodule repo just looks like a repo once you get inside it; it
doesnt care its part of something bigger.
The tricky part about working with submodules is keeping everyones changes in
sync. There are so many moving parts here:
the main repo contains a submodule, which knows which directory holds the
submodule and what revision that submodule should be on
the submodule directory contains a repo that behaves like a normal repo
when you push changes to your main repo, you must also push changes to
your submodule repo, otherwise other users will get a pointer to a revision
that doesnt exist in their submodules
https://www.npmjs.org/
http://bundler.io/
https://getcomposer.org/
https://github.com/pypa/pip
CONTENTS 63
when you make a change to your submodule, you must also commit and push
on your main repo otherwise your collaborators repos wont realise they need
to use the newer repository in their own submodules
If this all sounds very confusing, dont worry. Most things make more sense when
you actually see them in action, so heres the assignment for you.
Assignment
1) Make sure youre not already inside a git repo, and create a fresh one
called project. Add some commits here.
2) Now come back out of your repo and create another new repo, called
library - this will be our dependency. Make some commits here too.
3) Go back to your project repo and well add in our submodule de-
pendency: git submodule add /path/to/library library. This adds
a submodule in the library directory pointing to the repository you
specified (which might often be a remote URL rather than a local path).
4) Run git status and check that you see a new library file and also
a .gitmodules file. Look what is in the .gitmodules file.
5) Add both files and commit. Git records the submodule as just which
commit it should be on.
6) Go back to the library repository (the original copy, not the one
inside your project repo) and add another commit.
7) How can you get that change to show up in your project repo?
Go into the project repo and make a change to a file unrelated to the
submodule, but dont commit. Run git status and look at the output
showing what you changed.
8) Enter the library subdirectory and run git status again this
repository has no idea that it is inside another repository. Bring in the
changes that you made earlier by running git pull and check that you
can see them as you expect. Run git status (yes, this is becoming a
habit, but a useful one).
CONTENTS 64
9) Come back up a level into the project directory and run git status.
The submodule shows as changed. This is a really key point. If you
change the submodule (it is completely fine to work on it in place and
commit and push from there), then you ALSO need to change the parent
project, otherwise it just continues to point to the old version of the
submodule.
10) Add and commit the library submodule in the parent project to
move it on to point to the current version of the submodule. When you
share your changes, everyones repositories will know to use this new
revision
11) Now make a change directly inside the submodule inside your parent
project. It is important to note that if you change the submodule, then
commit and push the parent project, you MUST also push the submodule
otherwise everyone else will have a repo that points to a commit that
doesnt exist.
I can use submodules in my project, and safely make, track and share changes
in both my own project and the submodule
To do well at the game, or indeed in searching for anything, it helps to reduce the
amount of space you need to look in - so you guess somewhere in the middle and
thereby halve the amount of space you need to look in.
CONTENTS 65
Git has a command called bisect which helps us to quickly narrow down where in
the history of a branch a particular problem was introduced. To do this, it looks at
the last good commit, the current bad commit, and checks out something in between
the two to let us test if the problem is present in this version. This is a very handy
feature to know about so that you can quickly identify when something went wrong,
and in fact you can also ask git to use a script to test each revision so it does all the
checking out and testing itself.
First of all, we use git bisect bad [rev] and git bisect good [rev] to indicate
where we have known good and bad states. Then git picks a revision from the middle
of the range for us to test.
In these diagrams, grey commits are outside of the range under consideration, red
ones are known to be broken, green ones are known to be working, and the blue one
is the commit were testing
We test the code at that point, and say whether this is good (working) or bad (broken),
and then bisect will give us another revision to try.
Then we repeat this over and over, eliminating part of the search range each time.
CONTENTS 66
Assignment
1) Clone the bisect example repository from this location: https://github.com/gitworkbook/
example
2) Start by telling git that the last good commit was e2acddb by running
git bisect start followed by git bisect good e2acddb. This is a
commit where we are sure everything was working correctly at that
point.
3) Tell git that the repo is currently broken by running git bisect bad
(the omitted committish causes git to assume you mean HEAD).
4) At this point, git has checked out a revision for you which is in the
middle of the range of commits that we need to search. The bug is that
the file important.txt got deleted - so for each revision it checks out,
you need to check whether the file exists. If it does, you can say git
https://github.com/gitworkbook/bisect-example
CONTENTS 67
bisect good (again, itll assume you mean the current HEAD revision),
but if it is missing, run git bisect bad instead.
5) Repeat the previous step until git tells you which revision introduced
the problem
6) Finally run git bisect reset to go back to the tip of the master
branch (and at this point you can go on and fix the problem, perhaps
using git revert!)
Quiz Answers
Config Settings
Jo will end up with [email protected] because she set this as her local setting, and it is
more specific. Check settings with git config --list - and if you want to remove
the local one, you can just remove the line from the config file that lives inside the
.git/ directory at the top level of your project.
Staging Changes
The first set of changes that Mia made will be included since they were present when
she added the file. The edits she made after adding wont be included in the commit.
CONTENTS 68
Branches
Branches are easy to make.
Git takes care of combining the changesets on the branches when we merge.
Merge commits are different to normal commits because they have two parents.
Branching gives a safe way of working on new features or bug fixes.
Git Undo
you didnt commit yet? git reset
its your most recent commit (and you didnt push)? git reset --hard
the commit is not the most recent, but you didnt push yet? git revert or git
rebase -i (which we havent covered yet, but it would be correct)
you already pushed your changes? git revert
Rebasics
To fix a branch that branched from the wrong place? git rebase
To make a branch be based off the current tip of master and therefore include
all the changes in master? git rebase
To edit the history on a single branch? git rebase -i
Credits
Checkboxes designed by Arthur Shlain from the thenounproject.com