Introduction to Git

A Distributed Version Control System

Mauro Silberberg

Motivation

Motivation

Don’t do this, let git do it

Which is the last? What changed?

How to use git

Why is it that hard?

Git commands

There are 141 git commands (as of git v2.50.1).

But we just use a small subset:

Local workflow (8-14)

git init
git add / rm
git commit
git status / log / diff
git checkout
git branch / tag
git reset / restore
git merge / rebase

Shared workflow (3-5)

git clone / remote
git fetch / pull
git push

More commands (8)

…that we won’t cover.

git bisect
git blame
git cherry-pick
git clean
git grep
git reflog
git submodule
git stash

Code vs data structures

Show me your code, and I’ll probably be mystified.

Show me your data structures, and I won’t usually need your code; it’ll be obvious.

Show me your code, and I’ll probably be mystified.

Show me your data structures, and I won’t usually need your code; it’ll be obvious.

— Fred Brooks (1975),

The Mythical Man-Month (paraphrased)

Show me your code, and I’ll probably be mystified.

Show me your data structures, and I won’t usually need your code; it’ll be obvious.

— Fred Brooks (1975),

The Mythical Man-Month (paraphrased)

— Brandon Rhodes (2025),

Keynote: Skip the Design Patterns — Architecting with Nouns and Verbs,

PyCon Lithuania (YouTube)

Let’s see what git commands do

to git’s data structures.

Index

Index

Git internals

  • the .git directory
  • Objects: blob, tree, commit

More Git internals

  • References: HEAD, heads, tags

Git commands

  • add, commit, checkout, diff

More Git commands

  • tag, branch, merge

Exercises

More Exercises

Hydration break

Let’s see the commands in action.

Git internals

Git directory

Git creates a .git directory next to the project files.

Figure 1: VS Code directory explorer. .git is hidden by default.

The .git directory contains:

path description
.git/objects/ blob, tree, commit
.git/index objects for next commit
.git/refs/ “important” commits
.git/HEAD current commit
.git/config repository configuration

among other things…

Content-addressable storage with hash functions

Hash functions

Map data of arbitrary size to fixed size.


Example: SHA256 uses 256 bits.

data size (bytes) hash (64 bytes)
hola 4 133ee9...332d44
holA 4 9b31a9...daa7a1
a0a1…a999 3890 ea624d...3f378b

Content-addressable storage

Assign filename based on the file content.


Usage: do you have…

  • a0a1a2...a999? 3890 bytes. Wasteful
  • ea624d...3f378b? 64 bytes. Efficient.


Git uses this to store objects:

blobs, trees and commits

Git object: blob (Binary Large OBject)

Create a text file:

file.txt
hola

To store it, Git computes its hash:

>>> git hash-object -w file.txt
5c1b14949828006ed75a3e8858957f86a2f7e2eb

and saves it into .git/objects/5c1b14...

We can retrieve it with:

>>> git cat-file -p 5c1b14...
hola

Note it does not store its filename.

Git object: tree (directory)

To store a directory tree, Git creates a list of metadata:

type hash         name
blob 5c1b14...    file.txt
blob 5c1b14...    file2.txt
blob e87bc7...    file3.txt
tree 3112bb...    carpeta

and saves it in: .git/objects/eddce4...

Why eddce4...? It is the hash of that text file.

>>> git cat-file -p eddce4...
blob 5c1b14...    file.txt
blob 5c1b14...    file2.txt
...

What do file.txt and file2.txt have?

Git object: commit

A commit is a checkpoint in history.

It stores:

  1. Main directory tree
  2. Previous commit(s)
  3. Author(s)
  4. Description of commit
>>> git cat-file -p ea5c17
tree eddce4...
parent f802dcc...
author Mauro <maurosilber@...> 1784298093 -0300
committer Mauro <maurosilber@...> 1784298093 -0300

Primer commit

Commit graph

The project history is a graph of commit objects

f802dc…ea5c17…811e8d…

commit ea5c17...

tree eddce4...
parent f802dc...
author Mauro <mauro@df.uba.ar> 1784298093 -0300
committer Mauro <mauro@df.uba.ar> 1784298093 -0300

Primer commit

tree eddce4...

blob 5c1b14...    file.txt
blob 5c1b14...    file2.txt
blob e87bc7...    file3.txt
tree 3112bb...    carpeta

blob 5c1b...

hola

A commit’s hash “has” all the repository’s history.

Git commands

First commit: add file

Commands:

git init
echo 'hola' > file.txt
git add file.txt
git commit -m "First commit"

Directory state:

└── .git/
    ├── index
    └── objects/
├── .git/
   ├── index
   └── objects/
   
└── file.txt
├── .git/
   ├── index
   └── objects/
       └── 5c1b14..  # blob
   
└── file.txt
├── .git/
   ├── index
   └── objects/
       ├── 5c1b14..  # blob
       └── 27b5ad..  # tree
   
└── file.txt
├── .git/
   ├── index
   └── objects/
       ├── 5c1b14..  # blob
       ├── 27b5ad..  # tree
       └── 4a53b0..  # commit
   
└── file.txt
file.txt
hola
.git/objects/5c1b14..
hola
.git/index
5c1b14.. file.txt
.git/27b5ad..
5c1b14.. file.txt
.git/4a53b0..
tree 27b5ad..
author Mauro <maurosilber@...
committer Mauro <maurosilber@...

First commit

Second commit: modify file

Commands:

echo 'chau' > file.txt
git add file.txt
git commit -m "Second commit"
file.txt
hola
file.txt
chau

Directory state:

├── .git/
   ├── index
   └── objects/
       ├── 5c1b14.. #   blob: hola
       ├── 27b5ad.. #   tree: 5c1b14.. file.txt
       └── 4a53b0.. # commit: tree 27b5ad..
   
└── file.txt
├── .git/
   ├── index
   └── objects/
       ├── 5c1b14.. #   blob: hola
       ├── 27b5ad.. #   tree: 5c1b14.. file.txt
       ├── 4a53b0.. # commit: tree 27b5ad..
       └── 4099a6.. #   blob: chau
   
└── file.txt
├── .git/
   ├── index
   └── objects/
       ├── 5c1b14.. #   blob: hola
       ├── 27b5ad.. #   tree: 5c1b14.. file.txt
       ├── 4a53b0.. # commit: tree 27b5ad..
       ├── 4099a6.. #   blob: chau
       ├── c6a5f5.. #   tree: 4099a6.. file.txt
       └── 6d43b7.. # commit: tree c6a5f5..
   
└── file.txt

Third commit: remove file

Commands:

git rm file.txt
git commit -m "Third commit"
file.txt
chau

Directory state:

├── .git/
   ├── index
   └── objects/
       ├── 5c1b14.. #   blob: hola
       ├── 27b5ad.. #   tree: 5c1b14.. file.txt
       ├── 4a53b0.. # commit: tree 27b5ad..
       ├── 4099a6.. #   blob: chau
       ├── c6a5f5.. #   tree: 4099a6.. file.txt
       └── 6d43b7.. # commit: tree c6a5f5..
   
└── file.txt
└── .git/
    ├── index
    └── objects/
        ├── 5c1b14.. #   blob: hola
        ├── 27b5ad.. #   tree: 5c1b14.. file.txt
        ├── 4a53b0.. # commit: tree 27b5ad..
        ├── 4099a6.. #   blob: chau
        ├── c6a5f5.. #   tree: 4099a6.. file.txt
        └── 6d43b7.. # commit: tree c6a5f5.. 
└── .git/
    ├── index
    └── objects/
        ├── 5c1b14.. #   blob: hola
        ├── 27b5ad.. #   tree: 5c1b14.. file.txt
        ├── 4a53b0.. # commit: tree 27b5ad..
        ├── 4099a6.. #   blob: chau
        ├── c6a5f5.. #   tree: 4099a6.. file.txt
        ├── 6d43b7.. # commit: tree c6a5f5.. 
        ├── 825dc6.. #   tree: 
        └── 954305.. # commit: tree 825dc6.. 

Question: If I uploaded sensitive information, ¿did I remove it correctly?

Checkout commit: navigate history

Commands:

git checkout 4a53b0..
git checkout 6d43b7..
file.txt
hola
file.txt
chau

Directory state:

└── .git/
    ├── index
    └── objects/
        ├── 5c1b14.. #   blob: hola
        ├── 27b5ad.. #   tree: 5c1b14.. file.txt
        ├── 4a53b0.. # commit: tree 27b5ad..
        ├── 4099a6.. #   blob: chau
        ├── c6a5f5.. #   tree: 4099a6.. file.txt
        ├── 6d43b7.. # commit: tree c6a5f5..
        ├── 825dc6.. #   tree: 
        └── 954305.. # commit: tree 825dc6.. 
├── .git/
   ├── index
   └── objects/
       ├── 5c1b14.. #   blob: hola
       ├── 27b5ad.. #   tree: 5c1b14.. file.txt
       ├── 4a53b0.. # commit: tree 27b5ad..
       ├── 4099a6.. #   blob: chau
       ├── c6a5f5.. #   tree: 4099a6.. file.txt
       ├── 6d43b7.. # commit: tree c6a5f5..
       ├── 825dc6.. #   tree: 
       └── 954305.. # commit: tree 825dc6.. 
   
└── file.txt

Difference between commits

Command:

> git diff 4a53b0.. 6d43b7..
diff --git a/file.txt b/file.txt
index 5c1b149..4099a6a 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-hola
+chau

Directory state:

├── .git/
   ├── index
   └── objects/
       ├── 5c1b14.. #   blob: hola
       ├── 27b5ad.. #   tree: 5c1b14.. file.txt
       ├── 4a53b0.. # commit: tree 27b5ad..
       ├── 4099a6.. #   blob: chau
       ├── c6a5f5.. #   tree: 4099a6.. file.txt
       ├── 6d43b7.. # commit: tree c6a5f5..
       ├── 825dc6.. #   tree: 
       └── 954305.. # commit: tree 825dc6.. 
   
└── file.txt

Hydration break

Let’s do some exercises. # Git internals: named commits

Git directory (again)

Already covered some of the internals:

seen path description
.git/objects/ blob, tree, commit
.git/index objects for next commit
.git/HEAD current commit
.git/refs/ “important” commits
.git/config repository configuration

Next:

ref description
HEAD points to the current commit
tag fixed alias for a commit
branch moving alias for a commit

Git HEAD

In .git/HEAD, git stores the current checked-out commit.


Commit graph

c86…a2d…b83…

c86…a2d…b83…HEAD

c86…a2d…HEADb83…

Files

.git/HEAD
b83..
.git/HEAD
a2d..

Commands

git checkout a2d...

Git tags

In .git/refs/tags/, git stores “fixed” aliases for commits.


Commit graph

c86…a2d…b83…HEAD

c86…a2d…v1b83…HEAD

c86…a2d…HEADv1b83…

Files

.git/HEAD
b83..
.git/HEAD
b83..
.git/refs/tags/v1
a2d..
.git/HEAD
a2d..
.git/refs/tags/v1
a2d..

Commands

git tag v1 a2d...
git checkout v1

Usually used for “annotating” a release.

Git branches

In .git/refs/heads/, git stores “moving” aliases for commits.


Commit graph

a2d…b83…HEAD

a2d…b83…HEADdev

a2d…b83…c22…HEADdev

Files

.git/HEAD
b83..
.git/HEAD
b83..
.git/refs/heads/dev
b83..
.git/HEAD
ref: refs/heads/dev
.git/refs/heads/dev
b83..
.git/HEAD
ref: refs/heads/dev
.git/refs/heads/dev
c22..

Commands

git branch dev
git commit

Usually used for developing changes.

Summary: HEAD, heads and tags

In .git/HEAD, git stores the current commit.

  • ref: refs/heads/<NAME> for a branch,
  • <HASH> when in detached HEAD state.

In .git/refs/, git stores “named” commits:

  • .git/refs/heads/<NAME> for branches,
  • .git/refs/tags/<NAME> for tags.

When HEAD is a reference to a branch,

doing commit advances that branch.


Important:

objects not referenced by

✨ named commits ✨

could be deleted.

Git garbage collect

Important: objects not referenced by ✨ named commits ✨ could be deleted.

A1…A2…B1…B2…devC1…C2…A3…mainHEAD

Which commits could be deleted by git gc?

Git branches

Using branches

Idea:

  1. Leave the main in a working state.
  2. Start developing changes in a new branch.
  3. Switch back to main if we need to run (a working version of) the code.


Commit graph

b83..mainHEAD

b83..maindevHEAD

b83..dev9f1..mainHEAD

b83..devHEAD9f1..main

b83..9f1..main6bb..devHEAD

Files

.git/HEAD
ref: refs/heads/main
.git/refs/heads/main
b83..
.git/HEAD
ref: refs/heads/main
.git/refs/heads/main
b83..
.git/refs/heads/dev
b83..
.git/HEAD
ref: refs/heads/main
.git/refs/heads/main
9f1..
.git/refs/heads/dev
b83..
.git/HEAD
ref: refs/heads/dev
.git/refs/heads/main
9f1..
.git/refs/heads/dev
b83..
.git/HEAD
ref: refs/heads/dev
.git/refs/heads/main
9f1..
.git/refs/heads/dev
6bb..

Commands

git branch dev
git commit
git checkout dev
git commit

Merging branches

The changes are ready. Let’s add them to main.


Commit graph

b83..9f1..mainHEAD6bb..dev

b83..9f1..6bb..dev0e4..mainHEAD

Note: main also had a commit.

Files

.git/HEAD
ref: refs/heads/main
.git/refs/heads/main
9f1..
.git/refs/heads/dev
6bb..
.git/HEAD
ref: refs/heads/main
.git/refs/heads/main
0e4..
.git/refs/heads/dev
6bb..
.git/objects/0e4..
tree c6a..
parent 9f1..
parent 6bb..
author Mauro <maurosilber@..
committer Mauro <maurosilber@..

<COMMIT MESSAGE>

Commands

git merge dev

The merge commit has two parents.

Merge fast-forward (FF)

Skiping the merge commit.


Before

b83..mainHEAD6bb..dev

No FF

b83..6bb..dev0e4..mainHEAD

FF (default)

b83..6bb..devmainHEAD

The dev branch contains all commits from main.

git merge dev --no-ff

With merge commit.

git merge dev --ff

No merge commit.

A simpler commit history.

Merge conflict

When both branches modified the same lines.

Git asks us to fix conflicts and then commit the result.

maindevb83..9f1..HEAD6bb..

file.txt @ b83..
hola
file.txt @ 9f1..
Hola
file.txt @ 6bb..
hola Mauro
file.txt @ merge
<<<<<<< HEAD
Hola
=======
hola Mauro
>>>>>>> dev
file.txt @ merge
Hola Mauro

Hydration break

Let’s do more exercises.

Git remotes and GitHub