Intermediate
So far, we've seen how to stage and commit files to create linear commit graphs that look something like this.
Of course, the purpose of Git isn't just to make commits! We want the ability to
- experiment with drastic changes to the codebase
- experiment with divergent changes in parallel
- restore our work to previous versions (commits) when necessary
This means our commit graph should have the ability to branch
and merge.
Commit IDs¶
Every commit has a unique ID. You can see them when you run git log
(although the full IDs may be truncated for the display).
bill@gates:~$ git log
commit 8436ff07b9752ddb387f913f47e4a15052413e0d (HEAD -> main)
Author: bill <billgates@gmail.com>
Date: Fri Sep 16 19:44:08 2022 -0500
inserted some text in s1
commit 562322d16ecde3e8109e7fb57fb8fbf81bb08b43
Author: bill <billgates@gmail.com>
Date: Fri Sep 16 19:43:37 2022 -0500
added s2
commit c59ba58182b4b4009f3ed5ef7ee1df04b65ed083
Author: bill <billgates@gmail.com>
Date: Fri Sep 16 19:43:27 2022 -0500
added s1
Each ID is a 40-character SHA-1 hash of the commit content, author, message, parent commit ID, and other things.
Tip
Suppose Bob and Jane each look at the most recent commit ID on their computers and they are the same..
Bob@Smith:~$ git log --format="%H" -n 1
562322d16ecde3e8109e7fb57fb8fbf81bb08b43
Jane@Doe:~$ git log --format="%H" -n 1
562322d16ecde3e8109e7fb57fb8fbf81bb08b43
Because they are the same, Bob and Jane can be confident that they both have access to a copy of the same exact commit and the same exact history leading up to that commit.
Branching¶
By default, every git repo starts with one branch - main. If you have a project with at least one commit,
you can make a new branch using git branch <branchname>
.
bill@gates:~$ git branch feature1
What exactly is a branch?¶
Internally, a branch is just a pointer (i.e. a reference) to a particular commit ID.
Consider the following commit graph.
It has three branches.
- feature1: points to commit ID
de3e
- feature2: points to commit ID
2d16
- main: points to commit ID
8b43
Although each branch is stored as a pointer to a commit, most people interpret a branch as the path of reachable commits from the branch tip.
For example, you can interpret the feature1 branch as this path of commits.
And you can interpret the feature2 branch as this path of commits.
See for yourself
You can reproduce the commit graph depicted above with the following commands.
cd path/to/parent/dir/
mkdir foo && cd foo
git init
touch foo.txt
git add foo.txt
git commit -m "first commit"
git checkout -b feature1
touch bar.txt
git add bar.txt
git commit -m "experiment feature"
echo "xyz" > bar.txt
git add bar.txt
git commit -m "implement algo"
touch config
git add config
git commit -m "added config"
git switch main
git checkout -b feature2
echo "abc" > foo.txt
git add foo.txt
git commit -m "fixed bug"
git switch main
git merge feature2
touch module1
git add module1
git commit -m "added module"
Now take a look inside the .git/
directory. The branches are listed inside .git/refs/heads
.
If you view the contents of each file, you'll see the commit ID representing the tip of each branch.
bill@gates:~$ cat .git/refs/heads/main
4a9554edd4807df75f613feb654c4813bc1f9284
bill@gates:~$ cat .git/refs/heads/feature1
3a591f695de105481b930f1f5cc2af7f41ca6b7c
bill@gates:~$ cat .git/refs/heads/feature2
bb5bf66ab36ee92619397bdd6150643f078112fa
You can also visualize the commit graph with git log --oneline --all --graph
.
bill@gates:~$ git log --oneline --all --graph
* 3a591f6 (feature1) added config
* aa1b63a implement algo
* 3cc35e9 experiment feature
| * 4a9554e (HEAD -> main) added module
| * bb5bf66 (feature2) fixed bug
|/
* 91ee3d8 first commit
Which branch am I on?¶
git branch
shows a list of available branches.
bill@gates:~$ git branch
feature1
feature2
* main
The one with the asterisk is
- the "current branch"
- AKA "the branch you're on"
- AKA "the branch you've checked out".
You can also see the current branch by running git status
.
bill@gates:~$ git status
On branch main
nothing to commit, working tree clean
How do I change the current branch?¶
To change the current branch, use git switch <branchname>
.
bill@gates:~$ git switch feature1
Switched to branch 'feature1'
What happens when I make a new commit?
- The new commit maps to the previous commit (its parent).
- The branch (pointer) you were on when you made the commit updates to point at the new commit.
What's HEAD?¶
By now, you've probably noticed something named HEAD. HEAD is a special pointer (i.e. a reference) to a commit. It comes in two flavors:
-
Non-Detached HEAD:
HEAD references a branch. But since a branch references a commit, this means that HEAD references a commit indirectly. -
Detached HEAD:
HEAD references a commit directly.(Even though HEAD and main point to the same commit, HEAD is still considered "detached" because it points at a commit instead of a branch.)
What's the purpose of HEAD?¶
Git needs to compare your working tree against something to determine if you've modified files, added files, deleted files, etc. That something is the commit pointed to by HEAD.
See for yourself
cd path/to/parent/dir/
mkdir roux && cd roux
git init
touch foo.txt
git add foo.txt
git commit -m "added foo"
touch bar.txt
git add bar.txt
git commit -m "added bar"
git log
shows that HEAD points at main.
bill@gates:~$ git log --oneline
df98e21 (HEAD -> main) added bar
4fc1b3d added foo
We can inspect the contents of .git/HEAD
to confirm that HEAD points at main.
bill@gates:~$ cat .git/HEAD
ref: refs/heads/main
We can tell HEAD to point at a particular commit via git checkout <commit_id>
.
bill@gates:~$ git checkout 4fc1b3d
Note: switching to '4fc1b3d'. # (1)!
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 4fc1b3d added foo
- Your commit ID will be different!
Now when we inspect the contents of .git/HEAD
, it shows a commit ID.
bill@gates:~$ cat .git/HEAD
4fc1b3d5463b12c03f24004a4ed16c3c9230408a
How do I get out of a detached HEAD state?¶
Use
git switch -
to switch to the latest checked out branch, or
git switch <branchname>
to switch to a particular branch.
Relative commits¶
We've seen how to reference commits using their SHA1 hash IDs, but sometimes you'll want to reference them relatively.
You can achieve this with ~
and ^
.
In the definitions below, Z
represents a commit. However, you can replace Z
with HEAD
. If HEAD
points to Z
, then HEAD~1
and Z~1
mean the same thing.
Definitions
Z~N
-
Traverse the commit graph N steps up from
Z
. If a commit has multiple parents, step towards the first parent. Z^N
-
Return
Z
's Nth parent. Z^M^N
-
Return
Z
's Mth parent. Then return that commit's Nth parent.
When a commit has multiple parents, how are they ordered?
When merging commit Y
into X
to create Z
, X
becomes Z
's first parent.
git switch X
git merge Y
Z <-- latest commit
/ \
/ \
X Y <-- earlier commits
git switch Y
git merge X
Z <-- latest commit
/ \
/ \
Y X <-- earlier commits
Examples
D E <-- HEAD
| / \
|/ \
B C
\ /
\/
A <-- first commit
HEAD~1 = E~1 = B
D E <-- HEAD
| / \
|/ \
B C
\ /
\/
A <-- first commit
HEAD~2 = E~2 = A
D E <-- HEAD
| / \
|/ \
B C
\ /
\/
A <-- first commit
HEAD^1 = E^1 = B
D E <-- HEAD
| / \
|/ \
B C
\ /
\/
A <-- first commit
HEAD^2 = E^2 = C
D E <-- HEAD
| / \
|/ \
B C
\ /
\/
A <-- first commit
HEAD^2^1 = E^2^1 = A
Merging¶
After splitting a commit graph into branches, you'll eventually want to merge them back together. (1) You can merge two
branches using git merge
.
- Actually, sometimes you might just delete a branch. For example, if you were working on an experimental feature and you hit a dead end.
There are two patterns for merging in Git: fast forward and three way.
Fast Forward merge¶
Suppose you have a commit graph like this, and you want to merge branches A and B.
Merge B into A
In order to merge branch B into branch A, do
git switch A
git merge B
In this case, A's pointer updates to point at the commit pointed to by B. (The working tree will update as well.)
Merge A into B
In order to merge branch A into branch B, do
git switch B
git merge A
In this case, A already exists in B's history, so Git doesn't do anything.
Some tools will tell you "B is ahead of A by one commit".
Three Way merge¶
Suppose you have a commit graph like this, and you want to merge branches A and B.
In this case, Git creates a new commit to represent the merge.
Merge B into A
git switch A
git merge B
Merge A into B
git switch B
git merge A
In a three way merge, it's possible that A and B conflict with each other. (Perhaps A modified a file that B deleted.) When a merge has conflicts, you'll need to resolve them. We'll show examples of this in the problem set.
git reset
¶
The git reset
command can be used to reset the index (staging area) and working tree back to a previous
state. You can also use it to move a branch pointer.
For example, suppose you set up a repository like this
cd path/to/parent/dir/
mkdir cookie && cd cookie && git init
echo "me want cookie" > f1
git add .
git commit -m "added f1"
echo "me love cookies" >> f1
echo "chocolate chip" > f2
git add .
git commit -m "modified f1 and added f2"
echo "oatmeal raisin" >> f2
echo "flour" >> f3
Here's what happens when you call git reset
under various scenarios.
Stage all files¶
In this scenario, we stage all modified and untracked files. (See the initial code here.)
bill@gates:~$ git add .
Click on the tabs below to see what git reset
does in each case
- Staged files become unstaged.
- No changes to the commit graph.
-
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin
f3flour
-
- Staged files become unstaged.
- HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin
f3flour
-
Note
git reset <commit ID>
is equivalent to git reset --mixed <commit ID>
-
Diffs from commit
4d7bd61
to commite3ccf9c
are put in the staging area (unless they conflict with the items in staging) (1).-
You can view the staged diffs with
git diff --staged
, or view a staged file's contents withgit show :file
f1me want cookie me love cookies
f2chocolate chip oatmeal raisin
f3flour
-
-
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin
f3flour
-
- Staged files become unstaged.
- HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin
f3flour
-
Note
git reset --mixed <commit ID>
is equivalent to git reset <commit ID>
- Staged files become unstaged.
- HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
f1
,f2
, andf3
revert to their states as of commite3ccf9c
. Sincef2
andf3
didn't exist in commite3ccf9c
, they are removed. (1).-
f1
me want cookie
-
Stage all files, then make changes¶
In this scenario, we stage all modified and untracked files. Then we append a line to f2
and modify the only line
in f3
. (See the initial code here.)
bill@gates:~$ git add .
bill@gates:~$ echo "m&m" >> f2
bill@gates:~$ echo "butter" > f3
Click on the tabs below to see what git reset
does in each case
- Staged files become unstaged.
- No changes to the commit graph.
-
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin m&m
f3butter
-
- Staged files become unstaged.
- HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin m&m
f3butter
-
Note
git reset <commit ID>
is equivalent to git reset --mixed <commit ID>
-
Diffs from commit
4d7bd61
to commite3ccf9c
are put in the staging area (unless they conflict with the items in staging) (1).-
You can view the staged diffs with
git diff --staged
, or view a staged file's contents withgit show :file
f1me want cookie me love cookies
f2chocolate chip oatmeal raisin
f3flour
-
-
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin m&m
f3butter
-
- Staged files become unstaged.
- HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
The contents of
f1
,f2
, andf3
are preserved in the working tree (1).-
f1
me want cookie me love cookies
f2chocolate chip oatmeal raisin m&m
f3butter
-
Note
git reset --mixed <commit ID>
is equivalent to git reset <commit ID>
- Staged files become unstaged.
- HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected . -
f1
,f2
, andf3
revert to their states as of commite3ccf9c
. Sincef2
andf3
didn't exist in commite3ccf9c
, they are removed. (1).-
f1
me want cookie
-