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).
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..
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>
.
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
Which branch am I on?¶
git branch
shows a list of available branches.
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
.
How do I change the current branch?¶
To change the current branch, use git switch <branchname>
.
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
git log
shows that HEAD points at main.
We can inspect the contents of .git/HEAD
to confirm that HEAD points at main.
We can tell HEAD to point at a particular commit via git checkout <commit_id>
.
Now when we inspect the contents of .git/HEAD
, it shows a commit ID.
How do I get out of a detached HEAD state?¶
Use
to switch to the latest checked out branch, or
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.
Examples
Merging¶
After splitting a commit graph into branches, you'll eventually want to merge them back together. You can merge two branches using git merge
.
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
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
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
Merge A into B
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
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.)
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 .
-
Staged files become unstaged.
-
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected :octicons-trash-24:. -
The contents of
f1
,f2
, andf3
are preserved in the working tree .
Notegit 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) . -
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected :octicons-trash-24:. -
The contents of
f1
,f2
, andf3
are preserved in the working tree .
-
Staged files become unstaged.
-
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected :octicons-trash-24:. -
The contents of
f1
,f2
, andf3
are preserved in the working tree .
Notegit 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 :octicons-trash-24:. -
f1
,f2
, andf3
revert to their states as of commite3ccf9c
. Sincef2
andf3
didn't exist in commite3ccf9c
, they are removed. .
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.)
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 .
-
Staged files become unstaged.
-
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected :octicons-trash-24:. -
The contents of
f1
,f2
, andf3
are preserved in the working tree .
Notegit 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) . -
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected :octicons-trash-24:. -
The contents of
f1
,f2
, andf3
are preserved in the working tree .
-
Staged files become unstaged.
-
HEAD and main move to commit
4d7bd61
. Since there are no remaining references to commite3ccf9c
, it's garbage collected :octicons-trash-24:. -
The contents of
f1
,f2
, andf3
are preserved in the working tree .
Notegit 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 :octicons-trash-24:. -
f1
,f2
, andf3
revert to their states as of commite3ccf9c
. Sincef2
andf3
didn't exist in commite3ccf9c
, they are removed. .