Bggit A4 C 1-GIT
Bggit A4 C 1-GIT
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.2 What is GitHub? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.3 What is GitHub? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.4 The Most Basic Git Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.5 What is Cloning? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.6 How Do Clones Interact? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7 Actual Git Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7.1 Step 0: One-time Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7.2 Step 1: Clone an Existing Repo . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7.3 Step 2: Make Some Local Changes . . . . . . . . . . . . . . . . . . . . . . . 8
2.7.4 Step 3: Add Changes to the Stage . . . . . . . . . . . . . . . . . . . . . . . . 10
2.7.5 Step 4: Commit those Changes . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.7.6 Step 5: Push Your Changes to the Remote Repo . . . . . . . . . . . . . . . . . 11
i
CONTENTS ii
11 File States 51
11.1 What States Can Files in Git Be In? . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
11.2 Unmodified to Untracked . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
11.3 Files In Multiple States . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
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
additionally 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
4
Chapter 2. Git Basics 5
What about GitLib and Gitea? GitLaba is a competitor to GitHub. Giteab is an open-source
competitor 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 for you to work on. Periodically, in a common workflow, you’ll sync your
copy 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.
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 changes 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.
$ cd git-example-repo
$ ls -la
total 16
drwxr-xr-x 5 user user 160 Jan 26 11:50 .
drwxr-xr-x 14 user user 448 Jan 26 11:50 ..
drwxr-xr-x 12 user user 384 Jan 26 11:50 .git
-rw-r--r-- 1 user user 65 Jan 26 11:50 README.md
-rwxr-xr-x 1 user user 47 Jan 26 11:50 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 things the current status of the local repo is:
$ git status
Gives us:
On branch main
Your branch is up to date with 'origin/main'.
There is one important thing to notice here: there are two main branches. There’s the main branch on
your local repo, and there’s a corresponding main branch on the remote (origin) repo.
Remember how clones are separate? That is, changes you make on one clone aren’t automatically visible
on the other? This is an indication of that. You can make changes your your local main branch, and these
won’t affect the remotes origin/main branch. (At least, not until you push those changes!)
Lastly, it mentions we’re up-to-date with the latest version of origin/main (that we know of), and that
there’s nothing to commit because there are no local changes. We’re not sure what that means yet, but it
all sounds like vaguely good news.
If you’re using VS Code, you can run it in the current directory like so:
$ code .
Otherwise, open the code in your favorite editor, which, admit it, is Vim2 .
Let’s change hello.py:
It was:
1 #!/usr/bin/env python
2
3 print("Hello, world!")
1 #!/usr/bin/env python
2
3 print("Hello, world!")
4 print("Hello, again!")
$ 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
2
https://www.vim.org/
Chapter 2. Git Basics 10
print("Hello, world!")
+print("Hello, again!")
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:
1. The file name. We see this change was to hello.py.
2. The line with the + at the front. This indicates an added line.
A - at the front of a line would indicate the line was deleted. And changed lines are often shown as the
old line being deleted and a new one added.
Finally, if you want to see the diff for things you’ve added to the stage already (in the next step), you can
run git diff --staged.
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:
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.
Chapter 2. Git Basics 11
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.
But look! 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 ex-
plicitly 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
13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-
started-with-git/about-remote-repositories#cloning-with-
https-urls for information on currently recommended modes
of authentication.
fatal: Authentication failed for 'https://github.com/beejjorgensen/
git-example-repo.git/'
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!
13
Chapter 3. GitHub: How To Use It 14
3.3 Authentication
Before we get to cloning, let’s talk authentication. In the previous part of the intro, we say that user-
name/password logins were disabled, so we have to do something different.
There are a couple 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 authentica-
tion 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.
$ 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.
2
https://cli.github.com/
3
https://github.com/cli/cli#installation
Chapter 3. GitHub: How To Use It 15
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 in 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, say.
And the 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.
Chapter 3. GitHub: How To Use It 16
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 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.
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.
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 17
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.
• Option 2: Choose the “GitHub CLI” tab. Run the command as they have it, which will be something
like:
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.
19
Chapter 4. The Git Log and HEAD 20
• The date
• The user who made the commit
Also we have those huge hex1 numbers after the word commit.
This is the commit ID or commit hash. This is universally unique number that you can use to identify a
particular commit.
Normally you don’t need to know this, but it can be useful for going back in time or keeping track of
commits in multideveloper projects.
We also see a bit at the top that says (HEAD -> main). What’s that about?
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 subdirectory. 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.
More output
We see HEAD right there on the first line, indicating that HEAD is referring to commit with ID:
5e8cb52cb813a371a11f75050ac2d7b9e15e4751
Again that’s a bit of a lie, though. 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.
1
https://en.wikipedia.org/wiki/Hexadecimal
Chapter 4. The Git Log and HEAD 21
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 been since
removed, and you wanted to look at them, for example.
I can use the git checkout command to make that happen.
Let’s checkout 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:
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 -
Looks sort of scary, but look—Git is telling us how to undo the operation if we want, and so there’s really
nothing to fear.
Let’s take a look around with git log:
Chapter 4. The Git Log and HEAD 22
Added
That’s all! Just one commit?! Where’s the second commit I made? Is it gone forever?!
No. Everything is fine.
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.
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” com-
ment. So we don’t see it from this perspective.
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 get back to the main branch. There are three options:
1. git switch -, just like the helpful message says.
2. git switch main
3. git checkout main
Git replies:
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
and our working tree will be updated to show the files as they are in the main commit.
and it says:
Hmmm! git switch is warning us that we’re about to go into detached head state, and is that what we
really want? It’s not a crime or anything to do so, but it’s just letting us know that we’re not going to be
on a branch any longer.
So we can override, just like it suggests:
All right! No big message about being detached, but we don’t need it because we know it’s detached since
we specified.
And like before, we can get back to the main branch with either:
1. git switch -, switch to the previous state
2. git switch main
Easy.
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:
Chapter 4. The Git Log and HEAD 24
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.
But Git offers something more powerful, allowing you (or collaborators) to pursue multiple branches
simultaneously.
So there might be multiple collaborators working on the project at the same time.
25
Chapter 5. Branches and Fast-Forward Merges 26
And then, when you’re ready, you can merge those branches back together. In this diagram we’ve merged
commit 6 and 7 into a new commit, commit 9. In Figure 5.4, 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.
Chapter 5. Branches and Fast-Forward Merges 27
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.
When we talk about rebasing later, this will make more sense.
But if we check out an earlier commit that doesn’t have a branch, we end up in detached head state, and
it looks like Figure 5.8.
So far, we’ve been making commits on the main branch without really even thinking about branching.
Recalling 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 the 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.
Chapter 5. Branches and Fast-Forward Merges 28
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.
You might already have main checked out (i.e. HEAD points to main), but let’s do it again to be safe, and
then we’ll create a branch with git switch:
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 use the -c switch to create the
branch before switching to it.
ProTip: 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 with git stash.
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.
Important note: the branches we’re making here exist only on your local clone; they’re not au-
tomagically propagated 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 an example of a remote-tracking branch, which is why
git push from main works while git push from newbranch gives an error. But we’ll talk about
all this later.
Right after creating newbranch, we had the situation in Figure 5.12. Now let’s edit something in the
working tree and make a new commit. With that, we’ll have the scenario in Figure 5.13.
We can see that newbranch and main are pointing at different commits.
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.
Chapter 5. Branches and Fast-Forward Merges 31
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! 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.
Chapter 5. Branches and Fast-Forward Merges 32
At this point, assuming a completed merge, we can delete the topic 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 your 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!
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?
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.
Is all hope lost? How can we merge?
33
Chapter 6. Merging and Conflicts 34
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.
Which one is “right”? Git has no idea because it’s just dumb software and doesn’t know our business
needs.
So it asks us, during the merge, to fix it. After we fix it, Git can complete the merge.
There’s an important point here. When you’re merging, if a conflict occurs, you’re still merging.
Git is is the “merge” state, waiting for more merge-specific commands.
You can resolve the conflict then commit the changes to complete the merge. Or you can back out
of the merge making as if you’d never started it in the first place.
The important point is that you’re aware Git is in a special state and you have to either complete
(or abort) the merge to get back to normal before you continue to use it.
Let’s have an example where both main and newbranch have added a line to end of file, i.e. they both
added line 4. Git doesn’t know which one is correct, so there’s a conflict.
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")
But what I didn’t realize was that my teammate had also made another commit on newbranch that added
different lines to the bottom of the file.
So when I went to merge newbranch into main, I got this conflict. Git doesn’t know which additional
lines are correct.
Here’s where the fun begins. Let’s edit foo.py here in the middle of the merge and see what it looks
like:
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 merg-
ing 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
everything else, and make it look exactly like we agreed upon. I make it look Right.
Then I add the file to the stage and make a merge commit. (Here we’re manually making the merge
commit, unlike above where Git was able to automatically make it.)
Changes to be committed:
modified: foo.py
Chapter 6. Merging and Conflicts 37
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.
So let’s do that, making the merge commit:
$ 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
Commmit 4
Commmit 3
commit 3ab961073374ec26734c933503a8aa988c94185b
Author: User Name <user@example.com>
Date: Sun Feb 4 13:16:32 2024 -0800
Commmit 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 UUIDs for the two commits that it came from
(the first 7 digits, anyway).
This is a shorter chapter, but we want to talk about Git’s behavior when it comes to working in subdirec-
tories and some gotchas that you probably don’t want to get wrapped up in.
39
Chapter 7. Using Subdirectories with Git 40
We 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.
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.
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)
.gitignore
41
Chapter 8. Ignoring Files with .gitignore 42
$ git status
On branch main
nothing to commit, working tree clean
subdir/subdir2/foo.txt
That will match anywhere in the project. If you want to only match a specific file from the project root,
you can prepend a slash:
/subdir/subdir2/foo.txt
Note that means subdir in the root of the project, not the root directory of your entire filesystem.
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.”
!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 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 push (part of which is
fetch) and pull. 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.
45
Chapter 9. Remotes: Repos in Other Places 46
$ 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 the 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 cus-
tomary 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.
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 and 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.
Importantly, not only do the words origin/main refer to the main branch on origin in casual conver-
sation, 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 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.
48
Chapter 10. Remote Tracking Branches 49
This will do a couple things: 1) it’ll push changes on your local main to the remote server, and 2) it’ll
remember that the remote branch origin/main is tracking your local main branch.
And then, from then on, from the main branch, you can just:
$ 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.
Bonus Info: 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.
Now let’s push!
$ 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.
If you pull down that main button, you’ll see topic99 there as well. You can select either branch and
view it in the GitHub interface.
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).
51
Chapter 11. File States 52
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
$ 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.
Chapter 11. File States 53
% 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()
54
Chapter 12. Comparing Files with Diff 55
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 the that 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.
So in this mental model, something is always on the stage. It’s just that you don’t see it unless it’s some-
thing different than the last commit that you put there with git add.
OK? I know I’m asking you to just bear with me on faith, so thank you for that.
1
The --staged flag is more modern. Older versions of Git used git diff --cached.
Chapter 12. Comparing Files with Diff 56
Back to the question: if you have added some modified files to the stage, why does git diff show nothing
is changed?
It’s because 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.
Got it?
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:
Or use HEAD:
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
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
Admittedly, that 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, you get some nice diffing for free and don’t necessarily 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 12.1.
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 try that, it won’t work. You have to configure it first.
Chapter 12. Comparing Files with Diff 59
12.4.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 PATH2 , 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.
• Meld6
• Kdiff37
• Beyond Compare8
• DiffMerge9
• P4Merge10
• Araxis Merge11
Some of these are free, some are paid, and some are free trial.
And remember, VS Code has this functionality without using difftool.
6
https://meldmerge.org/
7
https://kdiff3.sourceforge.net/
8
https://www.scootersoftware.com/
9
https://sourcegear.com/diffmerge/
10
https://www.perforce.com/products/helix-core-apps/merge-diff-tool-p4merge
11
https://www.araxis.com/merge/index.en
Chapter 13
This is an extension of dealing with file states, so make sure you read that chapter first!
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.
Just remember this part: the easiest way to undo a Staged rename is to just do the reverse rename.
Let’s say we renamed and got here:
61
Chapter 13. Renaming and Removing Files 62
$ 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
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”.
Chapter 13. Renaming and Removing Files 63
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
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
64
Chapter 14. Collaboration across Branches 65
Two people shouldn’t generally be editing the same part of the same file, or even any part of the same file.
That’s more of a guideline than a rule, but if you follow it, you will never have a merge conflict.
As we’ve seen, it’s not the end of the world if there is a merge conflict, but life sure is easier if they’re just
avoided.
Takeaway: without good communication and a good distribution of work on your team, you’re doomed.
Make a plan where no one is stepping on toes, and stick to it.
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 looking at 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. (More on tagging later.)
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.
• Unless you’re rebasing, the incremental work on your branch might “pollute” the commit history
on dev 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 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.
Chapter 14. Collaboration across Branches 68
• 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.
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.
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.
69
Chapter 15. Rebasing: Moving Commits 70
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 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!
$ 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:
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.
Chapter 15. Rebasing: Moving Commits 72
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.
You are currently rebasing branch 'topic' on '6ceeefb'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
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
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 you 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:
Done.
Wait! There’s another typo! Are you kidding me?
So you fix it:
Chapter 15. Rebasing: Moving Commits 75
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>
Date: Wed Jul 17 11:53:10 2024 -0700
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 instruc-
tions.
Notice that they’re listed in forward order instead of the reverse log order we’re used to.
Look at all those options! 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”.
And pick just means “use this commit as-is”.
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.
Chapter 15. Rebasing: Moving Commits 77
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.
Finally, after all this, now you can push.
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:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
79
Chapter 16. Stashing: Temporarily Set Changes Aside 80
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.
• git stash list shows you the current stash stack.
• git stash drop deletes a particular stash stack entry.
Because of this, I could stash, then do something else, then stash again, and we’ll have two stashes on
the stack.
1
https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
Chapter 16. Stashing: Temporarily Set Changes Aside 81
Similarly stash drop will pop the top of the stack and not apply the changes to the working tree, discard-
ing 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.
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 were tried to fix it, but then we see that conflicts with
Chris’s fix from upstream.
Chapter 16. Stashing: Temporarily Set Changes Aside 82
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")
What if you want to make changes to a repo on GitHub but you don’t have write permission? Here’s how.
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.
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.
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.
83
Chapter 17. GitHub: Forking and Pull Requests 84
At this point, you should land on the project page for your fork, and the fine print on the page reads:
“forked from beejjorgensen/git-example-repo”.
And now we have our own version of that repo on GitHub to do with as we please.
We can clone it as normal, pull, push, delete the repo, etc. The owner of the original repo will not know
about it—our changes affect our repo alone. Later we’ll see how to issue a pull request to try to get our
changes in the original upstream repo.
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 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.
Chapter 17. GitHub: Forking and Pull Requests 85
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 diff2 . Lines marked with a + are added, and lines marked
with - are removed.
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(+)
There’s no way in the UI to delete PRs, whether you’re the forker or forkee. And this can be a bummer
especially if you’ve accidentally included some sensitive information like social security number 078-05-
11203 .
But hope is not all lost! The upstream owner can visit the virtual assistant at GitHub and ask for a pull
request removal4 which apparently works. I haven’t tried it.
If there’s a way as the forker to delete the PR they created, I haven’t seen it. You’ll have to plead your
case with the upstream owner and get them to do it.
In any case, you most definitely should change your leaked credentials right now and let that be a lesson
to you.
And then we need to get the new commits from the upstream repo and merge them into our stuff.
3
https://en.wikipedia.org/wiki/Social_Security_number#SSNs_used_in_advertising
4
https://support.github.com/request?q=pull+request+removals
Chapter 17. GitHub: Forking and Pull Requests 89
Automatic merge failed; fix conflicts and then commit the result.
$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 999 bytes | 999.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local
remote: object.
To github.com:user/fork.git
8b2476c..c8a7e0a main -> main
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!
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:
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!
90
Chapter 18. Reverting: Undoing Commits 91
$ 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:
$ 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)
Chapter 18. Reverting: Undoing Commits 92
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.
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
discussed.
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 it is?
Doing a reset allows you change where the HEAD and your current branch point. 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.
93
Chapter 19. Reset: Moving Branches Around 94
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.
This will reset the current branch to where it already was (assuming HEAD points to the current branch),
and reset the stage to be the same as that commit. This unstages the files that were there. And it changes
the working tree files to have the changes that were already present in those files at that point, which
would be any changes you introduced.
And that unstages the files!
Another use might be if you want to squash a bunch of unpushed commits but simply don’t want to stage
the changes at the old commit yet, leaving them as modified.
$ 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
3
No guarantees. You shouldn’t rewrite commit history that is already public!! It makes a big mess!
Chapter 19. Reset: Moving Branches Around 97
Never do a forced push without completely understanding why you’re doing it. Git is trying
to stop you from doing a push for a reason: your own good! Everyone else who has cloned the
repo will very likely be impacted and they need to be informed. Everybody. We use it here to
demonstrate when it is necessary.
Our process will be something like this:
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 example.
4
Perhaps using git reflog and git cherry-pick or git cherry-pick -n and potentially git add -p, all of which are cov-
ered 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 98
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 99
$ 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.
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
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:
100
Chapter 20. Appendix: Making a Playground 101
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.
$ 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 see-
ing 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
Since playground and playground2 are both clones of the same repo, you can push from one and pull
from the other to get the changes.
You can even make conflicting changes and try to git pull or git pull --rebase and see how things
go wrong and how to fix them.
And if everything goes complete off the rails, you can just delete the directories and start again. It’s a
playground!
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(+)
Protip: If you run sh -x buildrepo.sh it will also show you the commands it is running.
$ 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
Chapter 20. Appendix: Making a Playground 104
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 21
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. Learning Vim1 is beyond the scope of this guide, but this author
thinks it’s worth it for the editing speed you can achieve.
1
https://www.openvim.com/
105
Chapter 22
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”.
git switch -
106
Chapter 22. Appendix: Errors and Scary Messages 107
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.
Index
109
INDEX 110
Untracking files, 52
Workflow
basic, 5–12
Dev branch, 66
File states, 51
One Branch, 65
One Branch per Dev, 65