Bggit Usl C 1
Bggit Usl C 1
1 Foreword 1
1.1 Audience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Official Homepage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Email Policy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4 Mirroring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.5 Note for Translators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.6 Copyright and Distribution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.7 Dedication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Git Basics 4
2.1 What is Git? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.1 Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2 What is GitHub? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.3 What is GitHub? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.4 The Most Basic Git Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.4.1 Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.5 What is Cloning? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.5.1 Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.6 How Do Clones Interact? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7 Actual Git Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7.1 Step 0: One-time Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7.2 Step 1: Clone an Existing Repo . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.7.3 Step 2: Make Some Local Changes . . . . . . . . . . . . . . . . . . . . . . . . 9
2.7.4 Step 3: Add Changes to the Stage . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.7.5 Step 4: Commit those Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.7.6 Step 5: Push Your Changes to the Remote Repo . . . . . . . . . . . . . . . . . . 12
i
CONTENTS ii
11 File States 58
11.1 What States Can Files in Git Be In? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
11.2 Unmodified to Untracked . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
11.3 Files In Multiple States . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
24 Configuration 126
24.1 Local Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
24.2 Listing the Current Config . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
24.3 Getting, Setting, and Deleting Variables . . . . . . . . . . . . . . . . . . . . . . . . . 128
24.4 Some Popular Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
24.5 Editing the Config Directly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
24.6 Conditional Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
24.7 Older Git Versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
28 Difftool 140
28.1 Configuring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
28.2 Available Difftools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
29 Mergetool 143
29.1 Merge Tool Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
29.2 Some Example Merge Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
29.3 Using Vimdiff as a Merge Tool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
29.4 Backing up the Originals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
30 Submodules 147
30.1 Using a Repo with Submodules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
30.2 Creating a Submodule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
30.3 Setting the Commit for the Submodule . . . . . . . . . . . . . . . . . . . . . . . . . . 150
30.4 Getting Submodule Latest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
30.5 Updating the Actual Submodule Itself . . . . . . . . . . . . . . . . . . . . . . . . . . 153
30.5.1 Modify the Submodule Repo Elsewhere . . . . . . . . . . . . . . . . . . . . . . 153
30.5.2 Modify the Submodule Repo in the Submodule Directory . . . . . . . . . . . . . 153
30.6 Getting the Submodule Status . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
30.7 Some Behind the Scenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
30.8 Deleting a Submodule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
31 Tags 157
31.1 Lightweight Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
31.2 Annotated Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
31.3 Pushing Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
31.4 Deleting Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Foreword
1.1 Audience
The initial draft of this guide was put online for the university students where I worked (or maybe still work,
depending on when you’re reading this) as an instructor. So it’s pretty natural to assume that’s the audience
I had in mind.
But I’m also hoping that there are enough other folks out there who might get something of use from the
guide as well, and I’ve written it in a more general sense with all you non-college students in mind.
This guide assumes that you have basic POSIX shell (i.e. Bash, Zsh, etc.) usage skills, i.e.:
• You know basic commands like cd, ls, mkdir, cp, etc.
• You can install more software.
It also assumes you’re in a Unix-like environment, e.g. Linux, BSD, Unix, macOS, WSL, etc. with a POSIX
shell. The farther you are away from that (e.g. PowerShell, Commodore 64), the more manual translation
you’ll have to do.
Windows is naturally the sticking point, there. Luckily Git for Windows comes with a Bash shell variant
called Git Bash. You can also install WSL1 to get a Linux environment running on your Windows box. I
wholeheartedly recommend this for hacker types, since Unix-like systems are hacker-awesome, and addition-
ally I recommend you all become hacker types.
1
https://learn.microsoft.com/en-us/windows/wsl/
1
Chapter 1. Foreword 2
1.4 Mirroring
You are more than welcome to mirror this site, whether publicly or privately. If you publicly mirror the site
and want me to link to it from the main page, drop me a line at beej@beej.us.
1.7 Dedication
The hardest things about writing these guides are:
• Learning the material in enough detail to be able to explain it
• Figuring out the best way to explain it clearly, a seemingly-endless iterative process
• Putting myself out there as a so-called authority, when really I’m just a regular human trying to make
sense of it all, just like everyone else
• Keeping at it when so many other things draw my attention
A lot of people have helped me through this process, and I want to acknowledge those who have made this
book possible:
• Everyone on the Internet who decided to help share their knowledge in one form or another. The free
sharing of instructive information is what makes the Internet the great place that it is.
• Everyone who submitted corrections and pull-requests on everything from misleading instructions to
typos.
Thank you! ♥
Chapter 2
Git Basics
2.1.1 Definitions
• Source Code Control System/Version Control System: Software that manages changes to a software
project potentially consisting of thousands of source files edited by potentially hundreds of developers.
4
Chapter 2. Git Basics 5
What about GitLab and Gitea? GitLaba is a competitor to GitHub. Giteab is an open-source com-
petitor that allows you to basically run a GitHub-like front-end on your own server. None of this
information is immediately important.
a
https://gitlab.com
b
https://docs.gitea.com/
Regardless of whatever repos you have on GitHub, you’ll also have copies (known as clones) of those repos
on your local system to work on. Periodically, in a common workflow, you’ll sync your clone of the repo
with GitHub.
You don’t need GitHub. Even though you might be commonly using GitHub, there’s no law that
says you have to. You can just create and destroy repos on your local system all you want, even if
you’re not connected to the Internet. See Appendix: Making a Playground for more information once
you’re more comfortable with the basics.
1. Clone a remote repo. The remote repo is commonly on GitHub, but not necessarily.
2. Make some local changes in your working tree, where the project files are on your computer.
3. Add those changes to the stage (AKA the index).
4. Commit those changes.
5. Push your commit back to the remote repo.
6. Go back to Step 2.
This is not the only workflow; there are others that are also not uncommon.
2.4.1 Definitions
• Clone (verb): to make a copy of a remote repo locally.
• Clone (noun): a local copy of a remote repo.
• Remote: In Git, a clone of a repo in another location.
• Working Tree: The directory that you go into to edit and change the files of the project. This is created
when you clone.
• Stage: In Git, a place you add copies of files to in preparation for a commit. The commit will include
all the modified files that you’ve placed on the stage. It will not include modified files you haven’t
placed on the stage.
• Index: A less-common name for the stage.
2.5.1 Definitions
• Distributed Version Control System: A VCS in which there is no central authority of the data, and
multiple clones of a repo exist.
This means after you clone a repo, there are two: one that is remote, and one that is local to your
computer.
These clones are completely separate and changes you make to your local repo will not be reflected in
the remote clone. Unless, that is, you explicitly make them interact.
• Pull: This takes the remote commits and downloads them to your local repo.
Behind the scenes, there’s a process going on called a merge, but we’ll talk more about that later.
Until you push, your local changes aren’t visible on the remote repo.
Until you pull, the changes on the remote repo aren’t visible on your local repo.
For this example, we’ll assume we have a GitHub repo already in existence that we’re going to clone.
Recall the process in The Most Basic Git Workflow, above:
1. Clone a remote repo. The remote repo is commonly on GitHub, but not necessarily.
2. Make some local changes.
3. Add those changes to the stage.
4. Commit those changes.
5. Push your commit back to the remote repo.
6. Go back to Step 2.
In this guide, things you type at the shell prompt are indicated by a prefaced $. Don’t type the
$; just type what follows it. Your actual shell prompt might be % or $ or something else, but here we
use the $ to indicate it.
If you need to change them in the future, just run those commands again.
If you get an error with the above commands you might be running an older version of Git. Try
them again, but leave out the word set. Or, better yet, see if you can get a newer version of Git.
Chapter 2. Git Basics 8
$ cd git-example-repo
$ ls -la
total 16
drwxr-xr-x 5 beej staff 160 Jan 18 13:35 .
drwxr-xr-x 106 beej staff 3392 Jan 18 13:35 ..
drwxr-xr-x 12 beej staff 384 Jan 18 13:35 .git
-rw-r--r-- 1 beej staff 162 Jan 18 13:35 README.md
-rwxr-xr-x 1 beej staff 75 Jan 18 13:35 hello.py
The directory .git has special meaning; it’s the directory where Git keeps all its metadata and
commits. You can look in there, but you don’t have to. If you do look, don’t change anything. The
only thing that makes a directory a Git repo is the presence of a valid .git directory within it.
Let’s ask Git what it thinks the current status of the local repo is:
$ git status
Gives us:
Chapter 2. Git Basics 9
On branch main
Your branch is up to date with 'origin/main'.
$ code .
Otherwise, open the code in your favorite editor, which, admit it, is Vim4 .
Let’s change hello.py:
It was:
1 #!/usr/bin/env python
2
3 print("Hello, world!")
4 print("This is my program!")
2
We’re glazing over an important topic here that we’ll come back to later called remote tracking branches.
3
And again we’re doing some hand-waving. There are actually three branches. Two of them, main and origin/main are on your
local clone. And there’s a third main on the remote origin that your origin/main is tracking. Feel free to ignore this detail until we
get to the remote tracking branch chapter.
4
https://www.vim.org/
Chapter 2. Git Basics 10
1 #!/usr/bin/env python
2
3 print("Hello, world!")
4 print("This is my program!")
5 print("And this is my modification!")
$ git status
On branch main
Your branch is up to date with 'origin/main'.
no changes added to commit (use "git add" and/or "git commit -a")
$ git diff
diff --git a/hello.py b/hello.py
index 9db78d2..1187d32 100755
--- a/hello.py
+++ b/hello.py
@@ -2,3 +2,4 @@
print("Hello, world!")
print("This is my program!")
+print("And this is my modification!")
What the heck is this sorcery? If you’re lucky, it got color-coded for you with (by convention) added lines
in green and deleted lines in red.
For now, just notice a couple things:
Chapter 2. Git Basics 11
We’ll dive more into diff later, but I wanted to introduce it here since it’s so useful.
no changes added to commit (use "git add" and/or "git commit -a")
It’s suggesting that git add will add things to the stage—and it will.
Now we, the developers, know that we modified hello.py, and that we’d like to make a commit that reflects
the changes to that file. So we need to first add it to the stage so that we can make a commit.
Let’s do it with git add:
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: hello.py
Now it’s changed from saying “Changes not staged for commit” to saying “Changes to be committed”, so
we have successfully copied hello.py to the stage!
There’s also a helpful message there about how to unstage the file. Let’s say you accidentally
added it to the stage and you changed your mind and wanted to not include it in the commit after all.
You can run
and that will change it back to the “Changes not staged for commit” state.
The -m switch allows you to specify a commit message. If you don’t use -m, you’ll be popped into
an editor, which will probably be Nano or Vim, to edit the commit message. If you’re not familiar
with those, see Getting Out of Editors for help.
If you do get into the editor, know that every line in the commit message that begins with # is a
comment that is ignored for the purposes of the commit. It’s a little weird that the commit message
is a comment about the commit, and then you can have commented-out lines in the comment, but I
don’t make the rules!
And that’s good news! Let’s check the status:
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
“Nothing to commit, working tree clean” means we have no local changes to our branch.
Turns out there’s an optional shortcut here. If you’ve modified a file, you can just commit it
directly (without adding it to the stage!) by naming it on the command line.
Let’s say you modified foo.txt but didn’t add it. You could:
And that would add it and make the commit. You can only do this with files that you added before.
And there’s never any harm in using git add to add things to the stage.
You can specify multiple files here, or a directory. Also, this doesn’t affect files that are already on
the stage.
But look! The status says we’re “ahead of ‘origin/main’ by 1 commit”! This means our local commit history
on the main branch has one commit that the remote commit history on its main branch does not have.
Which makes sense—the remote repo is a clone and so it’s independent of our local repo unless we explicitly
try to sync them up. It doesn’t magically know that we’ve made changes to our local repo.
And Git is helpfully telling us to run git push if we want to update the remote repo so that it also has our
changes.
So let’s try to do that. Let’s push our local changes to the remote repo.
$ git push
Well, that’s all kinds of not-working. Largely this is because you don’t have permission to write to that repo
since you’re not the owner. And, notably, support for authenticating with a password seems to have been
removed in 2021 which, last I checked, was in the past.
So what do we do? Firstly, we should be the owner of the GitHub repo that we’ve cloned and that’ll solve
some of the permission problems. Secondly, we’d better find another way to authenticate ourselves to GitHub
that’s not plain password.
Let’s try that in the next section.
Chapter 3
Now, we’ve said that GitHub (which is a proprietary web front-end to Git run and owned by Microsoft) is
not Git, and it’s true. It’s also true that you never even need to touch GitHub in order to use Git.
That said, it’s really common for people to use GitHub, so we’ll get it set up in this chapter.
Here we’ll make a new GitHub account and see how authentication works. This involves some one-time
setup.
If you already have a GitHub account, you can skip that section.
If you already have authentication set up with GitHub CLI or with SSH keys, you can skip that section, as
well.
If you don’t need to use GitHub, you can skip the entire chapter!
14
Chapter 3. GitHub: How To Use It 15
3.3 Authentication
Before we get to cloning, let’s talk authentication. In the previous part of the intro, we saw that user-
name/password logins were disabled, so we have to do something different.
There are a few options:
• Use a tool called GitHub CLI
• Use SSH keys
• Use Personal Authentication Tokens
GitHub CLI is likely easier. SSH keys are geektacular. I only recently learned that you could authentication
with personal access tokens, so I can’t really speak to them much.
Personally, I use SSH keys. But other people… don’t. It’s up to you.
If you already have authentication working with GitHub, skip these sections.
Otherwise, choose one of them (like SSH) and use it.
$ gh --version
gh version 2.42.1 (2024-01-15)
https://github.com/cli/cli/releases/tag/v2.42.1
$ gh auth setup-git
$ gh auth login
(The -C sets a “comment” in the key. It can be anything, but an email address is common.)
This results in a lot of prompts, but you can just hit ENTER for all of them.
Best practice is to use a password to access this key, otherwise anyone with access to the private
key can impersonate you and access your GitHub account, and any other account you have set up to
use that key. But it’s a pain to type the password every time you want to use the key (which is any time
you do anything with GitHub from the command line), so people use a key agent which remembers
the password for a while.
If you don’t have a password on your key, you’re relying on the fact that no one can get a copy of the
private portion of your key that’s stored on your computer. If you’re confident that your computer is
secure, then you don’t need a password on the key. Do you feel lucky?
Setting up the key agent is outside the scope of this document, and the author is unsure of how it even
works in WSL. GitHub has documentation on the mattera .
For this demo, we’ll just leave the password blank. All of this can be redone with a new key with a
password if you choose to do that later.
a
https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-
ssh-agent
Anyway, just hitting ENTER for all the prompts gets you something like this:
If you chose any file name other than the default for your key, you’ll have to do some additional
configuration to get it to work with GitHuba .
a
https://www.baeldung.com/linux/ssh-private-key-git-command
What’s that randomart thing with all the weird characters? It’s a visual representation of that
key. There are ways to configure SSH so that you see the randomart every time you log in. And the
Chapter 3. GitHub: How To Use It 17
idea is that if one day you see it looks different, something could be amiss security-wise. I doubt most
people every look at it again once it’s been generated, though.
Now if you type ls ~/.ssh you should see something like this:
id_ed25519 id_ed25519.pub
The first file is your private key. This is never to be shared with anyone. You have no reason to even copy it.
The second file is your public key. This can be freely shared with anyone, and we’re going to share it with
GitHub in a second so that you can log in with it.
If you have trouble in the following subsections, try running these two commands:
You only have to do that once, but SSH can be a bit picky if the file permissions on those files aren’t
locked down.
Now in order to make this work, you have to tell GitHub what your public key is.
First, get a copy of your public key in the clipboard. Be sure you’re getting the file with the .pub extension!
$ cat ~/.ssh/id_ed25519.pub
Copy the entire thing into the clipboard so you can paste it later.
Now go to GitHub, and click on your avatar icon in the upper right.
Choose “Settings”.
Then on the left, choose “SSH and GPG keys”.
Click “New SSH Key”.
For the title, enter something identifying, like, “My laptop key”.
Key type is “Authentication Key”.
Then paste your key into the “Key” field.
And click “Add SSH key”.
We’ll be using SSH to clone URLs later. Remember that.
GitHub has a lot of documentation on this4 but the gist of it is that you’re going to create a token that represents
some kind of access, e.g. “ability to read and write my repos”, and you’re going to use that in lieu of the
password on the command line.
So that last failed example would look like this:
In other words:
1. Generate a token.
2. Use that token as your password.
Your computer might automatically save those credentials so you don’t have to enter them every time. Or it
might not.
One of the main things personal access tokens can give you is fine-grained control over access. You can limit
access to read-only, or just to certain repos, and so on.
Additionally, you can use GitHub CLI authentication with a token as well. You just have to feed it in there
on standard input. Let’s say you have your token in a file called mytoken.txt. You can authenticate with
GitHub CLI like so:
Like with SSH keys, if you lose a laptop that uses a particular access token, you can simply invalidate that
token through GitHub’s UI so that mean people can’t use it.
4
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
Chapter 3. GitHub: How To Use It 19
• Option 2: Choose the “GitHub CLI” tab. Run the command as they have it, which will be something
like:
They’ll have to accept the invitation from their GitHub inbox, but then they’ll have access to the repo.
Be sure to only do this with people you trust!
Chapter 4
When we make commits to a Git repo, it tracks each of those commits in a log that you can visit. Let’s take
a look at that now.
$ git log
produces:
Added
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
Notice that the most recent commit entry is at the top of the output.
21
Chapter 4. The Git Log and HEAD 22
Luckily, there are a few ways to refer to commits with more-human symbolic names.
HEAD is one of these references. It indicates which branch or commit you’re looking at right now in your
project subdirectory2 . Remember how we said you could go look at previous commits? The way you do that
is by moving HEAD to them.
We haven’t talked about branches yet, but the HEAD normally refers to a branch. By default, it’s
the main branch. But since we’re getting ahead of ourselves, I’m going to just keep saying that HEAD
refers to a commit, even though it usually does it indirectly via a branch.
So this is a bit of a lie, but I hope you forgive me.
Some terminology: the Git subdirectory you’re looking at right now and all the files within it is referred to
as your working tree. The working tree is the files as they appear at the commit pointed to by HEAD, plus any
uncommitted changes you might have made.
So if you switch HEAD to another commit, the files in your working tree will be updated to reflect that.
Importantly, the data in the files in the working tree might differ from the data in the files at
the current commit pointed to by HEAD. This happens when you’ve modified a file in the working
tree but haven’t yet committed it.
Okay then, how do we know which commit HEAD is referring to? Well, it’s right there at the top of the log:
More output
We see HEAD right there on the first line, indicating that HEAD is referring to commit with ID:
1
https://en.wikipedia.org/wiki/Hexadecimal
2
I’m stretching it a bit, here. HEAD looks at the commit you’ve switched to. This might not be quite the same as what’s in your
project subdirectory if you’ve modified some of the files since the moving HEAD to that commit. The commit is a snapshot, but that
snapshot doesn’t include modifications to files until you make another commit that contains them.
Chapter 4. The Git Log and HEAD 23
5e8cb52cb813a371a11f75050ac2d7b9e15e4751
Again, that’s a bit of a lie. The HEAD -> main means that HEAD is actually referring to the main branch, and
that main is referring to the commit. HEAD is therefore indirectly referring to the commit. More on that later.
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
If I look at the files, I’ll see the changes indicated by the “More output” commit. But let’s say I want to go
back in time to the previous commit and see what the files looked like then. How would I do that?
Maybe there were some changes that existed back in an earlier commit that had since been re-
moved, and you wanted to look at them, for example.
I can use the git switch command to make that happen.
Before you switch branches, you should be all committed with git status telling you everything
is clean. If you’re not, make a commit or stash your stuff before your switch.
Let’s check out the first commit, the one with ID 5a02fede3007edf55d18e2f9ee3e57979535e8f2.
Now, I could say:
and that would work, but the rule is that you must specific at least 4 unique digits of the ID, so I could have
also done this:
Added
That’s all! Just one commit?! Where’s the second commit I made? Is it gone forever?!
No. Everything is fine. [Soothing image of a kitten sleeping in the sun.]
When you have HEAD at a certain commit, you’re looking at the world as it looked at that snapshot in time.
Future commits haven’t “happened” yet from this perspective. They are still out there, but you’ll have to
change back to them by name. (It’s like a flippin’ time machine!)
Also, do you see anything different about that first line that reads (HEAD)? That’s right: no main to be seen.
That’s because the main branch is still looking at the latest commit, the one with the “More output” comment.
So we don’t see it from this perspective.
Furthermore, this means that HEAD is no longer attached to main. We call this state detached head. And
git switch doesn’t let you do that unless you mean it, which is why we have that --detach in there. (And
reattaching is easy: just switch to the branch you want to attach to.)
Remember earlier when I said it was a bit of a lie to say that HEAD points to a commit? Well,
detached head state is the case where it actually does. Detached head state is just what happens when
HEAD is pointing to a commit instead of a branch. To reattach it, you have to change it to point to a
branch again.
Let’s reattach HEAD to the main branch. There are two options:
1. git switch -: this switches to wherever we were before this, which, in this case, was main.
2. git switch main: this explicitly switches to main.
Let’s try:
Notice there was no --detach in that git switch! We’re reattaching the head, not detaching it, so we don’t
have to tell Git we know what we’re doing.
Don’t worry if you forget the --detach. Git will tell you if you need it.
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Chapter 4. The Git Log and HEAD 25
Added
and our working tree will be updated to show the files as they are in the main commit.
And you see the HEAD -> main? The arrow means HEAD is reattached to main. (If HEAD were detached at the
same commit as main, you’d see HEAD, main.)
There are still times when you need to use checkout, but if your version of Git supports switch,
this isn’t one of them. Use switch if you can and skip this section.
But let’s redo the previous section by using just git checkout instead of git switch. Let’s try:
and it says:
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -
Well, that’s a lot of scary stuff, but it’s just Git telling us that we’re now in detached head state. Which of
course we are since we just detached the head from branch main by switching to a specific commit hash.
And we can get back to the main branch with:
You can also switch back with the aforementioned git switch variants, but we’re pretending those don’t
exist for this section.
Chapter 4. The Git Log and HEAD 26
This moves HEAD to where HEAD was. That is, it moves it nowhere. (Though it does have the effect of
detaching it from the branch.)
But what if I wanted to move to the commit right before where HEAD is now? You can do it with caret
notation like this:
Typing all these carets is wearing me out. Luckily there’s another shorthand we have at our disposal with
tilde notation. The two following lines are equivalent:
After the tilde, you can just give the number of commits back you want to go. So back to my example:
All this said, personally I usually just look at the log and go to a specific commit instead of counting back.
“But that’s just, like, my opinion, man.”
—The Dude
Chapter 5
In Git we might think of this as a sequence of commits. Let’s look at a graph (Figure 5.1) where I’ve numbered
commits 1-5. There, (1) was the first commit we made on the repo, (2) is some changes we made on top
of (1), and (3) is some changes we made on top of (2), etc.
Git always keeps track of the parent commit for any particular commit, e.g. it knows the parent commit of
(3) is (2) in the above graph. In this graph, the parent relationship is indicated by an arrow. “The parent of
commit 3 is commit 2”, etc. It’s a little confusing because clearly commit 3 came after commit 2 in terms of
time, but the arrow points to the parent, which is the opposite of the nodes’ temporal relationship.
A branch is like a name tag stuck on one specific commit. You can move the name tag around with various
Git operations.
The default branch is called main.
The default branch used to be called master, and still is called that in some older repos.
So to make it a little more complete, we can show that branch in Figure 5.2. There’s our main branch attached
to the commit labeled (5).
It’s tempting to think of the whole sequence of commits as “the branch”, but this author recom-
mends against it. Better to keep in mind that the branch is just a name tag for a single commit, and
that we can move that name tag around.
27
Chapter 5. Branches and Fast-Forward Merges 28
But Git offers something more powerful, allowing you (or collaborators) to pursue multiple branches simul-
taneously.
So there might be multiple collaborators working on the project at the same time.
And then, when you’re ready, you can merge those branches back together. In Figure 5.4, we’ve merged
commit 6 and 7 into a new commit, commit 9. Commit 9 contains the changes of both commits 7 and 6.
In that case, somebranch and anotherbranch both point to the same commit. There’s no problem with
this.
And then we can keep merging if we want, until all the branches are pointing at the same commit (Figure
5.5).
And maybe after all this we decide to delete somebranch and anotherbranch; we can do this safely because
they’re fully merged, and can do this without affecting main or any commits (Figure 5.6).
This chapter is all about getting good with branching and partially good with merging.
If you like interactive tutorials, Peter Cottle has put together a great website called Learn Git Branching1 . I
highly recommend it before, during, and/or after reading this chapter.
If two or more people are committing to the same branch, eventually git pull is going to have to merge.
And it turns out there are a few ways it can do this.
For now, we’re going to tell git pull to always classically merge divergent branches, and you can do that
with this one-time command:
If you don’t do that, Git will pop up an error message complaining about it the first time it has to merge on
a pull. And you’ll have to do it then. (Leave the word set out of that command if it fails on older Gits.)
When we talk about rebasing later, this will make more sense.
So far, we’ve been making commits on the main branch without really even thinking about branching. Re-
calling that the main branch is just a label for a specific commit, how does the main branch know to “follow”
our HEAD from commit to commit?
It does it like this: the branch that HEAD points to follows the current commit. That is, when you make a
commit, the branch HEAD points to moves along to that next commit.
If we were here back at Figure 5.7, when HEAD was pointing to the main branch, we could make one more
commit and get us to Figure 5.9.
Contrast that to detached head state, back in Figure 5.8. If we were there, a new commit would get us to
Figure 5.10, leaving main alone.
At this point, there’s nothing stopping you from creating a new branch at the same commit as HEAD, if you
want to do that. Or maybe you are just messing around and decide to switch back to main later, abandoning
the commits you’ve made in detached HEAD state.
Now that we have the abstract theory stuff laid out, let’s talk specifics.
$ git branch
* main
This is telling you there’s one branch, and you have it checked out (the * lets you know that).
If I make a new branch called foobranch and switch to that, I’ll see this:
Chapter 5. Branches and Fast-Forward Merges 31
% git branch
* foobranch
main
% git branch
* (HEAD detached at 10b6242)
foobranch
main
But you can always see what branch you’re on with git branch or git status.
Normally you can just switch to another branch (i.e. have HEAD point to that branch) with git switch branchname.
But if the branch doesn’t exist, you need to use the -c switch to create the branch before switching to it.
Chapter 5. Branches and Fast-Forward Merges 32
Make sure all your local changes are committed before switching branches! If you git status
it should say “working tree clean” before you switch. Later we’ll learn about another option when we
talk about stashing.
So after checking out main, we have Figure 5.11.
And then with git switch -c newbranch, we create and switch to newbranch, and that gets us to Figure
5.12.
That’s not super exciting, since we’re still looking at the same commit, but let’s see what happens when we
make some new commits on this new branch.
The branches we’re making here exist only on your local clone; they’re not automagically propa-
gated back to wherever you cloned the repo from.
The upshot is that if you accidentally (or deliberately) delete your local repo, when you git clone
again, all your local branches will be gone (along with any commits that aren’t part of main or any
other branches pushed to the server).
There is a way to set up that connection where your local branches are uploaded when you push,
called remote-tracking branches. main is connected to a remote tracking branch (usually called
origin/main) which is why git push from main works while git push from newbranch, which
is not by default connected to a remote tracking branch, gives an error. But we’ll talk about all this
later.
If we wanted to see the state of the repo from main’s perspective, what would we have to do?
We’d have to git switch main to look at that branch.
Now for another question. Let’s say we’ve decided that we’re happy with the changes on newbranch, and
we want to merge them into the code in the main branch. How would we do that?
Nothing happened? What’s that mean? Well, if we look at the commit graph, above, all of main’s changes
are already in newbranch, since newbranch is a direct ancestor.
Git is saying, “Hey, you already have all the commits up to main in your branch, so there’s nothing for me
to do.”
But let’s reverse it. Let’s check out main and then merge newbranch into it.
And newbranch is not a direct ancestor of main (it’s a descendant). So newbranch’s changes are not yet in
main.
So let’s merge them in and see what happens (your output may vary depending on what files are included in
the merge):
Wait a second—didn’t we say to merge newbranch into main, like take those changes and fold them into the
main branch? Why did main move, then?
We did say that! But let’s stop and think about how this can happen in the special case where the branch
you’re merging into is a direct ancestor of the branch you’re merging from.
It used to be that main didn’t have commits (5) or (6) in the graph, above. But newbranch has already
done the work of adding (5) and (6)!
The easiest way to get those commits “into” main is to simply fast-forward main up to newbranch’s commit!
Again, this only works when the branch you’re merging into is a direct ancestor of the branch you’re merging
from.
That said, you certainly can merge branches that are not directly related like that, e.g. branches that share a
common ancestor but have both since diverged.
Git will automatically fast-forward if it can. Otherwise it does a “real” merge. And while fast-forward merges
can never lead to merge conflicts, regular merges certainly can.
But that’s another story well get into in the Merging and Conflicts chapter.
Chapter 5. Branches and Fast-Forward Merges 35
At this point, assuming a completed merge, we can delete the topic1 branch:
Done!
A topic branch is what we call a local branch made for a single topic like a feature, bug fix,
etc. In this guide I’ll name branches literally topic to indicate that it’s just an arbitrary branch. But
in real life you’d name the topic branch after what it is you’re doing, like bugfix37, newfeature,
experiment, etc.
But what if you were working on a branch and wanted to abandon it before you merge it into something? For
that, we have the more imperative Capital D option, which means, “I really mean it. Delete this unmerged
branch!”
Use lowercase -d unless you have reason to do otherwise. It’ll at least tell you if you’re about to lose your
reference to your unmerged commits, and then you can override with -D if you really want to.
Chapter 6
We’ve seen how a fast-forward merge can bring to branches into sync with no possibility of conflict.
But what if we can’t fast-forward because two branches are not direct ancestors? In other words, what if the
branches have diverged? What if a change in one branch conflicts with a change in the other?
Yes, I’ve bent the graph a bit there, but we can merge somebranch into main as a fast-forward because main
is a direct ancestor and somebranch is therefore a direct descendant.
But what if, before we merged, someone made another commit on the main branch? And now it looks like
it does in Figure 6.2.
There’s a common ancestor at commit (2), but there’s no direct line of descent. main and somebranch have
diverged.
36
Chapter 6. Merging and Conflicts 37
The # is a shell comment delimiter. You can paste that in if you want, but it does nothing.
The difference here is that Git can’t simply fast-forward. It has to somehow, magically, bring together the
changes from commit (7) and commit (8) even if they’re radically different than one other.
This means that after we bring those two commits together, the code will look like it’s never looked before,
a combination of two sets of changes.
And because it looks like it hasn’t before, we need another commit (another snapshot of the working tree) to
represent the joining of both sets of changes.
We call this the merge commit, and Git will automatically make it for you. (When this happens, you’ll see an
editor pop up with some text in it. This text is the commit message. Edit it (or just accept it as-is) and save
the file and exit the editor. See Getting Out of Editors if you need help with this.)
So after our merge, we end up with Figure 6.3.
Commit labeled (9) is the merge commit. It contains both the changes from (8) and (7). And has the
commit message you saved in the editor.
And we see main has been updated to point to it. And that somebranch is unaffected.
Importantly, we see that commit (9) has two parents, the commits that were merged together to make it.
And look! If we want, we can now fast-forward somebranch to main because it’s now a direct ancestor!
In this example, Git was able to determine how to do the merge automatically. But there are some cases
where it cannot, and this results in a merge conflict that requires manual intervention. By you.
Chapter 6. Merging and Conflicts 38
Now if I look at my status, I see we’re in merge state, as noted by You have unmerged paths. We’re in the
middle of merge; we have to either go out the front or back out the back to get back to normal.
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: foo.py
no changes added to commit (use "git add" and/or "git commit -a")
print("Commit 1")
print("Commit 1")
print("Commit 4")
print("Commit 1")
<<<<<<< HEAD
print("Commit 4")
=======
print("Commit 2")
print("Commit 3")
>>>>>>> newbranch
What the giblets is all that? Git has totally screwed with the contents of my file!
Yes, it has! But not for no reason; let’s examine what’s in there.
We have three delimiters: <<<<<<, ======, and >>>>>>.
Everything from the top delimiter to the middle one is what’s in HEAD (the branch you’re on and merging
into).
Everything from the middle delimiter to the bottom one is what’s in newbranch (the branch you’re merging
from).
So Git has “helpfully” given us the information we need to make a semi-informed decision about what to do.
And here’s exactly the steps we must follow:
1. Edit the conflicting file(s), remove all those extra lines, and make the file(s) Right.
2. Do a git add to add the file(s).
3. Do a git commit to finalize the merge.
Now, when I say “make the file Right”, what does that mean? It means that I need to have a chat with my
teammate and figure out what this code is supposed to do. We clearly have different ideas, and only one of
them is right.
So we have a chat and hash it out. We finally decide the file should look like this:
print("Commit 1")
print("Commit 4")
print("Commit 3")
And then I (since I’m the one doing the merge), edit foo.py and remove all the merge delimiters and every-
thing else, and make it look exactly like we agreed upon. I make it look Right.
Then I add the file to the stage:
Chapter 6. Merging and Conflicts 40
Changes to be committed:
modified: foo.py
Notice that git status is telling me we’re still in the merging state, but I’ve resolved the conflicts. It tells
me to git commit to finish the merge.
What if I added the conflict file too soon? For example, what if you add it but then you realize there
are still unresolved conflicts or the file isn’t Right? If you haven’t committed yet, you have a couple
options. (If you have committed, all you can do is reset or revert.)
One option is to just edit the file again, and re-add it when it’s done. (After editing the file will show
up as a “change not staged for commit” until you add it again.)
Another option is to move the file off the stage with git checkout --merge on the file to get it back
to the “both modified” state. Helpfully, this won’t delete the changes you already added. This is
especially useful if you’re using a merge tool.
So now that we’ve added the file, let’s make the merge commit. Here we’re manually making the merge
commit, unlike above where Git was able to automatically make it.
$ git status
On branch main
nothing to commit, working tree clean
Success!
Just to wrap up, let’s take a look at the log at this point:
$ git log
commit 668b5065aa803fa496951b70159474e164d4d3d2 (HEAD -> main)
Merge: e4b69af 81d6f58
Author: User Name <user@example.com>
Date: Sun Feb 4 13:18:09 2024 -0800
commit e4b69af05724dc4ef37594e06d0fd323ca1b8578
Author: User Name <user@example.com>
Date: Sun Feb 4 13:16:32 2024 -0800
Commit 4
Commit 3
commit 3ab961073374ec26734c933503a8aa988c94185b
Author: User Name <user@example.com>
Date: Sun Feb 4 13:16:32 2024 -0800
Commit 1
We see a few things. One is that our merge commit is pointed to by main (and HEAD). And looking down a
couple commits, we see our now-direct ancestor, newbranch back on Commit 3.
We also see a Merge: line on that top commit. It lists the commit hashes for the two commits that it came
from (the first 7 digits, anyway), since the merge commit has two parents.
This is a shorter chapter, but we want to talk about Git’s behavior when it comes to working in subdirectories
and some gotchas that you probably don’t want to get wrapped up in.
42
Chapter 7. Using Subdirectories with Git 43
This is particularly insidious because if you’re in a subdirectory that you think is a standalone repo, you might
have been misled since Git searches parent folder for the .git directory and it could be finding the spurious
one you accidentally made in your home directory.
We1 recommend against one big repo from your home directory. You should have separate subdirectories
for each of your repos.
If you accidentally create a repo where you didn’t want to, changing a Git repo to a regular subdirectory is as
simple as removing the .git directory. Be careful that you’re removing the correct one when you do this!
One hack you can do to prevent Git from creating a repo in your home directory is to preemptively
put an unwriteable .git directory there.
This way when Git tries to make its metadata folder there, it’ll be stopped because you don’t have
write permission to that .git directory.
(You can always delete this directory with rmdir even if you don’t have write permission to it.)
The file .gitkeep isn’t special in any way, other than convention. The file could be called anything.
For example, if you know you’ll need to eventually put a .gitignore in that directory, you might
just use that instead. Or a README.
1
All Git enthusiasts collectively, that is.
Chapter 8
What if you have files in your subdirectory you don’t want Git to pay any attention to? Like maybe you have
some temporary files you don’t want to see in the repo. Or maybe you have an executable you built from a C
project and you don’t want that checked in because your incredibly strict instructor won’t grade your project
if the repo contains any build products? For example.
That’s what this part of the guide is all about.
$ git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
doom
So I edit a .gitignore file in that directory and add this one line to it:
doom
$ git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
44
Chapter 8. Ignoring Files with .gitignore 45
.gitignore
$ git status
On branch main
nothing to commit, working tree clean
and we’re all clear. That doom file is still there in the working tree, but Git pays it no heed since it’s in the
.gitignore.
subdir/subdir2/foo.txt
That will match anywhere in the repo. If you want to only match a specific file from the repo root, you can
prepend a slash:
/subdir/subdir2/foo.txt
Note that means subdir in the root of the repo, not the root directory of your entire filesystem.
If you put this in your .gitignore:
foo.txt
8.4 Wildcards
Do I have to individually list all the files I don’t want in the .gitignore? What a pain!
Luckily Git supports wildcards in ignored file naming.
For example, if we wanted to block all the files that ended with a .tmp or .swp (Vim’s temp file name)
extension, we could use the * (“splat”) wildcard for that. Let’s make a .gitignore that blocks those:
*.tmp
*.swp
And now any files ending with .tmp or .swp will be ignored.
Turns out that Vim has two kinds of swap files, .swp and .swo. So could we add them like this?
*.tmp
*.swo
*.swp
Sure! That works, but there’s a shorter way where you can tell Git to match any character in a bracketed set.
This is equivalent to the above:
*.tmp
*.sw[op]
You can read that last line as, “Match file names that begins with any sequence of characters, followed by
.sw, followed by either o or p.”
Bad news, though, since *.tmp is ignored at the root level across all subdirectories in the repo! Can we fix
it?
Yes! You can add a new .gitignore to the subdirectory with needed.tmp in it, with these contents:
!needed.tmp
This tells Git, “Hey, if you were ignoring needed.tmp because of some higher-up ignore rule, please stop
ignoring it.”
So while needed.tmp was being ignored because of the root level ignore file, this more-specific file overrides
that.
If you needed to allow all .tmp files in this subdirectory, you could use wildcards:
!*.tmp
And that would make it so all .tmp files in this subdirectory were not ignored
*
!*.c
!Makefile
The first line ignores everything. The next two lines negate that rule for those specific files.
1
https://github.com/github/gitignore
Chapter 9
A remote is just a name for a remote server you can clone, push, and pull from.
We identify these by a URL. With GitHub, this is a URL we copied when we went to clone the repo initially.
It’s possible to use this URL to identify the server in our everyday Git usage, but it’s unwieldy to type. So
we give the remote server URLs nicknames that we just tend to call “remotes”.
A remote we’ve already seen a bunch of is origin. This is the nickname for the remote repo you cloned
from, and it gets set automatically by Git when you clone.
For example, this refers to the main branch on the remote named origin:
origin/main
And this refers to the branch named feature3490 on a remote named nitfol:
nitfol/feature3490
We’ll talk more about this in the Remote Tracking Branches chapter.
$ git remote -v
origin https://github.com/example-repo.git (fetch)
origin https://github.com/example-repo.git (push)
We see that we’re using the same URL for the remote named origin for both pull (part of which is fetch)
and push. Having the same URL for both is super common.
And that URL is the exact same one we copied from GitHub when cloning the repo in the first place.
48
Chapter 9. Remotes: Repos in Other Places 49
$ git remote -v
origin https://github.com/example-repo.git (fetch)
origin https://github.com/example-repo.git (push)
And then you try to push, and GitHub tells you that you can’t push to an HTTPS remote… dang it!
You meant to copy the SSH URL when you cloned, which for me looks like:
git@github.com:beejjorgensen/git-example-repo.git
Luckily it’s not the end of the world. We can just change what the alias points to.
(The example below is split into two lines so that it’s not too wide for the book, but it can be on a single line.
The backslash lets Bash know that the line continues.)
$ git remote -v
origin git@github.com:beejjorgensen/git-example-repo.git (fetch)
origin git@github.com:beejjorgensen/git-example-repo.git (push)
And now we can push! (Assuming we have our SSH keys set up.)
I don’t have access to the real Linux source code, but I can fork it and get my own copy of the repo.
Now, if Linus Torvalds makes changes to his repo, I won’t automatically see them. So I’d like some way to
get his changes and merge them in with my repo.
I need some way to refer to his repo, so I’m going to add a remote called reallinux that points to it:
Normally when setting up a remote that refers to the source of a forked repo on GitHub, people tend
to call that remote upstream, whereas I’ve clearly called it reallinux.
I did this because when we subsequently talk about remote tracking branches, we’re going to use
“upstream” to mean something else, and I don’t want the two to be confusing.
Just remember IRL when you set up a remote to point to the forked-from repo, it’s relatively customary
to call that remote upstream.
Now I can run this to get all the changes from Linus’s repo:
And I can merge it into my branch (the Linux repo uses master for the main branch):
That will merge the master branch from the reallinux into my local master, once we’ve dealt with any
conflicts.
At this point if I did a git log, I’d see that the latest commit would indicate that my HEAD was attached to
my master branch, and it was pointing to the same commit as the reallinux/master:
This is expected, since I just merged reallinux/master into my master, so they definitely should be
pointing to the same commit.
But looking farther down, I’d see the master branch on my origin lagging behind a few commits:
(origin/master, origin/HEAD)
You might or might not have origin/HEAD depending on how you made your repo. No worries either
way.
At this point I’d do a git push to get them all on the same commit, so the top commit would show:
We’ve seen how to create local branches that you do work on, then merge back into the main branch, then
git push it up to a remote server.
This part of the guide is going to try to clarify what’s actually going on behind the scenes, as well as give us
a way to push our local branches to a remote for safe keeping.
That’s right! When you make a GitHub repo and then clone it, there are two main branches!
How do we differentiate them?
Well, on your local clone, we just refer to branches by their plain name. When we say main or topic2, we
mean the local branch by that name on our repo.
If we want to talk about a branch on a remote, we have to give the remote name along with the branch using
that slash notation we’ve already seen:
Importantly, not only do the words origin/main refer to the main branch on origin in casual conversation,
but you actually have a branch on your local repo called origin/main.
This is called a remote-tracking branch. It’s your local copy of the main branch on the remote. You can’t
move your local origin/main branch directly; Git does it for you as a matter of course when you interact
with the remote (e.g. when you push or pull).
We’re going to call the main branch on our local machines the local branch, and we’ll call the one on origin
the upstream branch.
51
Chapter 10. Remote Tracking Branches 52
And this is going to get confusing later when we name a remote upstream in a way that has nothing
to do with the upstream branch terminology we’re using here.
I want to go over that one more time to drive it home.
Let’s say you have these two branches on your computer because you’ve just cloned the remote repo at the
origin:
When you have those two branches on your computer, there are actually three branches in the world.
1. main on your computer.
2. origin/main on your computer.
3. main on the origin computer, usually a different computer than yours, e.g. one at GitHub or some-
thing.
Notice that the first two of these are on the repo on your computer!
The branch origin/main is just where your computer thinks that main on the origin is. Your computer
got this information the last time you pulled or fetched from origin.
If other people have pushed to main on origin since your last pull, your origin/main on your local com-
puter won’t be up to date.
And normally you don’t have to worry about this much; when you try to push, Git will tell you if someone
else has pushed changes in the meantime and you have to pull first to update your origin/main branch. No
biggie.
But I wanted to spell that out so you had a more complete mental model of what’s happening behind the
curtain, here.
We see my two local branches (main and sphinx). Looking on those two top lines, you see remote tracking
branches in brackets (origin/main and origin/sphinx). When I push or pull from main or sphinx, those
are the remote tracking branches that are merged to.
Additionally, we see information about the remote below that.
Chapter 10. Remote Tracking Branches 53
The first line about remotes/origin/HEAD is a little strange. It just points to origin/main which simply
lets us know that main is the initial branch for the repo that Git will use when you clone it. You typically
don’t need to think about this line.
The remaining two lines tell us what commits the remote tracking branches origin/main and
origin/sphinx are pointing at. Looking closely, we see they’re pointing to the same commits as
our local main and sphinx indicating that everything is in sync. (As far as we know—someone else might
have pushed something to the repo since our last pull and we don’t know about that yet.)
$ git push
and it’ll automatically push to origin/main thanks to your earlier usage of --set-upstream.
And git pull has the same option, as well, though you only need to do it once with either push or pull.
“But wait! I’ve never used --set-upstream, either!”
That’s because by default when you clone a repo, Git automagically sets up a local branch to track the main
branch on the remote.
Depending on how you made your repo, you might also have a reference to origin/HEAD. It
might be weird to think that there’s a HEAD ref on a remote server that you can see, but in this case it’s
just referring to the branch that you’ll be checking out by default when you clone the repo.
“OK, so what you’re telling me is that I can just git push and git pull like always and just ignore everything
you wrote in this section?”
Well… yes. Ish. No. We’re going to make use of this to push other branches to the remote!
Initial checkin
HEAD refers to topic99, and that’s one commit ahead of main (local) and main (upstream on the origin
remote), as far as we know. And we know this because it’s one commit ahead of our remote-tracking branch
origin/main.
$ git push
fatal: The current branch topic99 has no upstream branch.
To push the current branch and set the remote as upstream, use
Ouch. The short of all this is that we said “push”, and Git said, “To what? You haven’t associated this branch
with anything on the remote!”
And we haven’t. There’s no origin/topic99 remote-tracking branch, and certainly no topic99 branch on
that remote. Yet.
The fix is easy enough—Git already told us what to do.
For example:
For example:
And that’s it! Make sure you delete your remote tracking branch
Wait! We’re tracking at remote2? That’s a little weird because it’s the other person’s repo. Maybe you have
permission to write to it, and that’s what you want to do. But it’s more probably you’d like your own version
of this branch on your repo as well.
You can do that by pushing it to your remote with -u again.
foobranch
remotes/origin/foobranch
remotes/remote2/foobranch
If you want to keep your origin/foobranch in sync with that on remote2, you’ll have to do a bunch of
merging.
(You can leave the origin foobranch off the push if you’ve already pushed it with -u earlier, of course.)
At that point, every foobranch should be on the same commit.
Chapter 11
File States
So clearly files can exist in a variety of “states” and we can move them around between those states.
To figure out what state a file is in and get a hint on how to “undo” it from that state, git status is your
best friend (except in the case of renaming, but more on that mess soon).
58
Chapter 11. File States 59
You can remove the file from the stage and back to Modified State with git restore --staged.
A file typically goes through this process to be added to a repo:
1. The user creates a new file and saves it. This file is Untracked.
2. The user adds the file with git add. The file is now Staged.
3. The user commits the file with git commit. The file is now Unmodified and is part of the repo and
ready to go.
After it’s in the repo, the typical file life cycle only differs by the first step:
1. The user changes the file and saves it. The file is now Modified.
2. The user adds the file with git add. The file is now Staged.
3. The user commits the file with git commit. The file is now Unmodified and is part of the repo and
ready to go.
Keep in mind that often a commit is a bundle of different changes to different files. All those files would be
added to the stage before the single commit.
Here’s a partial list of ways to change state:
• Untracked → git add foo.txt → Staged (as “new file”)
• Modified → git add foo.txt → Staged
• Modified → git restore foo.txt → Unmodified
• Unmodified → edit foo.txt → Modified (with your favorite editor)
• Staged → git commit → Unmodified
• Staged → git restore --staged → Modified
Again, git status will often give you advice of how to undo a state change.
$ ls
foo.txt
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: foo.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
foo.txt
Chapter 11. File States 60
$ ls
foo.txt
There you see in the status output that Git has staged the file for deletion, but it’s also mentioning that the
file exists and is untracked. And a subsequent ls shows that the file still exists.
At this point, you can commit and the file would then be in Untracked state.
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: foo.txt
You can overwrite the version on the stage by adding it again. And various incantations of restore can
change the files in different ways. Look up the --staged and --worktree options for git restore.
I’ll leave how to move files around in these simultaneous states as an exercise to the reader, but I wanted you
to at least be aware of it.
Chapter 12
The powerful git diff command can give you differences between two files or commits. We mentioned it
briefly at the beginning, but here we’re going to delve more deeply into all the things you can do with it.
It’s not the easiest thing to read at first, but you do get used to it after a while. My most common use case is
to quickly scan to remember what I’ve changed in the working tree so I know what to add to the stage and
what commit message to use.
$ git diff
diff --git a/hello.py b/hello.py
index 4a8f53f..8ee1fe4 100644
--- a/hello.py
+++ b/hello.py
@@ -1,4 +1,8 @@
def hello():
- print("Hello, world!")
+ print("HELLO, WORLD!")
+
+def goodbye():
+ print("See ya!")
hello()
+goodbye()
61
Chapter 12. Comparing Files with Diff 62
The index line has the blob hashes and file permissions. A blob hash is the hash of the specific
file in the states being compared. This isn’t something you need to worry about, typically. Or maybe
not even ever.
After that we have a couple lines indicating that the old version of the file a/hello.py is the one marked
with minus signs, and the new version (that you haven’t staged yet) is b/hello.py and is marked with plus
signs.
Then we have @@ -1,4 +1,8 @@. This means that lines 1-4 in the old version are shown, and lines 1-8 in the
new version are shown. (So clearly we’ve at least added some lines here.)
Finally, we get to the steak and potatoes of the whole thing—what has actually changed? Remembering that
the old version is minus and the new version is plus, let’s look at just that part of the diff again:
def hello():
- print("Hello, world!")
+ print("HELLO, WORLD!")
+
+def goodbye():
+ print("See ya!")
hello()
+goodbye()
Rules:
• If a line is prefixed with -, it means this is how the line was in the old version.
• If a line is prefixed with +, it means this is how the line is in the new, modified version.
• If a line is not prefixed with anything, it means it is unchanged between the versions.
The diff won’t show you all the lines of the file! It only shows you what’s changed and some of
the surrounding lines. If there are changes in different parts of the file, the unchanged parts of the file
will be skipped over in the diff.
Another way to read the diff is that lines with a - have been removed and lines with a + have been added.
Let’s say that these two things are true. Now, whether or not they’re true doesn’t really matter.
“It’s only a model.”
—Patsy, Monty Python and the Holy Grail
1. The stage contains a copy of all unmodified files at your current commit.
2. A git status or git diff only shows files that differ between your working tree and the stage.
So if you don’t have any modifications, git diff won’t show any differences. Because the stage and working
tree are the same.
Now if you modify a file in your working tree and then git diff, you will see some changes, because the
working tree differs from the stage.
But then if you add the modified file to the stage, then the stage and working tree become the same again.
And git diff will show no differences.
git diff always compares the working tree to the stage. (Unless you’re diffing specific commits—see
below.) And in this case, after you’ve added your modified file to the stage, it’s the same as the working tree.
So no diffs.
Contrast this to where you’ve modified the working tree but haven’t added the file to the stage. In this case,
the file on the stage is just like the last commit, which is different than your working tree. So git diff
shows the differences.
Well, okay, then… what if you want to diff what’s on the stage with the last commit? That is, instead of
diffing the working tree with the stage, you want to diff the stage with the HEAD?
Back to the punchline:
And that’ll do it. This will run a diff between what’s on the stage and the last commit, showing you the
changes you’ve staged.
Or use HEAD:
Chapter 12. Comparing Files with Diff 64
Or relative HEAD:
That last one diffs three commits before HEAD with four commits before HEAD.
But since HEAD~4 is the parent of HEAD~3, is there some shorthand we can use here? Yes!
You can use it anywhere you want to compare a commit with its parent, which is really showing just what
changes were in that one particular commit.
$ git diff -w
$ git diff --ignore-all-space # Same thing
Chapter 12. Comparing Files with Diff 65
Finally, you can restrict to a file extension using a glob and single quotes:
But sometimes you want to know what changed in a branch since the branches diverged.
That is, you don’t want to know what’s different now between branch1 and branch2, which is what the
above would give you.
You want to know what branch2 added or deleted that branch1 did not.
In order to see this, you can use this notation:
This means “diff the common ancestor of branch1 and branch2 with branch2.”
In other words, tell me all the changes that were made in branch2 that branch1 is unaware of. Don’t show
me anything that branch1 has changed since they diverged.
12.4 Difftool
I know the diff output is tough to read. I recommend practice and offer myself as living proof that with
enough practice, the output becomes penetrable. And eventually it even becomes easy to read, which might
be difficult to imagine. But it does!
That said, there are third-party tools that exist to make diffs more manageable, and Git supports these tools.
You can read more about it in the diff tool chapter.
Chapter 13
This is an extension of dealing with file states, so make sure you read that chapter first!
Also, I’m going to interchangeably use the terms rename and move to mean the same thing. Moving as a
concept is a little more powerful because not only can it rename, but it can also move files to other directories.
It’s notable because the command to rename is git mv.
So it knows the file is renamed, and the file has been moved to the stage. Like so:
• Unmodified → git mv foo.txt bar.txt → Staged (as “renamed”)
And if we look, we see the file has actually been renamed in the directory to bar.txt, as well.
If we make a commit at this point, the file will be renamed in the repo. Done.
But what if we want to undo the rename?
Git suggests git restore --staged to the rescue… But which file name to use, the old one or new one?
And then what? It turns out that while you can use git restore to undo this by following it with multiple
other commands, you should, in this case, ignore Git’s advice, and instead read the following section.
66
Chapter 13. Renaming and Removing Files 67
$ git rm foo.txt
rm 'foo.txt' # This is Git's output
This actually removes the file—if you look in the directory, it’s gone.
But let’s check the status:
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: foo.txt
So the now-deleted file is in Staged State, as it were. Which makes sense since there’s now a “difference”
between the working tree (where the file is gone) and the stage (where the file still exists).
If we do a commit here, the file is deleted. Done.
no changes added to commit (use "git add" and/or "git commit -a")
Hmmm. “Changes not staged for commit” are files in Modified State. This means that foo.txt has been
“modified”, which is, in this context, a friendlier way of saying “deleted”.
So we’ve backed up from Staged State to Modified State. But looking around, the file is still gone! I want
my file back!
We want to move it back to Unmodified State, which Git once again hints how to do in the status:
git restore. Let’s try:
Git’s telling us there are no Modified files here. Let’s look and see:
$ ls foo.txt
foo.txt
commit 1c9bf4514ee90a0e65fb9b0a916765bb6c78dee6
Author: User <user@example.com>
Date: Sun Jan 12 11:08:33 2025 -0800
commit cc7a1940f13fca9092dbe9ce4a8e9012babd9314
Author: User <user@example.com>
Date: Sun Jan 12 11:08:33 2025 -0800
Initial splungification
$ git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
foo.txt
It’s not even added yet. So if you want to bring it back to life, you’ll have to add and commit it just as if it
were a brand new file.
MASTER_PASSWORD_FOR_THE_ENTIRE_COMPANY=pencil
Let’s make the infraction less severe. Let’s say you’ve pushed to GitHub, but it’s a private repo. It’s still
kinda bad. You have to trust everyone who has access, and trust that no clones of the repo will ever end up
in the hands of anyone outside the company or those of any disgruntled employees. The only recourse is to
change that password.
Okay. Let’s make it even less severe, still. Let’s say you’ve committed the password to your repo, but you
haven’t yet pushed.
Now we can do something about it because there’s no chance anyone other than you has seen the code. You
didn’t push it, so no one can have pulled it. But we’ll not talk about that here; see the chapter on Amending
Commits for fixing the file before the push.
Chapter 14
Let’s say you’re on a team of coders and you all have access to the same GitHub repo. (One person on the
team owns the repo, and they’ve added you all as collaborators1 .)
I’m going to use the term collaborator to mean “someone to whom you have granted write access to
your repo”.
How can you all structure your work so that you’re minimizing conflicts?
There are a number of ways to do this.
• Everyone is a collaborator on the repo, and:
• Everyone uses the same branch, probably main, or
• Everyone uses their own remote tracking branch and periodically merges with the main branch,
or
• Everyone uses their own remote tracking branch and periodically merges with a development
branch, which itself is periodically merged into main for each official release.
• Or everyone has their own repo (and are not collaborators on the same repo), and:
• Everyone uses pull requests or other synchronization methods to get their repos merged into the
other devs’.
We’ll look at the first few ways in this chapter, but we’ll save pull requests for later.
There’s no one-size-fits-all approach to teamwork with Git, and the methods outlined below can be mixed
and matched with local topic branches, or people having multiple remote tracking branches, or whatever.
Often management will have an approach they want to use for collaboration which might be one of the ones
in this section, or maybe it’s a variant, or maybe its something completely different.
In any case, the best strategy for you, the learner, is to just be familiar with the tools (branching, merging,
conflict resolution, pushing, pulling, remote tracking branches) and use them for effect where it makes the
most sense.
And when you’re first starting out, your intuition about “where it makes the most sense” might not be dead-on,
but it probably won’t be lethal and you’ll figure it out in the school of hard knocks.
“Oh great. Another f---ing learning experience.”
—Actual quote from my mother
Finally, I’ll be using GitHub in all these examples, but you could use any server or service as the remote
instead.
1
https://tinyurl.com/y5kzpeyk
71
Chapter 14. Collaboration across Branches 72
Drawbacks:
• If your branch diverges too far from main, merging might become painful.
• Unless you’re rebasing and squashing, the incremental work on your branch might “pollute” the com-
mit history on main with a lot of tiny commits.
Initial setup:
• One person makes the GitHub repo.
• The owner of the GitHub repo adds all the team members as collaborators.
• Everyone clones the repo.
• Everyone makes their own branch, possibly naming it after themselves.
• Everyone pushes their branch to GitHub, making them remote-tracking branches. (We do this so that
your work is effectively backed up on GitHub when you push it.)
Workflow:
• Work is delegated to all collaborators. The work should be as non-overlapping as possible.
• As collaborators finish their tasks, they will:
• Test everything on their branch.
• Merge the latest main into their branch; do a pull to make sure you have it. (The collaborator
might already have the latest main if no one else has merged into it, which will cause Git to say
there’s nothing to do. This is fine.)
• Test everything, and fix it if necessary.
• Merge their functioning branch into main.
• Push.
• If someone else has modified main while you were testing, Git will complain that you have
to pull before you can push. If there’s a conflict at this point, you’ll have to resolve, test,
and push it. And you’ll have to merge main back into your branch so that your branch is
up-to-date.
The result will look something like Figure 14.1 to start, where all the collaborators have made their own
branches off of main.
Let’s say Chris (on branch chris) finishes up their work and wants other contributors to be able to see it.
It’s time to merge into main, as we graphically see in Figure 14.2.
After that, other contributors who pull main will see the changes.
Overall the process works as in Figure 14.3. This is a busy image, but notice how Bob and Alice are only
merging their work into the dev branch, and then every so often, their manager merges the dev branch into
main and tags that commit with a release number.
Benefits:
• All the benefits of everyone having their own branch.
• You have an internal branch from which you can make complete builds for internal or external testing.
Drawbacks:
• A little more complexity and management.
• If your branch diverges too far from dev, merging might become painful.
• If the dev branch diverges too far from main, merging might become painful.
• Unless you’re rebasing, the incremental work on your branch might “pollute” the commit history on
dev and main with a lot of tiny commits.
Initial setup:
• One person makes the GitHub repo.
Chapter 14. Collaboration across Branches 75
• The owner of the GitHub repo adds all the team members as collaborators.
• The owner creates the dev branch.
• Everyone clones the repo.
• Everyone makes their own branch, possibly naming it after themselves.
• Everyone pushes their branch to GitHub, making them remote-tracking branches. (We do this so that
your work is effectively backed up on GitHub when you push it.)
Workflow:
• Work is delegated to all collaborators. The work should be as non-overlapping as possible.
• As collaborators finish their tasks, they will:
• Test everything on their branch.
• Merge the latest dev into their branch; do a pull to make sure you have it. (The collaborator
might already have the latest dev if no one else has merged into it, which will cause Git to say
there’s nothing to do. This is fine.)
• Test everything, and fix it if necessary.
• Merge their functioning branch into dev.
• Push.
• If someone else has modified dev while you were testing, Git will complain that you have
to pull before you can push. If there’s a conflict at this point, you’ll have to resolve, test,
and push it. And you’ll have to merge dev back into your branch so that your branch is
up-to-date.
Managerial Workflow:
• Coordinate with all devs to get a candidate release in dev tested out and ready.
• Merge that candidate release (some commit) from dev into main.
• Tag the main commit with some version number, optionally.
Chapter 15
I’m going to start with the Number One Rule of Rebasing: never rebase anything that you have pushed.
That is, only rebase local changes that no one else has seen. You can push them after the rebase.
This is more of a guideline than a rule in that you can rebase things you’ve pushed if you understand the
consequences. It’s typically not a great situation, though, so you’ll want to generally avoid it.
The reason is that rebasing rewrites history. And that makes your history get out of sync with the history of
other devs who have cloned the repo with the old history, and it makes syncing up quite challenging.
There are other commands in Git that also rewrite history. And the general rule is never rewrite history on
anything that’s already been pushed. Unless you really know what you’re doing.
Then you hear that someone has made a change to main and you want to roll those changes into your topic
branch, but not necessarily get your changes in main yet.
At this point, if we wanted to get the changes in main into topic, our merge option was to make another
commit, the merge commit. The merge commit contains the changes from two parent commits (in this case
the commit labeled (2) and the one labeled (4) are the parents) and makes them into a new commit, marked
(5) in Figure 15.2.
If we look at our log at that point, we can see the changes from all the other commits in the graph from the
topic branch.
76
Chapter 15. Rebasing: Moving Commits 77
And we’re good at this point. That worked, and it did what we wanted. Merging is a completely acceptable
solution to this problem.
But there are a couple drawbacks to doing the merge. See, we really just wanted to get the latest stuff from
main into our branch so we could use it, but we didn’t really want to commit anything. But here we’ve made
a new commit for everyone to see.
Not only that, but now the commit graph forms a loop, so the history is a little more convoluted than perhaps
we’d like it.
What really would have been nice is if I could just have taken commits (3) and (4) from topic and just
somehow applied those changes to (2) on main. That is, could we pretend that instead of branching off (1)
like topic did, that we instead branched off (2)?
After all, if we branched off (2), then we’d have those changes from main that we wanted.
What we need is a way to somehow rewind our commits back to the branch point at (1), and then reapply
them on commit (2). That is, the base of our topic branch, which was commit (1), needs to be changed
to another base at commit (2). We want to rebase it to commit (2)!
After that, we’ll do the same thing with commit (4). We’ll apply the changes from old commit (4) to (3'),
making a new commit (4').
And if we do that, we end up with Figure 15.3.
And there you see (3') and (4') now rebased onto main! And now the topic branch includes commit (2)
from the main branch!
Again, these two commits have the same changes that you originally had in commits (3) and (4), but now
they’ve been applied to main at commit (2). So the code is necessarily different since it now contains the
Chapter 15. Rebasing: Moving Commits 78
changes from main. This means your old commits (3) and (4) are effectively gone, and the rebase has
replaced them with two new commits that contain the same changes, just on a different base point.
We just changed history. When we mentioned rewriting history at the top of this chapter, this is what
we were talking about. Imagine some other dev had your old commits (3) and (4) and was working
off those making their own new commits. And then you rebased effectively destroying commits (3)
and (4). Now your commit history is different than the other dev’s and all kinds of Fun™ will be
had trying to sort if out.
If you only rebase commits that you haven’t pushed, you’ll never get into trouble. But if some other
dev has a copy of your commits (because you’ve already pushed them and they pulled them), don’t
rebase those commits!
$ git pull
(Recall that origin/main is your remote-tracking branch—it’s the version of main that’s on origin, not
the main on your local machine.)
But merging isn’t the only thing you can do there. Given that this is the chapter on rebasing, you might
correctly suspect that we can make it do a rebase instead.
And here’s how:
If you want that to be the default behavior for the current repo, you can run this one-time command:
Chapter 15. Rebasing: Moving Commits 79
If you want it to be the default behavior for all repos, you can:
If you’ve configured your repo to always rebase on a pull, you can override that to force a merge (if you
want) with:
15.5 Conflicts
When you do a merge, there’s a chance that you might conflict with some of the changes in the other branch,
and you have to resolve those, as we’ve seen.
Can the same thing happen with a rebase?
Of course! If the commit you’re trying to rebase onto conflicts with your commit, you’ll have the same
trouble you’d have with a merge.
Luckily, Git will let you resolve the conflict in a way similar to the merge.
Let’s start with a simple example. I’m going to have a text file that contains the following:
Whoa, Nelly. OK, so it can’t do that. It says we need to “Resolve all conflicts manually”, and then add them,
and then we’ll run rebase again with the --continue flag to continue the rebase.
If you keep reading the hints, you’ll see there some more stuff in there. We’ll get to --skip later,
but do note that if the conflict is more than you want to take on right now, you can just run:
1 <<<<<<< HEAD
2 The magic number is 2
3 =======
4 The magic number is 3
5 >>>>>>> 9f19221 (Update to 3)
That’s just like in a merge conflict—Git is showing us the two choices we have for this line. So we’ll consult
with the team and come to an agreement on what should be in the file, and we delete everything that shouldn’t
be there and we make it Right.
$ git status
interactive rebase in progress; onto 6ceeefb
Last command done (1 command done):
pick 9f19221 Update to 3
No commands remaining.
Chapter 15. Rebasing: Moving Commits 81
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: magic.txt
no changes added to commit (use "git add" and/or "git commit -a")
What? Oh, we should have read more of the status message. It says to use git add to mark resolution of the
file magic.txt. Let’s do that.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: magic.txt
That status looks nicer. (But note that Git’s in a special “rebase” state similar to how it gets into a special
“merge” state when merging. We have to either abort or continue before we can use Git normally again.)
Now --continue.
This pops me into my editor to edit the commit message. This is your opportunity to change the commit
message if it no longer reflects the commit. (That is, if you changed the commit when resolving the conflict
to be something entirely different, you might need to edit the message.) Edit it if necessary and save it.
And Git says:
After all that, we see our new commit graph in Figure 15.5.
Done.
Wait! There’s another typo! Are you kidding me?
So you fix it:
That’s not super clean, right? Really this was supposed to be one commit that implemented feature #121.
But luckily you haven’t pushed yet, which means you’re still free to rewrite that history!
You can use a feature of rebase called squashing to get this done.
What you want to do is squash those two typo fixes into the previous commit, the one where you first tried
to implement the feature.
First, let’s look at the log.
$ git log
commit c1820e6d0da19013208b389d264310162477b099 (HEAD -> main)
Author: User <user@example.com>
Chapter 15. Rebasing: Moving Commits 83
commit c62c0db7b82e6b415d36bd0f00d568fd503164b7
Author: User <user@example.com>
Date: Wed Jul 17 11:53:10 2024 -0700
Fixed typo
commit ab84a428b8baae0078ee0647a67b34a89a6abed8
Author: User <user@example.com>
Date: Wed Jul 17 11:53:10 2024 -0700
commit a95854659e31d203e2325eee61d892c9cdad767c
Author: User <user@example.com>
Date: Wed Jul 17 11:53:10 2024 -0700
Added
Since this is a rebase, we’re going to rebase onto something, namely the commit prior to the added feature
commit, the commit ID starting with a9585.
And we want to do it interactively, which is a special rebase mode that lets us do the squashing, and we get
there with the -i flag.
This brings us into an editor that has this information, and a huge comment block below it full of instructions.
Notice that they’re listed in forward order instead of the reverse log order we’re used to.
Look at all those options shown in the comment block (and not shown here in the guide)! Pick, reword, edit,
squash, fixup… so many things to choose from. As you might imagine we’re in a pretty powerful history
rewriting mode.
For now, though, let’s just look at “squash” and “fixup”, which are almost the same thing.
Starting with “squash”, what I want to do is take those typo fix commits and work them into the “Added
feature” commit. We can use the squash mode to do this.
I’ll edit the file to look like this:
That will squash “Fixed another typo” into “Fixed typo” and then squash that result into “Added feature
#121”.
Chapter 15. Rebasing: Moving Commits 84
There are shorthand versions for all these commands. I could have used s instead of squash.
After I save the file, I get launched right back into another editor that has this in it:
8 Fixed typo
9
We’re making a new rebased commit here with the three commits squashed into one, and so we get to write
a new commit message. Helpfully, Git has included all three commit messages. Let’s hack it down to just
have the commit message we want.
What’s that about detached HEAD? Git detaches the HEAD briefly when doing a rebase. Don’t
worry—it gets reattached for you.
Now my commit history is all cleaned up.
commit a95854659e31d203e2325eee61d892c9cdad767c
Author: User <user@example.com>
Date: Wed Jul 17 11:53:10 2024 -0700
Added
And you can see, if you look at the earlier log, that the “Added feature” commit ID has changed. We did a
rebase, after all, so those old commits are gone, replaced by the new ones.
Chapter 15. Rebasing: Moving Commits 85
Finally, after all this, now you can push. And always remember that since this is a history rewrite, you
shouldn’t do it after you’ve pushed.
And Git log only shows the “Added feature #121” commit. With fixup, Git automatically discards the
squashed commit messages.
This is why you can conclude a merge with a simple commit, but you have to conclude a rebase by repeatedly
running git rebase --continue until all commits have been rebased cleanly.
Is this good or bad? It might be better in that you get a chance to merge each commit in isolation so it might
be easier to reason about and avoid errors. But at the same time it’s more legwork to get through it.
As always, use the right tool for the job!
Chapter 16
If you’re in the middle of working on something and you realize you want to pull some changes in, but you’re
not ready to make a commit because your stuff is still completely broken, git stash is your friend. It takes
the stuff you’re working on and stashes it away on the side, returning your working tree to the state of the
last commit.
So your changes will look like they’re gone—but don’t worry, they’re safely stashed away and you can bring
them back later.
Then you can pull the new stuff down so you’re up-to-date, and then unstash your stuff on top of it.
It’s kind of like a mini rebase in spirit.
16.1 Example
Let’s say we’re all caught up to the latest.
$ git pull
Great. And we start hacking. We open an existing file foo.rs and add some code to it as per usual.
Then Chris calls from the next desk over and says, “Hey wait—I just made a critical update to main and you
should use that!”
And you think, “Well, heck, I was in the middle of something.” You’re not ready to commit, but you want
Chris’s changes.
So you save your files and then run this:
$ git stash
Saved working directory and index state WIP on main: c72c245
some very descriptive commit message
And, if you were watching, you might have seen your file in your editor change back to what it used to be!
Your changes have been undone and stashed away!
If you git status at this point, you’ll see:
86
Chapter 16. Stashing: Temporarily Set Changes Aside 87
$ git status
On branch main
Your branch is up to date with 'origin/main'.
It’s all clean, which means now you can pull and get the latest main. So you do that.
$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: (from 0)
Unpacking objects: 100% (3/3), 943 bytes | 943.00 KiB/s, done.
From /home/beej/tmp/origin
10a8ad6..e286011 main -> origin/main
Updating 10a8ad6..e286011
Fast-forward
foo.rs | 1 +
1 file changed, 1 insertion(+)
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (046ac112f8c02c3dc02984ad71d353a3e5be9a7a)
Auto-merging sounds good. Looks like things went well. And if we look at our file now we’ll see our changes
brought out of the stash and reapplied. Our file foo.rs is in “modified” state and ready for us to work on,
or add and commit.
Yes, Git tracks stashes in a stack. If you’re not familiar with a stack, read up on it first.
• git stash pushes the working tree on the stash stack.
• git stash pop pops the top of the stash stack and applies it to the working tree.
1
https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
Chapter 16. Stashing: Temporarily Set Changes Aside 88
Similarly stash drop will pop the top of the stack and not apply the changes to the working tree, discarding
them instead.
And stash drop can also operate on a particular stash by name if you want to drop something from the
middle of the stack.
16.3 Conflicts
Now that you’ve spent so much time reading about conflicts during merge and rebase, you might start to get
a little worried here.
What if I stash then pull, but then popping the stash does something that conflicts with the changes I pulled?
Can that happen?
Of course it can. Hooray.
When it happens, it looks like this:
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: foo.rs
no changes added to commit (use "git add" and/or "git commit -a")
The stash entry is kept in case you need it again.
Sure looks like a merge conflict, and it looks doubly so in the editor.
Chapter 16. Stashing: Temporarily Set Changes Aside 89
1 fn main() {
2 <<<<<<< Updated upstream
3 println!("This is critically fixed");
4 =======
5 println!("This is sorta working");
6 >>>>>>> Stashed changes
7 }
You can see our stashed changes below where we tried to fix it, but then we see that conflicts with Chris’s
fix from upstream.
So we do the merge thing and make it Right, editing it to look the way we want, and we save it. Our status
is still not clean, though.
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: foo.rs
no changes added to commit (use "git add" and/or "git commit -a")
For a lot of projects you’re on, maybe you have write permission to the main repo that everyone is using.
That is, you’ve been added as a collaborator and you can just push directly.
If that’s the case, you don’t need to fork GitHub repos or create pull requests. You can just keep committing
and pushing as always.
But what if you want to make changes to a repo on GitHub but you don’t have write permission? This might
happen if you forked a repo that you want to make changes to. You can write to your fork, but not to the repo
you forked from. How do you get the changes you made to your fork into the original repo?
Let’s check it out.
A fork is a clone of someone else’s GitHub repo that you’ve made on GitHub using their “Fork” command.
It’s a regular clone except that GitHub is doing some bookkeeping to track which repo you forked from.
The upstream is by convention the name of the remote that you forked from. I know this conflicts with some
other definitions of “upstream”. But for this chapter, in the context of forks, let’s assume it means this.
A pull request (or “PR” for short) is a way for you to offer changes you’ve made to your fork to the owner
of the original repo.
Forks and Pull Requests are a GitHub thing, not a Git thing. It’s some additional functionality
that GitHub has implemented on their website that you can use.
Let’s say, for example, you found an open source project you liked and there was a bug in it. You don’t have
permission to write to the project’s GitHub repo, so how can you change it?
The process for someone making a pull request is:
1. On GitHub, fork the repo. Now you have your own clone of it.
2. Clone your repo to your local machine. Now you have two clones of it: your fork on GitHub and its
clone on your local machine. (And you own them both.)
3. Make the fix on your local machine and test it.
4. Push your fix to your GitHub fork.
5. On GitHub, create a pull request. This informs the upstream owner that you have changes you’d like
them to merge.
6. On GitHub, the upstream owner reviews your PR and decides if they want to merge it. If so, they
merge it. Otherwise they comment and ask for changes, or delete it.
7. At this point, if you’re done, you can optionally delete your fork.
Let’s give it a try! Feel free to issue PRs on my sample repo, used below. I’m just going to delete them (they
won’t be merged); don’t take it personally—I just don’t have time to review them all.
90
Chapter 17. GitHub: Forking and Pull Requests 91
And then you can cd into that directory and see the files there.
$ cd git-example-repo
$ ls
hello.py README.md
1 #!/usr/bin/env python
2
3 print("Hello, world!")
4 print("This is my program!")
1
https://github.com/beejjorgensen/git-example-repo
Chapter 17. GitHub: Forking and Pull Requests 92
5 print("This is my modification")
Again, this just pushed to our fork, not the upstream. You can look at your fork’s page on GitHub and see
the change there.
To review the code, look right below the description to the contributor’s avatar and the commit message.
Click on the commit message and you’ll see a diff, as described in the Comparing Files with Diff chapter.
Lines marked with a + are added, and lines marked with - are removed.
Chapter 17. GitHub: Forking and Pull Requests 94
If you just want a straight up view of the file as it was edited by the contributor, you can hit the “…” on the
right and then “View file”.
If it’s almost right but you need to make a modification, you can also hit “Edit file” and add commits directly
to the PR.
If everything looks good, scroll down and hopefully you’ll see some text that reads “This branch has no
conflicts with the base branch” and “Merging can be performed automatically”. This is good news.
If it says that, you can just click “Merge pull request”, and that will add the changes to your repo and close
the PR. It’s nice to also add a comment thanking the contributor—they just gave you work for free, after all!
Closing a PR doesn’t delete the PR. You can still reopen it.
But let’s say the PR does conflict and can’t be automatically merged. GitHub complains that “This branch
has conflicts that must be resolved” and gives you some options.
As the upstream owner, you can click the “Resolve conflicts” button and fix the issue if possible.
Or you can just reject the PR and ask the person who opened it to resolve the conflict so that your life might
be made easier with an automatic merge.
$ vim readme.txt
$ git add readme.txt
$ git commit -m "feature 1"
[feature1 1ad9e92] feature 1
1 file changed, 1 insertion(+)
Now you can jump back to GitHub and issue a PR. (And look at the response from the remote helpfully
telling you the GitHub URL to visit to issue the PR!)
In the GitHub UI, there might be a handy little popup there saying “feature1 had recent pushes 4 minutes
ago” and a button “Compare and pull request” you can click to make the PR.
But if it’s been too long and the popup is gone, not to worry. See the branch selector button on the upper left
that probably says “main” right now? Pull it down and select the branch “feature1” that you want to create
the PR for. Then click “Contribute” and open the PR.
There’s a line at the top of the PR that indicates the repo and branch that will be merged into, and, on the
right, your repo and branch name that you’ll be merging from.
The rest of the PR proceeds as normal.
Don’t delete your branch until after the merge! Once it has been safely merged, GitHub will pop up a
“Delete branch” button for you on the PR page. This will delete the branch on GitHub, but you’ll still have
to delete feature1 and origin/feature1 on the command line.
And then we need to get the new commits from the upstream repo and merge them into our stuff.
$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Chapter 17. GitHub: Forking and Pull Requests 97
If we jump back to the GitHub UI at this point and open a PR, it should tell us “These branches can be
automatically merged” which is music to everyone’s ears.
Once you have the upstream remote set up, all you have to do to sync in the future is do the
git fetch upstream and then merge or rebase your stuff with it.
Chapter 18
Let’s say you made some changes and committed them, but they actually botched everything up. You want
to just revert to an earlier version of the file.
There’s the cheesy way to do this that might have already occurred to you: detach the head to an earlier
commit where the file was like you wanted it, make a copy of the file someplace safe, then reattach the head
to main, then copy the old file over the existing one in your working tree. And add and commit! And this
would work…
But let’s be more proper, and we can do that with git revert.
Reverting allows us to actually undo the changes of a single commit, even if it wasn’t the one that got you to
the point you’re at now. That is, let’s say you’ve made 30 commits, but it turns out you don’t actually want
commit number 4 to be there any longer. You can revert just that one!
Performing a standard revert will actually make a new commit, and doesn’t erase any old commits. In this
way, it’s not rewriting history so using this method is safe to revert commits that have already been pushed.
commit 9fef4fe6d42b91c12b5217829e8d98d738f84d61
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Jul 26 16:59:44 2024 -0700
Added Line 50
and you decided you didn’t want that commit any longer, you could revert it by its commit ID. Here I’ll just
type the first few characters of the hash because that’s enough:
98
Chapter 18. Reverting: Undoing Commits 99
There’s no conflict (more on that, below) in this example, so it just pops me into my editor and allows me to
edit the commit message. Remember that the revert makes a new commit!
$ git log
commit de415f4f0cd645b1e551a6ac56e13f73850c88db (HEAD -> main)
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Jul 26 17:01:54 2024 -0700
You can revert any commit, even commits that were themselves reverts! Revert the revert!
Now that was an example where the revert went smoothly. But what if you’ve made some changes since the
revert commit that were close to the changes in the revert commit itself? Can it conflict? Of course it can!
And it points out we have a few options here. We can get even more info with our friend git status:
Chapter 18. Reverting: Undoing Commits 100
$ git status
On branch main
You are currently reverting commit 5af89a8.
(fix conflicts and run "git revert --continue")
(use "git revert --skip" to skip this patch)
(use "git revert --abort" to cancel the revert operation)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: foo.txt
no changes added to commit (use "git add" and/or "git commit -a")
And there will be two new revert commits after that. You’ll edit two revert commit messages over the course
of that revert.
You can also specify a range of commits. Be sure to do this in oldest-to-newest order, or you’ll get an
empty commit set passed error.
Again, that will make a lot of commits, one per revert. You can squash those commits if you want to, or you
can use -n (“no commit”) to keep Git from committing until you’re ready.
Chapter 18. Reverting: Undoing Commits 101
At this point, the file is staged with those two commits reverted. And you can now make a single commit
that holds them. And you can do the same thing specifying a range.
Of course, there might a conflict, and you’ll have to resolve those in the super fun way we’ve already dis-
cussed.
Chapter 19
Before we begin, using git reset rewrites history. This means that you shouldn’t use it on any branches
that other people might have copies of, i.e. branches that you have pushed.
Of course, this is a highly-recommended guideline, not a rule, and you can reset anything provided you know
what you’re doing and have good communication with your team.
But if you never reset a branch you haven’t pushed, you won’t get into trouble.
So what is it?
Doing a reset allows you change where the HEAD and your current branch point to. You can move your current
branch to a different commit!
When you move a branch to another commit, the branch “becomes” the repo at the point of that commit,
including all the history that led up to that commit. The upshot is that all the commits that led to the old
branch point are now effectively gone, as shown in Figure 18.1.
Figure 19.1: If we reset main to commit 2, commits 3 and 4 will eventually be lost.
102
Chapter 19. Reset: Moving Branches Around 103
Turns out we have three options: soft reset, mixed reset, and hard reset.
And which you choose controls what happens to the branch, the stage, and the working tree.
I want you to consider that all files exist in three places at all times in Git: the working tree, the
stage, and a commit.
And you’re supposed to say, “Wait—the stage has all the files on it? But I haven’t added anything to
it!”
Yes. I’m saying that when your working tree is clean that means that the files in the HEAD commit,
the files on the stage, and the files in your working tree are all the same. And, yes, all the files exist
in all three places!a
And git status won’t show anything because there are no differences between these three places.
And git status shows the differences.
Let’s say you modified a file in your working tree. In that case, git status would show you a
difference between your working tree and the stage as a “modified file”. But there would still be no
difference between the stage and the HEAD commit, so nothing would show as “ready to commit”.
Then let’s say you added the file to the stage. At the point, a copy of the file from the working tree
is placed on the stage. So now the working tree and the stage are the same. And nothing shows as
“modified”. But now, crucially, the stage differs from the HEAD commit! So now git status shows
that difference as “ready to commit”.
Finally, let’s say that before you committed, you modified the file again in the working tree. Now the
file in the working tree is different than the stage. And the file on the stage is different than the HEAD
commit! Now the file shows up as both “ready to commit” and “modified”.
The reason I want us to think about things this way is because it will make this whole thing with
git reset easier to digest. Sometimes a reset will change the files in the working tree, sometimes
on the stage, and sometimes both.
a
Who knows what it really does under the hood, but we’re going to use this as a mental model for how things work.
Note: in the following examples, I’m going to use the term “old commit” to refer to where the branch was
before the reset, and “new commit” to refer to where it will be after the reset.
With all three variants, the current branch moves to the new (specified) commit.
The summary of differences is:
• Soft:
• Stage: old commit
• Working tree: old commit
• Result: All the old files will show on the stage as “ready to commit”.
• Mixed:
• Stage: new commit
• Working tree: old commit
• Result: All the old files will show in the working tree as “modified”.
• Hard:
• Stage: new commit
• Working tree: new commit
• Result: All the old files will be gone, and the working tree and stage will be clean.
The upshot is that git status will show your old commit’s changes as staged, and none of the files as
modified.
In other words, you’ll see the old state of your files on the stage ready to commit.
A common use for this might be to collapse some of your previous commits similar to what we did with
rebase and squashing commits.
Let’s say we have commits like this (pretend the numbers are the commit hashes):
That’s a gnarly-looking commit history. It would be nice to rewrite it (but if and only if you haven’t pushed
it yet!).
We can do that with a soft reset back to commit 111.
If we do this soft reset:
$ git reset --soft 111 # Again, pretend 111 is the commit hash
We’ll then be in this point with all the other commits gone…
Except importantly our files as they existed in commit 555 will now be staged and ready to commit. That
means with the soft reset the changes weren’t lost, but effectively commits 222-555 are all squished together
on the stage.
So we commit them:
And now, finally we can push, happy that our changes are presentable to the general public.
Again, we’ve rewritten history here. Don’t do this if you’ve already pushed those commits past the
one you’re resetting to.
Chapter 19. Reset: Moving Branches Around 105
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: bar.txt
modified: foo.txt
And we want to reset foo.txt off the stage, but leave bar.txt on there.
Again, we’d use git restore --staged in these modern times. But we’re going to press on here
for the sake of example.
So let’s specify just that file:
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: bar.txt
1. Do the reset.
2. Do a forced push to your remote. For your protection, Git won’t push in this circumstance. You have
to override with a forced push.
Your coworkers will do something like this:
1. Do a git fetch to get the new branch position from the remote.
2. Stash or commit any local changes they need to preserve.
3. Maybe make a new branch at the old branch point in case they need to return to see old soon-to-be-
obliterated commits.
4. Do a reset of the branch in question to the remote branch commit. For example, if we’re resetting the
main branch, you would git reset --hard origin/main.
5. Pop their changes from the stash, if any.
6. Maybe apply earlier commits that got obliterated4 .
Note that your coworkers don’t necessarily need to do a hard reset; they could do a mixed reset, for instance.
If that happens, you’ll have to talk to your team to get them to stop, and then pull the changes, make sure
everyone is on board with the new reset, and then start again.
We’ll use --force-with-lease in our examples.
4
Perhaps using git reflog and git cherry-pick or git cherry-pick -n and potentially git add -p, all of which are covered
in later chapters. Along with judicious use of rebase, old commits or parts of old commits can be applied while keeping the commit
history clean.
5
https://en.wikipedia.org/wiki/Battle_of_Mobile_Bay#“Damn_the_torpedoes”
Chapter 19. Reset: Moving Branches Around 108
So far no harm, but now we’re going to push this history change to our origin. And we’ll use
--force-with-lease for safety.
Now we’ve publicly rewritten history. Tell the team, which you’ve been in contact with this entire time, that
you’ve done so. And they can begin to fix up their clones with much grumbling.
$ git stash
And perhaps make a new branch right here so we can revisit the old state of affairs for reference if we have
to:
And now it’s time for action. We need to fetch the new branch information.
We just fetched, so our clone doesn’t look different to us yet. But let’s put an end to that and get on the same
page as origin. This involves resetting our local main to be the same as it is on the remote tracking branch
origin/main. (Remember the latter has been force pushed to a different commit, and we want our main to
point to that commit, as well.)
Assuming we’re on branch main right now:
And if you want to refer to any old commits and you set up the oldmain branch as above, you can
git switch oldmain to examine them, and maybe use something like git cherry-pick to bring in any
functionality you need.
Chapter 19. Reset: Moving Branches Around 109
$ git log
commit 97c4da49eda8de7b273003515a660945c (HEAD -> topic1)
Author: User <user@example.com>
Date: Thu Aug 1 14:22:39 2024 -0700
See what happened to main? It moved to the current commit! You can see it in the output for the second
git log.
You could also specify a destination for main as a second argument if you wanted it to move somewhere
other than your current location.
All this time you’ve been committing things, branching, doing whatever. And Git’s been watching you,
listening like Big Brother, recording everything you do.
And you can use this to your benefit.
Let’s say you’ve done something like a hard reset because you wanted to abandon the branch you were on.
But then, wait! You actually needed something from one of those commits you just reset past! Is there any
way to get back to it? There’s no branch there, and you can’t remember the commit ID. And since it’s not an
ancestor to anything, git log won’t help you.
How can you get it back?
git reflog to the rescue!
The reflog contains a record of all manner of things you’ve done along with commit IDs, and it keeps them
for 90 days1 . After that time, orphan commits (that is commits with no branch above them) will be garbage
collected.
110
Chapter 20. The Reference Log, “reflog” 111
At this point let’s say we want to look back at the commits we made on bar.txt. Good luck with git log!
$ git log
commit 90bd7cc6c3c530798872827ba02cb7db4fd422c2 (HEAD -> main)
Author: User <user@example.com>
Date: Fri Oct 4 16:24:56 2024 -0700
added foo.txt
That’s it? Where’s all the bar.txt stuff? Well, it was on the topic1 commits, which were descendants from
this commit 90bd7. Because git log only shows ancestors, we’re not seeing any of the bar.txt changes.
So, finally, we arrive at the entire topic of this chapter: the reflog. Let’s take a peek.
$ git reflog
90bd7cc (HEAD -> main) HEAD@{0}: checkout: moving from topic1 to
main
Chapter 20. The Reference Log, “reflog” 112
Hey, that’s more like it! I see the changes I made to bar.txt in there! And I see the commit hash on the
left! This means I can switch to that commit!
appended to bar.txt
commit 4219f83f22f8a90cb8d57128501facb58b292003
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Oct 4 16:24:56 2024 -0700
added bar.txt
added foo.txt
$ cat bar.txt
Line 1
Line 2
Yup!
Let’s switch back to main and see what happens.
$ git switch -
Warning: you are leaving 2 commits behind, not connected to
any of your branches:
If you want to keep them by creating a new branch, this may be a good
time to do so with:
This is Git telling us, “Hey, I’m going to garbage collect these two commits after the 90 days are up. If you
want to keep them, attach a branch to them.”
And it’s helpfully telling us how to do that.
So even though we force deleted topic1 earlier, we could now simply recreate it if we didn’t mean to do
that. Let’s do that.
As you can see, the reflog can get you out of all kinds of trouble when you thought you’d lost commits for
good.
$ git reflog
598c84e (HEAD -> main) HEAD@{0}: checkout: moving from topic1 to
main
dc3d6a3 HEAD@{1}: commit: appended to bar.txt
0789880 HEAD@{2}: commit: added bar.txt
598c84e (HEAD -> main) HEAD@{3}: checkout: moving from main to
topic1
598c84e (HEAD -> main) HEAD@{4}: commit (initial): added foo.txt
See that HEAD@{3}-type stuff in there? You can use those to check out specific commits (instead of using
the commit hash, for example).
Now, HEAD@{3} doesn’t mean “3 commits before HEAD”. But it is an identifier you can use to switch to a
particular commit.
A lot of Git commands obey the -p switch that puts them in patch mode. This is a powerful mode that allows
you to select some of the changes for a particular command, but not all of the changes.
Commands that use -p include add, reset, stash, restore, commit, and more.
Basically any time you have changes to a file and you’re thinking, “I want to do something with just some
of these changes”, patch mode will help you out.
Some terminology: Git calls a collection of close changes a hunk. An example might be if you modified
function foo() by adding a few lines and modified function bar() by adding a few lines, you would likely
have two hunks, one for each group of changes.
Patch mode allows you to select which hunks will be operated on.
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
And we make a couple changes, adding a line to the top and bottom:
Line BEGIN
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
114
Chapter 21. Patch Mode: Applying Partial Changes 115
Line END
And I’m about to add and commit, but I realize that I only want to add Line BEGIN at this time, and not
Line END.
If I did a regular git add, it would add both changes to the stage. But if I do git add -p, we can select one
or the other. Let’s try it.
First let’s have a look at our diff.
$ git diff
diff --git a/foo.txt b/foo.txt
index a982fdc..125f6ac 100644
--- a/foo.txt
+++ b/foo.txt
@@ -1,3 +1,4 @@
+Line BEGIN
Line 1
Line 2
Line 3
@@ -6,3 +7,4 @@ Line 5
Line 6
Line 7
Line 8
+Line END
Poring over that, you see we’ve added Line BEGIN to the top and Line END to the bottom. (Recall that lines
with + in front of them are additions in diff.)
Now let’s do a patch add.
$ git add -p
diff --git a/foo.txt b/foo.txt
index a982fdc..125f6ac 100644
--- a/foo.txt
+++ b/foo.txt
@@ -1,3 +1,4 @@
+Line BEGIN
Line 1
Line 2
Line 3
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,p,?]?
Well, that’s a lot of options! The easy ones are y for “yes” and n for “no”. And also you can type ? to get
more detailed help.
Also we see that this is hunk 1 of 2, which makes sense because we have one change at the top of the file and
another at the bottom.
In our case, we do want to keep this first hunk, so we’ll answer y.
And then we get to hunk 2 of 2:
Line 6
Line 7
Line 8
+Line END
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]?
And for this one, I’m going to say n to not stage it. Then we’re back out to the shell prompt.
Now I’m going to type git status to see where we are, but first I want you to think about what it’s going
to tell us.
We have one of the changes staged, and the other change not staged. What state are files in when they have
unstaged changes? And when there are staged changes? We have both right now, right?
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: foo.txt
Sure enough! Because we only did a partial add of the changes in the file, the added changes are on the stage,
and the not-added changes are still out in the working directory. It has to be this way because we haven’t
staged all our changes!
At this point we can go ahead and commit the partially-added changes that are on the stage.
Here’s my log:
updated
commit aae754f46130b6d86680e74caa98642becc88d6e
Author: User Name <user@example.com>
Date: Fri Oct 11 16:12:04 2024 -0700
added
I want to do a partial reset to the earlier commit aae75. And I’m going to say “no” I don’t want to reset the
first hunk, and “yes” I want to reset the second. Here’s what it looks like:
The first question is asking, “Do you want to remove ‘Line BEGIN’?” And I said “no”. And the second
question is asking “Do you want to remove ‘Line END’?” And I said “yes”.
Where are we?
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: foo.txt
Hmm. Let’s check the difference between the stage and HEAD.
+++ b/foo.txt
@@ -7,4 +7,3 @@ Line 5
Line 6
Line 7
Line 8
-Line END
That’s telling us that, compared to HEAD, the stage has the Line END removed. Which is great, because that’s
what we asked for with reset -p. So we’re on track.
But why is foo.txt modified? Let’s see:
$ git diff
diff --git a/foo.txt b/foo.txt
index e0e1d89..125f6ac 100644
--- a/foo.txt
+++ b/foo.txt
@@ -7,3 +7,4 @@ Line 5
Line 6
Line 7
Line 8
+Line END
This is telling us that, compared to the stage, the working tree has Line END added to the end.
And sure enough, if we look at the foo.txt file in the working tree, it still has Line END in it.
$ cat foo.txt
Line BEGIN
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line END
What does it all mean? Well, it means reset -p messed with the stage, but not with the working tree. Our
working tree is still the same as it was with the last commit. (git diff HEAD will show no changes.)
Now, admittedly, it’s likely this isn’t what you want. Maybe you wanted to reset the hunk and get your
working tree reset to that hunk, as well.
But we can still get there! Remember that the reset hunk is on the stage ready to be committed! Let’s do
that!
There. Now the stage and HEAD are the same, both having had Line END removed. But Line END still exists
in our working tree, like status informs us:
Chapter 21. Patch Mode: Applying Partial Changes 119
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: foo.txt
So how do we get the reset change back into our working tree? The answer is right there in the hints.
There. Now we’re all on the same page with the Line END removed entirely.
There’s another way to synchronize the stage and working tree during a patch reset. After you
do the reset -p, you can copy the file foo.txt from the stage to the working tree with:
That will make the stage and working tree the same, so everything will all be on the same page when
the commit is complete.
Let’s say you’re working on branch1 and you have made a bug fix to branch2. You’re not ready to merge
all the changes in from branch2 into branch1, but you really want just that bug fixed.
Luckily, there’s a way to do that! You can merge a single commit into your branch with git cherry-pick.
You just have to tell it which commit to bring in.
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
120
Chapter 22. Cherry-Pick: Bringing in Specific Commits 121
Line 8
Line 9
Line 10
Branch: Line 101
Branch: Line 102
Line 1
Line 2
Line 3
Line 4
BRANCH: INSERTED LINE 5
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Branch: Line 101
Branch: Line 102
Additionally let’s create a branch here called checkpoint to make this demo a little easier. You don’t have
to do this, but it’ll enable us to cherry-pick this commit by its branch name instead of by its commit hash. Or
you could skip this step and just use the hash.
This doesn’t switch branches. It just makes a new branch on this commit. HEAD is still pointing to
branch like before.
Lastly, let’s add a couple more lines to the end, and commit one last time.
So here’s the file as it exists on branch:
Line 1
Line 2
Line 3
Line 4
BRANCH: INSERTED LINE 5
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Branch: Line 101
Branch: Line 102
Branch: Line 103
Branch: Line 104
commit 9533e0bdd5cba7d65401c3180b34b01700a7906e
Author: Branch User Name <branch-user@example.com>
Date: Sun Oct 20 13:08:30 2024 -0700
added
Okay—that’s the set-up part of the demo. Now it’s time to cherry-pick!
What we’re going to want to do for the demo is switch back to main and then cherry-pick the one commit
that inserts line 5 in the middle. You can always use its commit hash (407f2) for this, but we left behind that
branch checkpoint there we can use instead.
Let’s do it.
What that should have done is bring in that newly-inserted line 5, and none of the other changes. Let’s look
at foo.txt from main:
Line 1
Line 2
Line 3
Line 4
BRANCH: INSERTED LINE 5
Line 5
Line 6
Line 7
Line 8
Chapter 22. Cherry-Pick: Bringing in Specific Commits 123
Line 9
Line 10
Wait—wasn’t that just a merge? Not quite! Notice that we had added lines 101-102 in branch
before we inserted line 5. And yet that earlier commit is not reflected in main. We cherry-picked that
single commit with line 5 out of the stream of commits, ignoring the other ones before and after it!
Now let’s look at git log on main:
commit d6953bd746c813f5ba545cf0fd6044fd78e2c617
Author: User Name <user@example.com>
Date: Sun Oct 20 13:08:30 2024 -0700
added
1
Even if the changes were identical, the commit hash would still be different because the hash takes all kinds of other metadata into
account.
Chapter 23
Let’s say you find something in the shared codebase that’s just wrong. Or, charitably, we’ll say you found
something interesting.
And you want to know who was to blame for that incredible code.
This is where a simple Git command can enlighten you.
Here’s some example truncated output (so that it fits in the book margins):
I have the --date=short switch in there to compress it even more so it fits in the book. Otherwise it would
show a full time stamp.
What we see in this fabricated example is that Alice has checked in the majority of this function, but the next
day Chris came in and modified or added those additional lines in the middle.
And now we know.
124
Chapter 23. Who’s to Blame for this Code? 125
Finally, your IDE (like VS Code) might support blame, either natively or via an extension. Some people just
have this feature turned on all the time.
Chapter 24
Configuration
Waaaaay back at the beginning of this book, we did some Git configuration. We did this:
When we did, it added that configuration information to a file and the info in that file applies to all the Git
repos on your system.
Unless you override them with a local config, that is. But stay tuned for more on that later.
Let’s look at one of those lines again:
What those two variables, user.name and user.email, are doing is they’re setting the values that
will go in your commit messages! That’s your identity when you commit! A side note here it that it’s
incredibly easy to impersonate anyone else in the world just by putting their name and email there. To
mitigate this, one option is to digitally sign your commitsa , something you can read a little bit more
about in the Changing Identity chapter.
a
https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work
If the commands in this chapter are giving you errors, see the section on older Git versions, below.
Unless you explicitly say --global, Git assumes you mean the local configuration.
126
Chapter 24. Configuration 127
What is the local configuration? It’s the configuration that applies to the repo that you’re currently in, and
no others.
Configuration options in the local config override the global config!
Here’s a practical example of why you might do this. Let’s say you have a personal email for your fun
projects, and a contractor email that you use for work-for-hire. But since you’re an independent contractor,
you have all these projects on one computer.
However, you want to use your work identity (name and email) for your contract work and your hacker
identify for your fun work.
One thing you might do is set the following globally:
and that would be the default for all your repos. And then you might have a new repo for a job:
And we pop in there, and we set the local config just for that repo (it’s local because we’re not specifying
--global):
$ cd corporate_job_12
$ git config set user.name "Professional Name"
$ git config set user.email "professional@example.com"
And now, just in the corporate_job_12 directory, we’ll be using our professional name and email in our
commits. Everywhere else we’ll be using our elite hacker name.
You can override all the global configs on a per-repo basis by specifying local configs.
Finally, the local config for a repo is found in the .git/config file out of the repo’s root directory.
You can see in there that user.name and user.email appear twice. The first is from the global config,
which is overridden later by the value in the local config.
Chapter 24. Configuration 128
Note that it is only giving the active value (the local one in this case) even though we saw with
git config list that both the global and local values were there.
The double quotes are there so that the shell delivers the name as a single argument. Normally it splits all
arguments on spaces. You could also use single quotes which is useful if the value has special shell characters
in it. The grotesquely oversimplified rule, with apologies to shell enthusiasts, is to use quotes around the value
if it has a space in it.
Set will overwrite any previously-existing value of the name variable name.
And last but not least, we can delete a variable with unset:
Variable Description
user.name Your name
user.email Your email
pull.rebase Set to true to have a pull try to rebase. Set to false to have it try to merge.
core.editor Your default editor for commit messages, etc. Set to vim, nano, code, emacs, or
whatever.
merge.tool Your default merge tool, e.g. meld or whatever.
diff.tool Your default diff tool, e.g. vimdiff
difftool.prompt Set to false to stop Git from always asking you if you want to launch your
difftool.
color.ui Set to true for more colorful Git output
core.autocrlf Set to true if you’re on Windows and not in WSL and the remote repo has
Unix-style newlines and you want to use Windows-style newlines in your
working directory. On other systems, set to input. This is all about working
around Window’s ancient newlines.
commit.gpgsign Set to true if you’ve configured GPG commit signing and want to always sign.
1
https://git-scm.com/docs/git-config#_variables
Chapter 24. Configuration 129
Variable Description
help.autocorrect Set to 0 to show the command Git thinks you meant to type if you misspelled it.
Set to immediate to have it run the corrected command right now. Set to prompt
to ask you if you want to run it.
Again, there are a lot more of these. Peruse the docs for more.
1 [core]
2 repositoryformatversion = 0
3 filemode = true
4 bare = false
5 logallrefupdates = true
6
7 [user]
8 name = Your Name
9 email = user@example.com
If you look, you can see where user.name and user.email ended up. That’s how the config file is orga-
nized.
So you can edit it here and save those changes. Some people might find this easier than adding or modifying
variables on the command line.
If you corrupt your config with sloppy editing, you’re in for an interesting time. You won’t be
able to run git config edit again. You’ll have to manually fix the config file in your favorite text
editor.
The local config file can be found relative to the root directory for the repo in question in
.git/config.
The global config file can be found at ~/.gitconfig on Unix-like systems and
C:\Users\YourName\.gitconfig on Windows.
Bring the appropriate file up in your editor, fix the mistake, save it, and then git config edit should
work again.
You can also do conditional includes. That is, you can choose to include a file based on some condition being
true.
Testable conditions are:
• Which directory this repo is in
• If you’re on a particular branch
• If there is a particular remote configured
This gives you all kinds of power. Personally, all of it is more than I need, and I’ve never used this feature,
but that’s just me.
Get more info and examples in the official book2 .
2
https://git-scm.com/docs/git-config#_conditional_includes
3
https://git-scm.com/docs/git-config#_deprecated_modes
Chapter 25
Git Aliases
Some of these Git commands might be painstaking to type. So far, we haven’t had to do anything too
complicated, but we might eventually.
For example, let’s say you want to see the names of the files that were modified with git log. It’s no
problem; you can tell it to do that.
$ git logn
and it will be an alias for git log --name-only, effectively running that command.
I speculate that Git has a number of built-in commands (like log and push) and if you try to have it run
something that is not a built-in, it tries to find it as an alias variable. And if it does, it substitutes that
instead. 99% sure that’s what’s happening under the hood.
Since aliases are just regular configuration variables, getting, setting, and deleting them happens as described
in the config chapter.
131
Chapter 25. Git Aliases 132
If you want to see all of them, you can run this command:
More compact log showing the commit graph with git logc.
For that last one, we’re making heavy use of --pretty formatting which gives tons of control over the
output. See the “Pretty Formats” section of the git log manual page for more info1 .
1
https://git-scm.com/docs/git-log#_pretty_formats
Chapter 25. Git Aliases 133
$ git logx
fatal: unrecognized argument: --foobar
You can ask Git to give you more information by adding GIT_TRACE=1 to the beginning of the command
line.
This sets the environment variable GIT_TRACE to 1, but it only does it for this one command. It’s
not persistent. Git knows to look for GIT_TRACE and that it should alter its behavior if it finds it.
Unfortunately I had to truncate the lines on the right so they fit in the print version of the book, and that’s
what we really want to look at. We’ll get there in a moment.
For now, let’s look on the left. What we see there is a timestamp and some information about which part of
the Git code is sending the trace out. And the it ends with our error.
Let’s scroll to the right and just look at the lines following the trace:.
It might take some sifting through, but let’s look just at the lines with run_command and alias expansion
in them:
And there we can see exactly what’s being expanded into what. And that might be useful for debugging it.
It’s probably a bit of overkill for this simple example, but there are some aliases of extraordinary complexity
for which this technique might help.
Chapter 26
Changing Identity
There are a few ways you’re identified when you do work with Git.
They are held in:
• the user.name and user.email configuration variables
• the SSH key you use to authenticate with a remote server like GitHub
• the GPG key you use for signing commits (rare)
It’s all well-and-good if you only ever use a single identity, but sometimes you might want to use different
ones. For example, maybe for your personal fun work, you use one identity and SSH key, but then you got
a contract job and you want to use your professional email and have to connect to a different server with a
different SSH key.
Let’s check out what the defaults are for all these, as well as how to change them on a per repo basis.
And then when you make commits in this repo, that’s the identity that will be attached to them. Commits in
other repos will still obey your global username and email (unless you’ve overridden them, as well).
134
Chapter 26. Changing Identity 135
Admittedly, that’s a pain in the butt to type, so some people set up their SSH config to use a particular key
with a particular host name. But we’re not going down that route.
Instead, let’s tell Git to use a particular identity by setting the core.sshCommand variable locally for this
repo. This variable just holds the SSH command that Git uses to connect, which would normally be ssh.
Let’s override:
(The command above is split into two lines to fit in the margins—it is normally a single line and the \ is
Bash’s line continuation.)
And—wait a second—what’s that -F none on there? That’s just a safety that’s telling SSH to ignore its
default configuration file. Remember how above I said people sometimes set an identity by domain in their
SSH config? This would override that since overriding is what we’re trying to do here.
The reason I like this approach is that you can easily do it on a per-repo basis, and the config is stored with
the repo (instead of in an environment variable or in SSH’s somewhat-unrelated configuration).
1
https://superuser.com/questions/232373/how-to-tell-git-which-private-key-to-use
2
https://www.gnupg.org/
Chapter 26. Changing Identity 136
Then when you sign the commits, that key will be used.
$ ssh-keygen -t ed25519
And then you have to do some one-time config if you haven’t done so already. Use --global if you want
to set this up across all your repos. This tells Git to use SSH keys and always sign commits:
Then we can set the key we use for signing. Change the path, below, to point to the public key:
Lastly, we have to add your information to the allowed_signers file. This file can go anywhere; in this
example, we’ll put it in ~/.ssh/, but you could do one-offs per repo if you wanted.
Firstly in this lastly step is to tell Git where your allowed_signers file is.
The contents of that file should have at least two fields. First the email address found in your user.email con-
fig variable that you’ll use for the commits. Second, a copy of the public key from your user.signingkey
variable. Note that you want the contents of that file, not the file name.
An example line in the allowed_signers file looks like this (line truncated for formatting):
Chapter 26. Changing Identity 137
You can have multiple lines in that file for multiple identities.
Chapter 27
Amending Commits
Git gives you the power to relatively easily amend the last commit.
Caution! This section talks about changing history, and let’s not forget The One Rule Of Changing
History: thou shalt not change history of anything that you’ve pushed, lest someone else might have
already pulled thy earlier changes, causing your commit histories to become woefully out of sync and
much shouting.
In short, if you pushed a change, assume someone else has pulled it already and amending your commit
(changing history) would cause lots of pain.
In shorter, if you pushed, it’s too late. No more amending the commit.
So what are some use cases?
• Maybe you botched the commit message and you want to rewrite it.
• Maybe you forgot to add some files.
That kind of thing that none of us have ever done ever, right?
$ git status
On branch main
nothing to commit, working tree clean
$ git log
commit d7fba6838a689c3de15a27e272e8e4123d7c2460 (HEAD -> main)
Author: User <user@example.com>
Date: Thu Nov 21 20:39:04 2024 -0800
addded
That’s one “d” too many in the commit message. Fixing it up is as easy as this:
138
Chapter 27. Amending Commits 139
And that brings me right into my editor where I can change the message.
If I don’t want to use the editor, I can do it on the command line:
Note that doing this preserves the author of the commit. (Which is probably what you want 99.9999% of
the time since you were probably already the author.) If you want to change the identity, you’ll have to
reconfigure your identity with git config and then run:
$ ls
bar.c baz.h foo.c
$ git log --name-only
commit b307686933dca3db718e6a3e3f8226be11e7e278 (HEAD -> main)
Author: User <user@example.com>
Date: Thu Nov 21 20:47:08 2024 -0800
added
bar.c
foo.c
That’ll run the amend and not edit the commit message at all.
And there we have it—you can easily amend the last commit. Just be sure you haven’t pushed it before you
do.
Chapter 28
Difftool
Admittedly, this diff output is hard to read. I swear, though, you do get used to it. I use it all the time.
That said, it can be nicer to see something more visual, you know, like the old version on the left and the new
version on the right in a way that’s visually easier to comprehend.
If you’re using VS Code or some other IDEs, you get some nice diffing for free and don’t nec-
essarily need to pay attention to this section. See more in the VS Code chapter.
First, the bad news is that Git doesn’t support this out of the box.
The good news is that there are a lot of third-party tools that do, and you can easily hook them up so that they
work with Git super easily.
How easy?
Once you set it up, you’ll be able to just write difftool instead of diff on the command line. For example:
And what does that get you? For me, where I use Vim and have Vimdiff set up as my difftool, it gives me a
screen like in Figure 28.1.
140
Chapter 28. Difftool 141
This might be a little tough to see in black and white, but what we have is the old version on the left and the
new version on the right. The lines of minus signs on the left indicate lines that don’t exist in the old version,
and we can see highlighted lines on the right that exist in the new version.
But if you just try to run git difftool out of the box, it won’t work. You have to configure it first.
28.1 Configuring
Firstly, Git normally prompts you before launching a third-party difftool. This is annoying, so let’s turn it
off globally:
And that might be enough. If vimdiff (or whichever diff tool you’re using) is in your PATH1 , you should be
in business and you’re good to go.
If it’s not in your PATH, maybe because you installed it locally in your home directory tree somewhere, you
can either add it to your PATH (search the Net for how to do this), or you can specify the full path to your
particular difftool. Here’s an example with vimdiff, which is redundant for me because /usr/bin is already
in my PATH.
If you’re using a different difftool other than vimdiff, replace that part of the config line with the name of
the command.
Again, you only have to set the path if the tool isn’t installed in a standard place.
9
https://www.vim.org/
10
https://winmerge.org/?lang=en
Chapter 29
Mergetool
Do you hate all those >>>>>, =====, and <<<<< things that Git puts in your files during a merge conflict?
If so, using a merge tool might be what you’re after. A merge tool will give you a graphical display showing
your changes, the conflicting changes, and the desired result of the merge. And it shows it in an easy-to-digest
form.
Personally, I dislike merge tools. That seems nuts, but let me explain a moment. When you’re in a
merge conflict, the only thing you have to do is edit those files with the ===== delimiters and make
them Right, remember? You have to modify the file until it is correct, ripping out those delimiters as
you go.
It’s just you and the file, that’s it. No intermediaries messing with the contents. And when you’re
done, what you have is your final answer.
But merge tools are intermediaries by their very nature. And we must trust that we’re using them
correctly to get the job done. And, for me, even after I’ve used them probably correctly, I still feel
like I have to manually inspect the result to make sure it’s Right.
The benefit I do see is that with a merge tool you typically get a side-by-side view of the changes,
as opposed to the up-and-down view you effectively get when editing the conflicting file. This can
make the merge tool easier to use when you have a number of big conflicting hunks in a file.
But in real life, I never use one. Also in real life, lots of people use them.
143
Chapter 29. Mergetool 144
In terms of usage, here’s what we’re going to do, assuming that the merge tool starts you at the first conflict
when it is launched:
1. Choose either “yours” or “theirs” to keep the Right changes.
2. Go to the next conflict.
3. Repeat from Step 1 until all conflicts are resolved.
After you’ve gone through all the conflicts and chosen one or the other, make sure the final result is Right
and then save/finish the result.
The merge tool will have staged the result for you, ready to commit and finish the merge.
(Long commands split to fit in the book margins—it could be on a single line.)
1
https://www.araxis.com/merge/index.en
2
https://www.scootersoftware.com/
3
https://www.devart.com/codecompare/
4
https://invent.kde.org/sdk/kdiff3
5
https://meldmerge.org/
6
https://www.perforce.com/products/helix-core-apps/merge-diff-tool-p4merge
7
https://www.vim.org/
8
https://winmerge.org/
Chapter 29. Mergetool 145
Note that last line is to explicitly set a two-panel view for vimdiff and difftool. If it’s not set, the
mergetool.vimdiff.cmd directive will make difftool have a three-panel display—probably not what
you wanted.
Once that’s in place, let’s say we have a merge conflict.
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: foo.txt
no changes added to commit (use "git add" and/or "git commit -a")
But since we’ve set up our merge tool, let’s use it:
$ git mergetool
If Git is prompting you to ask if you really want to run the merge tool (which you presumably do
since you just ran git mergetool), you can turn off that “feature” with this config command:
This is going to bring up a Vim window with three panels. The left is your local changes, the middle is the
file as it exists in the repo, and the right is the result of the merge.
The goal is to make the one on the right look Right. Now, you could just do that outright by modifying the
file there, but at that point, why even use a merge tool?
So we’ll follow the steps we outlined earlier.
When we first run git mergetool, we get dropped at the first conflict with the cursor in the left window.
The left window holds the changes we made.
In the middle window, we’ll see the corresponding changes that are in the repo.
And in the right repo, we see what will be staged when we’re done. Right now in the right window, we see
all the ===== and <<<<< stuff. But we’ll change that in a moment.
Move the cursor to the right window. This is where the action will be.
Make sure the cursor is on a highlighted section (which probably will be in multiple colors). This highlighted
section is what we’ll replace.
Let’s choose which change to use.
If you want to keep your changes (and ditch the ones in the repo), use this Vim command:
Chapter 29. Mergetool 146
:diffget LOCAL
If you want to discard your changes (and keep the ones in the repo), use this command:
:diffget REMOTE
When you run one of those, you’ll see the content in the right window change to what you wanted.
And then you can go to the next conflict with ]c. (or to the previous with [c.)
Do this until the right window is Right. Note that you are also free to edit the right window directly all you
want.
When you’re done, save the right window and quit all the windows.
Importantly the second you exit the merge tool, Git will stage whatever you saved in the rightmost window.
If you exited too soon and got stuff staged before you were done, use git checkout --merge with the file
in question to get it off the stage and back to “both modified” state.
If there are multiple conflicting files, Git will bring up the merge tool again to handle the next file in line.
And when you’re done, the changes are made and you can finish the merge with a commit as per usual.
But wait—what’s that .orig file that wasn’t there before? Read on!
foo.txt.orig
bar.txt.orig
You can add these to your .gitignore if you want to, or you can prevent the creation of them in the first
place with this configuration variable:
Submodules
You can’t really have a Git repo inside a Git repo. I mean, yes, you can make one, but when you try to add
it to the outer repo, Git will have a lot to say about it.
So it’s probably not what you want, but Git offers a hint: maybe you wanted something to do with submodules,
instead!
Submodules give you a way to make a completely separate repo appear inside the working tree of your
current repo. Not only that, it allows the current working tree to have a specific commit of the submodule
represented in that submodule’s tree.
The canonical use case for this is when your project depends on a library that you also have the source for.
You can include the library’s repo as a submodule of your repo, and effectively pin it to a particular version
(specifically to a particular commit).
For example, maybe your code works with FooLib version 3.4.90. So you include FooLib as a submodule
and make sure it’s pinned to that version. Then even though another team might be updating FooLib, you’ll
always have version 3.4.90 available to build against.
Then later when you’re ready, you can update the submodule to the latest version, say 4.0.1, and pin that one
in place.
It’s important to note that a submodule is just another regular Git repo. Nothing special about it. The only
thing that’s notable is that we’ve decided to effectively clone it inside another repo, and logically tie it to that
147
Chapter 30. Submodules 148
repo.
$ git clone \
git@github.com:beejjorgensen/git-example-submodule-repo.git
$ cd git-example-submodule-repo
$ git submodule update --recursive --init
That will also get the submodules cloned. (--recursive is in case the submodules have submodules (!!)
and the --init does some necessary bookkeeping work in your local repo. How’s that for a handwavy
statement?)
And for now, that might be enough for you to get to work! All you really needed to build the existing project
was the repo and its submodules, and you might not be in charge of the submodules and just need them to
exist for the build. So now you can get to work.
But in case you need to do more, read on!
Again, a use case for this might be if your main repo project depends on another one for the build, e.g. like a
library. And you don’t want to use the binary form of the library (or maybe it doesn’t exist), so you need to
build it.
If you didn’t use submodules, anyone who wanted to build your repo would need to also clone the library
repo and juggle all that. Wouldn’t it be nicer if they could just add that --recurse-submodules flag to
their clone command and have it all set up an ready to build?
So let’s go through the steps of adding a submodule to an existing repo and see how that all works.
Feel free to use my sample repo as your submodule, use one of your own, or anyone else’s. No one knows
when you make a submodule out of their repo.
First, let’s create a new repo for testing and put a commit in there for fun:
There you go! Well, almost, anyway. Let’s check our status:
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .gitmodules
new file: git-example-repo
What are those things on the stage? Well, git-example-repo is the submodule. It’s a little strange because
Git is calling it a “file” when it’s a directory, but that’s just part of the special treatment submodules get.
And there’s another file in there called .gitmodules that holds information about all the submodules you’ve
added.
Both of these files (treating git-example-repo like a file) should be committed to your repo so that other
people who clone it get the submodule information.
Now you’re set! Anyone who clones the repo gets that submodule information.
You can even do it with your test repo. Change directory to the parent of the test repo and clone it:
After that you can cd into test_repo2 and see the submodule there.
Can I Make a Local Repo a Submodule? No! Git prohibits that because there’s some security risk
there that, to be honest, I haven’t really read about. There’s supposed to be a way to override that with
a config setting, which I thought would be quite useful for messing around to see how submodules
worked, but apparently that config setting doesn’t work as of late 2024. So you’ll have to use network-
remote repos for submodules.
We’re cloning a non-bare repo, which is weird. It’s OK to clone it—the Git Police aren’t going to
show up. You just won’t be able to push to it. And that’s perfectly good enough for this demo. But
it’s something that you wouldn’t normally do.
Chapter 30. Submodules 151
Also notice the detached HEAD in the cloned submodule repo! If you look in
test_repo2/git-example-repo and do a git log, you’ll see this on the first line:
$ cd test_repo/git-example-repo
$ git log
commit cd1bf77d2ef08115b18d7f15a9c172dace1b2222
(HEAD -> main, origin/main, origin/HEAD)
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Dec 6 15:07:43 2024 -0800
commit d8481e125e6ef49e2fa8041b16b9dd3b8136b550
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Dec 6 15:07:13 2024 -0800
improve functionality
commit 433252748b7f9bf85e556a6a0196cdf38198fc43
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Jan 26 13:30:08 2024 -0800
Added
So far so good. Now let’s cd back to the containing repo and have a look at where we stand.
$ cd ..
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working
directory)
modified: git-example-repo (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
Look at that! The submodule directory is listed as modified. It says “new commits”, but that’s just telling us
Chapter 30. Submodules 152
that “things have changed in the submodule from the commit that I was pinned onto before”.
So let’s add that and commit it.
And now let’s pull those changes in our clone, test_repo2—note the --recurse-submodule option on
the pull!
$ cd ../test_repo2
$ git pull --recurse-submodules
Fetching submodule git-example-repo
Already up to date.
Submodule path 'git-example-repo': checked out'
'd8481e125e6ef49e2fa8041b16b9dd3b8136b550'
And now on test_repo2 if we jump into the git-example-repo submodule, we can check the log:
$ git log
commit d8481e125e6ef49e2fa8041b16b9dd3b8136b550 (HEAD)
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Dec 6 15:07:13 2024 -0800
improve functionality
commit 433252748b7f9bf85e556a6a0196cdf38198fc43
Author: Brian "Beej Jorgensen" Hall <beej@beej.us>
Date: Fri Jan 26 13:30:08 2024 -0800
Added
And we see HEAD is on commit d8481e, just like we set it to in test_repo. (And we also do not see main.
It’s the child commit from where HEAD is now, so it’s not appearing in the log. We could still switch to it if
we wanted, of course.)
What have we done? We’ve changed the commit the submodule is pinned at in one repo, and then we’ve
pulled that change into another repo!
This will fetch the submodule data and set you up to point at the correct commit.
This will show you all the commits between HEAD and origin/main, inclusive so you can see what’s been
done. (Assuming they’re related, that is. If they’re on divergent branches you’ll have to get more creative.)
Then you choose the commit you want to pin HEAD to, switch to that, and run an add/commit from the
containing repo, as outlined in Setting the Commit for the Submodule, above.
Then make your changes and push them (from the submodule directory).
At this point, the containing repo is still pinned to the old commit. So you’ll want to run an add/commit
from the containing repo, as outlined in Setting the Commit for the Submodule, above.
That + means that, although HEAD in the submodule is at commit 1c10d, the containing repo has the submod-
ule pinned to a different commit! You might see this happen after you pull a submodule (thus moving HEAD),
but haven’t updated the containing repo to match.
If you see the +, git status will also tell you more:
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working
directory)
modified: mysubmod (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
Sure enough—we have changed HEAD in the submodule so its directory shows as modified.
You can get rid of the plus by either pinning the repo to a new commit as outlined in Setting the Commit for
the Submodule, above, or by moving the submodule HEAD back to where the containing module expects it.
There can also be a - in front of the commit hash. This means the submodule hasn’t been initialized
or downloaded. Try a git submodule update --recursive --init.
How do we figure out where the containing module expects the submodule HEAD to be? With this handy
command:
That --recurse-submodules did a lot of work for us, cloning the submodule and setting everything up so
it was ready to use.
We also noted that if we forgot that switch, we could still pull it off:
Chapter 30. Submodules 155
$ git clone \
git@github.com:beejjorgensen/git-example-submodule-repo.git
$ cd git-example-submodule-repo
$ git submodule update --recursive --init
$ git clone \
git@github.com:beejjorgensen/git-example-submodule-repo.git
$ cd git-example-submodule-repo
$ git submodule init
$ git submodule update --recursive
So that --recurse-submodules switch to git clone was actually running a bunch of commands for us
behind the scenes.
A little breakdown:
When you first clone the containing repo, there’s a .gitmodules file in there indicating the directory name
and the URL of the submodule remote. But that’s not enough info. You have to do a git submodule init
to cause Git to parse that file and set up some internal bookkeeping.
After that, you can run git submodule update to bring in the submodule data to use.
1. De-initialize the submodule. If the submodule HEAD is not where the containing module expects, you
can add -f to force this.
$ rm -rf .git/modules/mysubmod
5. Delete the submodule tree from Git. This will also add the deletion to the stage.
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .gitmodules
deleted: mysubmod
Looks good.
7. Commit and push (if appropriate).
Tags
Tags are a way to annotate a particular commit. You can kind of think of them like branches that don’t move.
A very common case is to take a particular commit with a version number like 1.2.3.
I can’t find any rules about what characters you can use in tag names, but it seems safe to use ASCII upper
and lower case, numbers, and punctuation like ., -, and _, etc.
There are two types of tags:
• Lightweight: Just a tag, e.g. v3.14.
• Annotated: Also a tag, e.g. v3.14, but includes a message and author, like a commit.
You can generally use tags the same way you’d use branches (you can diff them, switch to them, etc.) except
they don’t move.
And you can see them in the log with the other branch information.
You can also tag a commit, or anything that refers to a commit (like a branch or even another tag):
157
Chapter 31. Tags 158
If you want to just push one tag, you must specify the remote:
After a tag is pushed, other collaborators will automatically get the tags when they pull.
And that’s easy enough. Except if you’ve already pushed on the server. If you have, then the next time you
pull you’ll get the tag again.
So you’ll have to delete the one on the remote, as well, which also must be named explicitly:
That’ll delete the tag on the server, but it won’t delete it from other people’s clones. In fact, there’s no easy
way to do this.
The basic idea is that tags, once created, shouldn’t be deleted. Now, if you haven’t yet pushed, no problem.
Add, delete, and change all you want. But once you’ve pushed (and someone has pulled), if you need to
change a tag, just make a new tag.
Chapter 31. Tags 159
In programming circles in general, a playground is a place you can go to mess with code and tech and not
worry about messing up your production system.
And there are places you can go online to find these, but with Git, I find it’s just as easy to make your own
local repo.
Here’s a way to make a new local repo called playground out of the current directory. (You should not be
under a Git repo at this time; create the playground outside other existing repos.)
playground isn’t a special name. You can call it foo or anything. I’ll just use it for this example.
What that command did was create a new subdirectory called playground and create a Git repo in it.
Let’s continue at the end: how do you delete the repo? You just remove the directory.
$ cd playground
$ ls -la
total 4
drwxr-xr-x 3 user group 18 Jul 13 14:43 .
drwxr-xr-x 22 user group 4096 Jul 13 14:43 ..
drwxr-xr-x 7 user group 119 Jul 13 14:43 .git
160
Chapter 32. Appendix: Making a Playground 161
There’s a directory there called .git that has all the metadata in it.
Note: If we wanted to change this directory from a Git repo to just a normal directory, we could run
this:
Again, we have all the power! But let’s show some restraint and not do that yet.
What can we do?
What can’t we do? Let’s make a file and see where we stand:
$ ls -l
total 4
-rw-r--r-- 1 user group 13 Jul 13 14:47 hello.txt
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
hello.txt
If you look in there (to be clear, you have no reason to) you’ll just see metadata and directories.
Before we can use it, we’d better clone it. For ease, we’ll do this from the same directory we created it.
Chapter 32. Appendix: Making a Playground 162
$ cd playground
$ git remote -v
origin /user/origin_repo (fetch)
origin /user/origin_repo (push)
We have remotes! Of course we do. We cloned this repo, and Git automatically sets up the origin remote.
And remember that origin is just an alias for some remote that’s identified somehow. We’re used to seeing
remotes that start with https or ssh, but here’s an example of a remote that’s just another subdirectory on
your disk.
Let’s make a file and commit it, and see if we can push!
$ git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 907 bytes | 907.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To /user/origin_repo
* [new branch] main -> main
Let’s cd in there and see what we have. It’s a clone of the repo, so we’d better see the hello.txt we pushed
in there from playground earlier.
$ cd playground2
$ ls
hello.txt
$ cat hello.txt
Hello, world
That’s just a bunch of shell commands. But here’s the fun bit: if you run sh (the shell) with buildrepo.sh
as an argument, it will run all those commands in order!
$ sh buildrepo.sh
Initialized empty Git repository in /user/playground/.git/
[main (root-commit) 2239237] added
2 files changed, 2 insertions(+)
create mode 100644 foobar.txt
create mode 100644 hello.txt
[main 0533186] updated
1 file changed, 1 insertion(+)
To debug a shell script you can run it like this: sh -x buildrepo.sh and it will show you the
commands it is running.
Chapter 32. Appendix: Making a Playground 164
$ cd playground
$ git log
commit 05331869d77973dfbac38a31c40a44f99225e85d
Author: User Name <user@example.com>
Date: Sat Jul 13 15:19:42 2024 -0700
updated
commit 2239237cc44d11e9479dcc610e5d02ad283766ce
Author: User Name <user@example.com>
Date: Sat Jul 13 15:19:41 2024 -0700
added
$ cat foobar.txt
foobar
foobar again
By putting the initialization commands in a shell script, it’s almost like having a “saved game” at that point.
You can just rerun the shell script any time you want the same playground set up.
Chapter 33
If you try to git commit and don’t specify -m for a message, or if you git pull and there’s a non-fast-
forward merge, or if you git merge and there’s a non-fast-forward merge and you don’t specify -m, or what
I’m sure are a host of other reasons, you might get popped into an editor.
And you might not be familiar with that editor.
So here’s how to get out of it.
• Nano: If the editor says “Nano” or “Pico” in the upper left, then edit the commit message (if you want),
then then hit CTRL-X, and then hit Y to save, then ENTER to accept the given filename.
• Vim: If the screen has a bunch of ~ characters down the left and a crazy-looking file name at the
bottom maybe with the word All, you’re in Vim or some other vi (“vee eye”) variant. Press i, then
type a message (if you want), then hit the ESC key in the upper left, then type two capital Zs in a row.
ZZ. That should save and exit.
I love Vim. But it took a while. If you want to learn more, see the Appendix on Using Vim, that
has the briefest of tutorials. I guess the previous paragraph was really the briefest, so we’ll call it the
second-briefest.
165
Chapter 34
You are in 'detached HEAD' state. You can look around, make
experimental changes and commit them, and you can discard any
commits you make in this state without impacting any branches by
switching back to a branch.
git switch -
This means that you’ve checked out a commit directly instead of checking out a branch. That is, your HEAD
is no longer attached to a branch, i.e. it is “detached”.
To get out of this, you can:
1. Undo the checkout that got you detached:
git switch -
166
Chapter 34. Appendix: Errors and Scary Messages 167
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use
That tells us our local branch names and, in brackets, the corresponding remote-tracking branch. Notice
anything fishy?
It seems main corresponds with origin/main.
And that newbranch also corresponds with origin/main! How?!
Well, when you did git branch -c newbranch, that copies the current branch (main in this example) into
the other branch, including its remote-tracking branch. Bad news, since you really want newbranch to
correlate to origin/newbranch, if anything.
You have a few options.
1. You want to push newbranch up to the origin and track it as origin/newbranch.
Just do this to push and change the remote-tracking branch name:
2. You just want this to be a local branch and don’t need it on the remote.
In this case, just unset the upstream:
This just means there’s no upstream tracking branch for topic1—it’s just a local branch.
If you do want to push this branch, just follow the suggested instruction.
If you are pushing from the wrong branch by accident, switch to the right one first.
Chapter 35
$ vim foo.c
169
Chapter 35. Appendix: Just Enough Vim 170
Try it: launch Vim, type i, then type hello world!, then hit ESC to get back to normal mode.
$ vimtutor
If you don’t feel like messing with it right now, you can just :q! to quit.
There’s also an interactive online tutorial called OpenVim3 that gets you through the first steps.
Finally, ChatGPT4 or other AIs will be helpful in finding new normal mode commands and answering ques-
tions.
“This your last chance. After this there is no turning back. You take the blue pill, the story ends. You
wake up using your normal editor and edit files whatever way you want to. You take the red pill, you
stay in Vim and I show you how deep the rabbit hole goes.”
—With apologies to Morpheus, The Matrix
3
https://openvim.com/
4
https://chatgpt.com/
Chapter 36
$ man git-config
(The minus between “git” and “config” might or might not be required depending on your system,
but I’ve never found a place where it doesn’t work.)
Once you’re in there, the arrow keys, space bar, and page up/down probably work to navigate,
and hitting q or Escape probably quits. It depends on the pager your system uses.
These manual pages are very comprehensive, and don’t tend to be easy to read. But all the
information is there! They can also be found in The Git Reference Manual, below.
• Free Stuff!
• Pro Git1
• The Git Reference Manual2
• The Git Community Book3
• Learn Git Branching4
• GitHub Learning Lab5
• Learn Git and GitHub6
• Learn Git7
• Git Immersion8
• Git Internals9
• Post-Production Editing Using Git10
1
https://git-scm.com/book/en/v2
2
https://git-scm.com/docs
3
https://shafiul.github.io/gitbook/index.html
4
https://learngitbranching.js.org/
5
https://github.com/apps/github-learning-lab
6
https://www.codecademy.com/learn/learn-git
7
https://www.atlassian.com/git/tutorials
8
https://gitimmersion.com/
9
https://github.com/pluralsight/git-internals-pdf?tab=readme-ov-file
10
http://sethrobertson.github.io/GitPostProduction/gpp.html
172
Chapter 36. Appendix: Other References 173
• For Money
• Learn Git in a Month of Lunches11
• Getting Started with GitHub12
• Git Pocket Guide13
• Version Control with Git14
• Git in Practice15
• Git for Teams16
• Pragmatic Version Control using Git17
• Mastering Git18
If you know of more, mail them to beej@beej.us.
11
https://www.manning.com/books/learn-git-in-a-month-of-lunches
12
https://www.amazon.com/Introducing-GitHub-Non-Technical-Peter-Bell/dp/1491949740
13
https://www.amazon.com/Git-Pocket-Guide-Working-Introduction/dp/1449325866
14
https://www.amazon.com/Version-Control-Git-collaborative-development/dp/1449316387
15
https://www.amazon.com/Git-Practice-Techniques-Mike-McQuaid/dp/1617291978
16
https://www.amazon.com/Git-Teams-User-Centered-Efficient-Workflows/dp/1491911182
17
https://pragprog.com/titles/tsgit/pragmatic-version-control-using-git/
18
https://www.amazon.com/Mastering-Git-proficiency-productivity-collaboration/dp/1783553758
Chapter 37
Quick Reference
Quickly look up commands based on what you want to do! Caveat: this list is grotesquely incomplete! See
your man pages for more info!
In this reference section we use the following substitutions:
• URL: Some URL either SSH, HTTP, or even a local file, usually the URL you cloned from.
• FILE: Path to file, e.g. foo/bar.txt, etc.
• DIR: Path to directory, e.g. foo/, etc.
• PATH: Path to directory or file
• BRANCH: Some branch name, e.g. main, etc.
• REMOTE: A remote name, e.g. origin, upstream, etc.
• HASH: Some commit hash—you can get a commit hash from git log or git reflog.
• CMMT: a commit hash, branch, etc. Anything that refers to a commit. Officially this is called a tree-ish,
but that was more letters than I wanted to repeatedly type.
• VARIABLE: a Git config variable name, usually words separated by periods.
• VALUE: an arbitrary value for Git configs.
• TAG: a tag name
Also, don’t type the $—it’s the shell prompt. And everything after a # is a comment. A backslash \ at the
end of a line indicates that it continues on the next line.
37.1 Glossary
• Clone: a duplicate (or to duplicate) a remote repo, commonly for local use.
• Commit: a snapshot of all the files in the repo at a point in time.
• Fork: a GitHub construct to make a clone of someone else’s GitHub repo under your GitHub account.
• HEAD: the commit that is currently checked out/switched to.
• Index: another name for the stage.
• main: the default name of the first branch created.
• master: the historical name for main.
• origin: the default name for the remote from which this repo was cloned.
• Pull request: a way to get changes you made in your fork of a repo back into the repo you forked
from.
• Remote: an alias for a URL to another repo. Usually an HTTP or SSH URL.
• Stage: where you collect files to be bundled into a commit.
• upstream: the conventional name of the remote that you forked from. Not set up automatically.
• Working Tree: the collection of files you can see, which might have changes from the commit at
HEAD.
174
Chapter 37. Quick Reference 175
37.3 Configuration
For all git config commands, specify --global for a universal setting or leave it off to set the value just
for this repo.
SSH identity:
37.3.5 Autocorrect
Autocorrect will automatically run the command it thinks you meant. For example, if you git poush, it will
assume you meant git push.
37.3.7 Aliases
Setting aliases, some examples:
Getting aliases:
Amending commits—don’t amend commits you have pushed unless you know what you’re getting into!
To undelete a deleted file, you could manually recover it from an old commit, or revert the commit that
deleted it.
Chapter 37. Quick Reference 178
37.8 Branches
A local branch looks like branchname. A remote tracking branch looks like remote/branchname.
$ git fetch # Get data from remote but don't merge or rebase
$ git fetch REMOTE # Same, for a specific remote
Chapter 37. Quick Reference 180
37.10 Merging
37.11 Remotes
$ git remote -v # List remotes
$ git remote set-url REMOTE URL # Change remote's URL
$ git remote add REMOTE URL # Add a new remote
$ git remote rename REMOTE1 REMOTE2 # Rename REMOTE1 to REMOTE2
$ git remote remove REMOTE # Delete REMOTE
Exceptions to earlier rules, also useful in .gitignore files in subdirectories to override rules from parent
directories:
37.13 Rebasing
37.14 Stashing
Stashes are stored on a stack.
37.15 Reverting
37.16 Resetting
All resets move HEAD and the current checked out branch to the specified commit.
Obsolete usage:
37.18 Cherry-pick
37.19 Blame
$ git blame FILE # Who is responsible for each line
$ git blame --date=short FILE # Same, shorter date format
37.20 Submodules
$ git clone --recurse-submodules URL # Clone with submodules
$ git submodule update --recursive --init # If you cloned without
Deleting a submodule—do these in order. In this example, DIR is the name of the submodule directory.
37.21 Tags
184
INDEX 185