Skip to content

Intermediate

Skip to the problems!

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?

  1. The new commit maps to the previous commit (its parent).
  2. 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:

  1. Non-Detached HEAD:
    HEAD references a branch. But since a branch references a commit, this means that HEAD references a commit indirectly.

  2. 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

Setup
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
  1. 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.

merged Y into X
git switch X
git merge Y

   Z      <-- latest commit
  / \
 /   \
X     Y   <-- earlier commits
merged X into Y
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.

  1. 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

cookie
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 .

git-reset-2

Click on the tabs below to see what git reset does in each case ๐Ÿ‘‡

git-reset-3

  • Staged files become unstaged.
  • No changes to the commit graph.
  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      
      f3
      flour
      

git-reset-4

  • Staged files become unstaged.
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .
  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      
      f3
      flour
      

Note
git reset <commit ID> is equivalent to git reset --mixed <commit ID>

git-reset-5

  • Diffs from commit 4d7bd61 to commit e3ccf9c are put in the staging area (unless they conflict with the items in staging) (1).

    1. You can view the staged diffs with git diff --staged, or view a staged file's contents with git show :file

      f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      
      f3
      flour
      
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .

  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      
      f3
      flour
      

git-reset-4

  • Staged files become unstaged.
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .
  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      
      f3
      flour
      

Note
git reset --mixed <commit ID> is equivalent to git reset <commit ID>

git-reset-6

  • Staged files become unstaged.
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .
  • f1, f2, and f3 revert to their states as of commit e3ccf9c. Since f2 and f3 didn't exist in commit e3ccf9c, they are removed. (1).

    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

git-reset-7

Click on the tabs below to see what git reset does in each case ๐Ÿ‘‡

git-reset-8

  • Staged files become unstaged.
  • No changes to the commit graph.
  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      m&m
      
      f3
      butter
      

git-reset-9

  • Staged files become unstaged.
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .
  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      m&m
      
      f3
      butter
      

Note
git reset <commit ID> is equivalent to git reset --mixed <commit ID>

git-reset-10

  • Diffs from commit 4d7bd61 to commit e3ccf9c are put in the staging area (unless they conflict with the items in staging) (1).

    1. You can view the staged diffs with git diff --staged, or view a staged file's contents with git show :file

      f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      
      f3
      flour
      
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .

  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      m&m
      
      f3
      butter
      

git-reset-9

  • Staged files become unstaged.
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .
  • The contents of f1, f2, and f3 are preserved in the working tree (1).

    1. f1
      me want cookie
      me love cookies
      
      f2
      chocolate chip
      oatmeal raisin
      m&m
      
      f3
      butter
      

Note
git reset --mixed <commit ID> is equivalent to git reset <commit ID>

git-reset-11

  • Staged files become unstaged.
  • HEAD and main move to commit 4d7bd61. Since there are no remaining references to commit e3ccf9c, it's garbage collected .
  • f1, f2, and f3 revert to their states as of commit e3ccf9c. Since f2 and f3 didn't exist in commit e3ccf9c, they are removed. (1).

    1. f1
      me want cookie