Introduction to Git - Fall 2020

Lecture 2: Basic concepts

Slides: https://hackmd.io/@hpc2n-git-2020/L2-concepts


Remark

  • You are not intended memorize any commands or low-level details.
  • The goal is to learn the basic concepts:
    • hash sums, blobs, trees, commits, references, branches,
  • Understanding these concepts helps to understand what the commands actually do!

What is Git?

  • Git is a distributed VCS:
    • Does not rely on a server-client model.
    • Instead, everyone has a full copy the entire project (repository).
      • Complete history, metadata, etc.
    • People can work completely independently.
    • An (optional) server is used to only to distribute changes.

Why use Git?

  • It is popular.
    • Many project already use it, people know how to use it, people can tell you how to use it,
  • Relies on hash sums:
    • Built-in data corruption detection.
    • Built security.
  • Distributed.
  • Fast, simple and flexible.
  • Free and open-source.

How does Git store the history?


What is inside a repository?

$ mkdir repository && cd repository
$ git init
Initialized empty Git repository in .../repository/.git/
$ find
digraph {
  "repository/" -> ".git/"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/"
  "refs/" -> "tags/"
}

Most directories are empty and the files are not that interesting:

$ cat .git/config 
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
$ cat .git/HEAD 
ref: refs/heads/master
$ cat .git/description 
Unnamed repository; edit this file 'description' to name the
repository.

Lets add some content:

$ echo "This file is very interesting" > file.txt
$ git add file.txt
$ git commit -m "This is the first commit"
[master (root-commit) 23b3ed5] This is the first commit
 1 file changed, 1 insertion(+)
 create mode 100644 file.txt
$ find
digraph {
  "file.txt" [fontcolor=red]
  "logs/" [fontcolor=red]
  "master" [fontcolor=red]
  "COMMIT_EDITMSG" [fontcolor=red]
  "index" [fontcolor=red]
  "23" [fontcolor=red]
  "b3ed5b16..." [fontcolor=red]
  "1a" [fontcolor=red]
  "098a06bf..." [fontcolor=red]
  "09" [fontcolor=red]
  "c78e6e97..." [fontcolor=red]

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23" -> "b3ed5b16..."
  "objects/" -> "1a" -> "098a06bf..."
  "objects/" -> "09" -> "c78e6e97..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
}

Working tree

  • Everything inside repository/ is a part of the working tree (or the workspace).
    • .git/ is not included.
    • At the moment, the working tree contains just one file, file.txt.
    • Working tree is just a regular directory.
  • The git add and git commit commands tell Git to care about file.txt.
    • More on that later

Objects

  • Git stores files etc as objects:
    • Objects are stored under .git/objects/.
  • Git uses content-based addressing.
    • A hash sum is computed from the content of the object.
    • The hash "uniquely" identifies the object.
    • Two objects with identical contents have the same hash and are stored only once.

  • We can compute the hash manually:
$ git hash-object file.txt
09c78e6e971ce9e3d69e75bcb3ffd5de05b0d59a
  • We can find the corresponding object:
$ find
...
./.git/objects/09/c78e6e971ce9e3d69e75bcb3ffd5de05b0d59a
...
  • We can confirm that two files with identical contents have the same hash:
$ cp file.txt file2.txt 
$ git hash-object file.txt file2.txt
09c78e6e971ce9e3d69e75bcb3ffd5de05b0d59a
09c78e6e971ce9e3d69e75bcb3ffd5de05b0d59a

  • Note that we do not have to use the entire hash:
git cat-file -p 09c78e6e
This file is very interesting
  • We only need to use as many characters as is required to uniquely identify the object.
    • 7-8 is enough in most cases.
    • 12 in larger projects.
  • If more characters is required, an error message is printed.

  • Objects cannot not (and should not) be accessed directly:
$ hexdump -C ./.git/objects/09/c78e6e97*
00000000  78 01 4b ca c9 4f 52 30  ....  |x.K..OR06`...,VH|
00000010  cb cc 49 55 00 d2 65 a9  ....  |..IU..e.E...y%.E|
00000020  a9 c5 25 99 79 e9 5c 00  ....  |..%.y.\..I.3|
0000002c
  • However, we can observe the type and the content of an object:
$ git cat-file -t 09c78e6e
blob
$ git cat-file -p 09c78e6e
This file is very interesting

  • It is also important to realize that the object stays even when the file is removed:
$ rm file.txt
$ find
....
./.git/objects/09/c78e6e971ce9e3d69e75bcb3ffd5de05b0d59a
....
$ git cat-file -p 09c78e6e971ce9e3d69e75bcb3ffd5de05b0d59a
This file is very interesting
  • We can restore the file from the object:
$ git restore file.txt
$ cat file.txt 
This file is very interesting

Let's take a second look at the repository:

digraph {
  "file.txt"
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index"
  "23"
  "b3ed5b16..." [fontcolor=red]
  "1a"
  "098a06bf..." [fontcolor=red]
  "09"
  "c78e6e97..."

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23" -> "b3ed5b16..."
  "objects/" -> "1a" -> "098a06bf..."
  "objects/" -> "09" -> "c78e6e97..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
}

What are these two other objects?


Trees

  • Let's investigate one of the remaining objects:
$ git cat-file -t 1a098a06
tree
$ git cat-file -p 1a098a06
100644 blob 09c78e6e971ce9e3d69e75b....    file.txt
  • We can see that the type of the object is tree:
    • A tree stores pointers to
      • files (blobs) and
      • other trees,
    • Trees are used to represent directory structures.

In this case, the tree has one level and one blob:

digraph {
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]

  "tree 1a098a06b...\nblob 09c78e6e.... file.txt" -> first_blob
}

Let's take a third look at the repository:

digraph {
  "file.txt"
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index"
  "23"
  "b3ed5b16..." [fontcolor=red]
  "1a"
  "098a06bf..."
  "09"
  "c78e6e97..."

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23" -> "b3ed5b16..."
  "objects/" -> "1a" -> "098a06bf..."
  "objects/" -> "09" -> "c78e6e97..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
}

Just one object remains


Commits

  • Let's investigate the last object:
$ git cat-file -t 23b3ed5b
commit
$ git cat-file -p 23b3ed5b
tree 1a098a06bf0bcae9695238d9d5cb96345c00cacf
author Mirko Myllykoski <....@gmail.com> 1600867851 +0200
committer Mirko Myllykoski <....@gmail.com> 1600867851 +0200

This is the first commit
  • The type of the object is commit. It contains
    • a pointer to a tree,
    • an author and a committer (+time), and
    • a commit message

A commit stores the state of the project in a given point of time.


In this case, the commit points to a tree that has one level and one blob:

digraph {
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  file [label="file.txt\nThis file is very interesting" shape=box]

  "commit 23b3ed5b1...\ntree 1a098a06b\nMirko Myll...\nThis is the first commit" -> "tree 1a098a06b...\nblob 09c78e6e.... file.txt" -> first_blob
  
  "metadata" -> "repository/" -> file
}

In a more general case, the associated tree can contain several levels and multiple blobs:

digraph {
  file1 [label="file1.txt" shape=box]
  file2 [label="file2.txt" shape=box]
  file3 [label="file3.txt" shape=box]
  file4 [label="file4.txt" shape=box]

  blob1 [label="blob 1" shape=box]
  blob2 [label="blob 2" shape=box]
  blob3 [label="blob 3" shape=box]
  blob4 [label="blob 4" shape=box]

  "commit 1" -> "tree 1"
  "tree 1" -> blob1
  "tree 1" -> blob2
  "tree 1" -> "tree 2"
  "tree 2" -> blob3
  "tree 2" -> blob4
  
  "metadata" -> "repository/"
  "repository/" -> file1
  "repository/" -> file2
  "repository/" -> "directory/"
  "directory/" -> file3
  "directory/" -> file4
}

Working with Git


Let's see what else we can find

digraph {
  "file.txt"
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index"
  "23"
  "b3ed5b16..."
  "1a"
  "098a06bf..."
  "09"
  "c78e6e97..."

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23" -> "b3ed5b16..."
  "objects/" -> "1a" -> "098a06bf..."
  "objects/" -> "09" -> "c78e6e97..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
  
  "HEAD" [fontcolor=red]
  "refs/" [fontcolor=red]
  "heads/" [fontcolor=red]
  "master" [fontcolor=red]
}

HEAD and other references

  • HEAD points (indirectly) to 23b3ed5b1:
$ cat ./.git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
23b3ed5b16095bb84b18d06734fdd614c8982841
digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  "HEAD" -> "master" -> "commit\n23b3ed5b1..." -> "tree\n1a098a06b..." -> first_blob
}

  • HEAD and master are references.
    • A reference points to commits and an other reference.
  • HEAD determines "most recent" commit.
    • Many commands act on the current HEAD.
    • More on this later
  • master is the current branch (more later).

  • You can create a references yourself:
$ git tag first
$ find
digraph {
  "file.txt"
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index"
  "23"
  "b3ed5b16..."
  "1a"
  "098a06bf..."
  "09"
  "c78e6e97..."

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23" -> "b3ed5b16..."
  "objects/" -> "1a" -> "098a06bf..."
  "objects/" -> "09" -> "c78e6e97..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
  "tags/" -> first
  
  "refs/" [fontcolor=red]
  "tags/" [fontcolor=red]
  "first" [fontcolor=red]
}
$ git rev-parse first
23b3ed5b16095bb84b18d06734fdd614c8982841
digraph {
  rankdir=LR
  
  "first" [shape=plaintext]
  
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  "first" -> "commit\n23b3ed5b1..." -> "tree\n1a098a06b..." -> first_blob
}

Index (staging area)

Let's repeat some of the earlier steps:

$ echo "More content" >> file.txt
$ git add file.txt
$ find
digraph {
  "file.txt" [fontcolor=red]
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index"
  "23/"
  "b3ed5b16..."
  "1a/"
  "098a06bf..."
  "09/"
  "c78e6e97..."
  "3b/" [fontcolor=red]
  "23ff0c41..." [fontcolor=red]

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23/" -> "b3ed5b16..."
  "objects/" -> "1a/" -> "098a06bf..."
  "objects/" -> "09/" -> "c78e6e97..."
  "objects/" -> "3b/" -> "23ff0c41..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
  "tags/" -> first
}
$ git cat-file -p 3b23ff0c
This file is very interesting
More content

  • The git add command creates a blob that correspond to the update file.txt file.
    • No other object are created yet.
  • The command also adds the file to the index.
  • The index will become the next commit.
    • Contains a representation of the tree object.

The index is a binary file:

digraph {
  "file.txt"
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index" [fontcolor=red]
  "23/"
  "b3ed5b16..."
  "1a/"
  "098a06bf..."
  "09/"
  "c78e6e97..."
  "3b/"
  "23ff0c41..." 

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23/" -> "b3ed5b16..."
  "objects/" -> "1a/" -> "098a06bf..."
  "objects/" -> "09/" -> "c78e6e97..."
  "objects/" -> "3b/" -> "23ff0c41..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
  "tags/" -> first
}

We can now turn the index to the next commit:

$ git commit -m "This is the second commit"
[master d3c6c63] This is the second commit
 1 file changed, 1 insertion(+)
$ find
digraph {
  "file.txt"
  "logs/"
  "master"
  "COMMIT_EDITMSG"
  "index"
  "23/"
  "b3ed5b16..."
  "1a/"
  "098a06bf..."
  "09/"
  "c78e6e97..."
  "3b/"
  "23ff0c41..."
  "22/" [fontcolor=red]
  "b5208beb..." [fontcolor=red]
  "d3/" [fontcolor=red]
  "c6c635fb..." [fontcolor=red]

  "repository/" -> ".git/"
  "repository/" -> "file.txt"
  ".git/" -> "branches/"
  ".git/" -> "hooks/"
  ".git/" -> "info/ "
  ".git/" -> "logs/"
  ".git/" -> "objects/"
  ".git/" -> "refs/"
  ".git/" -> "COMMIT_EDITMSG"
  ".git/" -> "config"
  ".git/" -> "description"
  ".git/" -> "HEAD"
  ".git/" -> "index"
  "objects/" -> "23/" -> "b3ed5b16..."
  "objects/" -> "1a/" -> "098a06bf..."
  "objects/" -> "09/" -> "c78e6e97..."
  "objects/" -> "3b/" -> "23ff0c41..."
  "objects/" -> "22/" -> "b5208beb..."
  "objects/" -> "d3/" -> "c6c635fb..."
  "objects/" -> "info/"
  "objects/" -> "pack/"
  "refs/" -> "heads/" -> "master"
  "refs/" -> "tags/"
  "tags/" -> first
}

  • Just as before, we have a tree object that describes the directory structure:
$ git cat-file -p 22b5208b
100644 blob 3b23ff0c411faf22c9253ed0....    file.txt
  • And a commit, that describes the state of the repository:
$ git cat-file -p d3c6c635
tree 22b5208bebacfcf745691f799b08df492b2a7da9
parent 23b3ed5b16095bb84b18d06734fdd614c8982841
author Mirko Myllykoski <mirko...> 1601228824 +0200
committer Mirko Myllykoski <mirko....> 1601228824 +0200

This is the second commit

Parent

  • The major difference is that the commit contains a pointer to a parent:
parent 23b3ed5b16095bb84b18d06734fdd614c8982841
  • The parent pointer points to the previous commit:
digraph {
  rankdir=LR
  
  second_commit [label="commit d3c6c635...\ntree 22b5208b\nparent 23b3ed5b1\nMirko Myll..\nThis is the second commit"]
  first_commit [label="commit 23b3ed5b1...\ntree 1a098a06b\nMirko Myll...\nThis is the first commit"]
  
  second_blob [label="blob 3b23ff0c\nThis file is very interesting\nMore content" shape=box]
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  second_commit -> "tree 22b5208b...\nblob 3b23ff0c file.txt" -> second_blob

  first_commit -> "tree 1a098a06b...\nblob 09c78e6e.... file.txt" -> first_blob
  
  second_commit -> first_commit
}

Commit tree

  • Usually, we have a complete tree of commits (commit tree):
digraph {
  rankdir=LR
  "commit 1" -> "tree 1"
  "commit 2" -> "tree 2"
  "commit 2" -> "commit 1"
  "commit 3" -> "tree 3"
  "commit 3" -> "commit 2"
  "commit 4" -> "tree 4"
  "commit 4" -> "commit 3"
}
  • Each commit represents the state of the repository at a given point of time.

  • Each commit is allowed to have multiple parents:
digraph {
  rankdir=LR
  "commit 2" -> "commit 1"
  "commit 4" -> "commit 3"
  "commit 4" -> "commit 2"
}
  • These parents appear when two (or more) branches are merged.
    • More on this later

HEAD and other references (again)

  • Let's investigate HEAD and master:
$ cat .git/HEAD 
ref: refs/heads/master
$ cat .git/refs/heads/master
d3c6c635fb44c7084797d47050bff7961853c19b
digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  
  second_commit [label="commit d3c6c635...\ntree 22b5208b\nparent 23b3ed5b1\nMirko Myll..\nThis is the second commit"]

  first_commit [label="commit 23b3ed5b1...\ntree 1a098a06b\nMirko Myll...\nThis is the first commit"]
  
  second_commit -> first_commit
  
  "HEAD" -> "master" -> second_commit
  
  subgraph cluster_working_tree {
    label="Working tree"
    subgraph cluster_file {
      label="file.txt"
        "This file is very interesting\nMore content
" [shape=plain]
    }
  }
}
  • Remember, many Git commands act on the current HEAD.

  • We can change the HEAD to something else:
$ git checkout 23b3ed5b
....
HEAD is now at 23b3ed5 This is the first commit
$ cat .git/HEAD 
23b3ed5b16095bb84b18d06734fdd614c8982841
$ cat file.txt 
This file is very interesting
digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  
  second_commit [label="commit d3c6c635...\ntree 22b5208b\nparent 23b3ed5b1\nMirko Myll..\nThis is the second commit"]

  first_commit [label="commit 23b3ed5b1...\ntree 1a098a06b\nMirko Myll...\nThis is the first commit"]
  
  second_commit -> first_commit
  
  "HEAD" -> first_commit 
  "master" -> second_commit
  
  subgraph cluster_working_tree {
    label="Working tree"
    subgraph cluster_file {
      label="file.txt"
        "This file is very interesting
" [shape=plain]
    }
  }
}

Branches

  • We can modify the working tree and create a new commit:
$ echo "Different content" >> file.txt 
$ git commit -a -m "This is the third commit"
[detached HEAD a118ae8] This is the third commit
 1 file changed, 1 insertion(+)
  • Let's investigate the newly created commit:
$ git cat-file -p a118ae8c
tree 5fcc4f83fedf5a94cd773704bdb1ab2cdcadc6fd
parent 23b3ed5b16095bb84b18d06734fdd614c8982841
author Mirko Myllykoski <mirko....> 1601286412 +0200
committer Mirko Myllykoski <mirko....> 1601286412 +0200

This is the third commit

  • First, the parent points to the first commit:
digraph {
  rankdir=LR
  
  third_commit [label="commit a118ae8c...\nparent 23b3ed5b1...\nThis is the third commit"]
  first_commit [label="commit 23b3ed5b1...\nThis is the first commit"]
  
  third_commit -> first_commit
}
  • Second, the commit tree now has two branches:
digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  
  third_commit [label="commit a118ae8c...\nThis is the third commit"]
  second_commit [label="commit d3c6c635...\nThis is the second commit"]
  first_commit [label="commit 23b3ed5b1...\nThis is the first commit"]
  
  third_blob [label="blob ea5f4b8e\nThis file is very interesting\nDifferent content" shape=box]
  second_blob [label="blob 3b23ff0c\nThis file is very interesting\nMore content" shape=box]
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  third_commit -> third_blob [style=dashed]
  second_commit -> second_blob [style=dashed]
  first_commit -> first_blob [style=dashed]
  
  third_commit -> first_commit
  second_commit -> first_commit
  
  "HEAD" -> third_commit 
  "master" -> second_commit
  
  subgraph cluster_working_tree {
    label="Working tree"
    subgraph cluster_file {
      label="file.txt"
        "This file is very interesting\nDifferent content
" [shape=plain]
    }
  }
}

We can give the second brach a name:

$ git checkout -b second_branch
Switched to a new branch 'second_branch'
$ cat .git/HEAD 
ref: refs/heads/second_branch
$ cat .git/refs/heads/second_branch
a118ae8cda10a8f0a966ab7b9158b4a6d3b48cfc
digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  "second_branch" [shape=plaintext]
  
  third_commit [label="commit a118ae8c...\nThis is the third commit"]
  second_commit [label="commit d3c6c635...\nThis is the second commit"]
  first_commit [label="commit 23b3ed5b1...\nThis is the first commit"]
  
  third_blob [label="blob ea5f4b8e\nThis file is very interesting\nDifferent content" shape=box]
  second_blob [label="blob 3b23ff0c\nThis file is very interesting\nMore content" shape=box]
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  third_commit -> third_blob [style=dashed]
  second_commit -> second_blob [style=dashed]
  first_commit -> first_blob [style=dashed]
  
  third_commit -> first_commit
  second_commit -> first_commit
  
  "HEAD" -> third_commit
  "master" -> second_commit
  "second_branch" -> third_commit
  
  subgraph cluster_working_tree {
    label="Working tree"
    subgraph cluster_file {
      label="file.txt"
        "This file is very interesting\nDifferent content
" [shape=plain]
    }
  }
}

Merging

We can merge the two branches together:

$ git checkout master
$ git merge --no-ff second_branch
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the
result.
$ vim file.txt

We fix some conflicts at this point

$ git add file.txt
$ git merge --continue
[master f0d7298] Merge branch 'second_branch'

The created commit has two parents:

$ git cat-file -p f0d72989
tree f63f3a4c548f5065cee598bed4ae189bd2c099d8
parent d3c6c635fb44c7084797d47050bff7961853c19b
parent a118ae8cda10a8f0a966ab7b9158b4a6d3b48cfc
author Mirko Myllykoski <mirko....> 1601288485 +0200
committer Mirko Myllykoski <mirko....> 1601288485 +0200

Merge branch 'second_branch'

Finally, the tree looks like follows:

digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  "second_branch" [shape=plaintext]
  
  fourth_commit [label="commit f0d72989...\nMerge branch 'second_branch'
"]
  third_commit [label="commit a118ae8c...\nThis is the third commit"]
  second_commit [label="commit d3c6c635...\nThis is the second commit"]
  first_commit [label="commit 23b3ed5b1...\nThis is the first commit"]
  
  fourth_blob [label="blob e51364b9\nThis file is very interesting\nMore content\nDifferent content" shape=box]
  third_blob [label="blob ea5f4b8e\nThis file is very interesting\nDifferent content" shape=box]
  second_blob [label="blob 3b23ff0c\nThis file is very interesting\nMore content" shape=box]
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  fourth_commit -> fourth_blob [style=dashed]
  third_commit -> third_blob [style=dashed]
  second_commit -> second_blob [style=dashed]
  first_commit -> first_blob [style=dashed]
  
  fourth_commit -> second_commit
  fourth_commit -> third_commit
  third_commit -> first_commit
  second_commit -> first_commit
  
  "HEAD" -> fourth_commit
  "master" -> fourth_commit
  "second_branch" -> third_commit
  
  subgraph cluster_working_tree {
    label="Working tree"
    subgraph cluster_file {
      label="file.txt"
        "This file is very interesting\nMore content\nDifferent content
" [shape=plain]
    }
  }
}

Switching to a specific commit

We can always move back to any of the previous commits:

$ git checkout 23b3ed5b1
....
HEAD is now at 23b3ed5 This is the first commit
$ cat file.txt 
This file is very interesting
digraph {
  rankdir=LR
  
  "HEAD" [shape=plaintext]
  "master" [shape=plaintext]
  "second_branch" [shape=plaintext]
  
  fourth_commit [label="commit f0d72989...\nMerge branch 'second_branch'
"]
  third_commit [label="commit a118ae8c...\nThis is the third commit"]
  second_commit [label="commit d3c6c635...\nThis is the second commit"]
  first_commit [label="commit 23b3ed5b1...\nThis is the first commit"]
  
  fourth_blob [label="blob e51364b9\nThis file is very interesting\nMore content\nDifferent content" shape=box]
  third_blob [label="blob ea5f4b8e\nThis file is very interesting\nDifferent content" shape=box]
  second_blob [label="blob 3b23ff0c\nThis file is very interesting\nMore content" shape=box]
  first_blob [label="blob 09c78e6e...\nThis file is very interesting" shape=box]
  
  fourth_commit -> fourth_blob [style=dashed]
  third_commit -> third_blob [style=dashed]
  second_commit -> second_blob [style=dashed]
  first_commit -> first_blob [style=dashed]
  
  fourth_commit -> second_commit
  fourth_commit -> third_commit
  third_commit -> first_commit
  second_commit -> first_commit
  
  "HEAD" -> first_commit
  "master" -> fourth_commit
  "second_branch" -> third_commit
  
  subgraph cluster_working_tree {
    label="Working tree"
    subgraph cluster_file {
      label="file.txt"
        "This file is very interesting" [shape=plain]
    }
  }
}

The end.

And idea: Try to play with different commands. See what happens to the .git/ directory.

Select a repo