From Stage to Snapshot: Unpacking Git's Index, Blob, & Commit Operations
Tags: git, softwareengineering, programming, learning
Published: 2024-05-07 18:51:08

Welcome to a comprehensive exploration of Git's internal operations, specifically focusing on the activities between `git add` and `git commit`. This post is tailored for individuals eager to grasp the nuances of the `.git` directory, the processes involved in staging and committing changes, and the critical roles played by various Git operations.

## Revisiting the Fundamentals

The command `git add README.md` triggers a pivotal process in Git's management of your project. This action encompasses two primary operations:

1. **Updating the Index File**
2. **Creating a Blob Object**

Here's what this process entails:

```sh
$ git add .
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
  new file:   README.md
```

At this juncture, the newly added file `README.md` is registered in the `.git/index` file. Essentially, `git add` writes to this index file, thereby staging the file for the upcoming commit.

## Delving into the `.git` Directory and Git Objects

The `.git` directory houses several crucial objects essential for Git’s operations:

- **Blob Object**: Holds the content of the files.
- **Tree Object**: Manages a directory tree of the project, linking to blobs and other trees.
- **Commit Object**: Contains metadata about the commit, including author, message, and pointers to parent commits and the tree object of that commit.
- **Tag Object**: Used to mark specific commits with tags, especially annotated tags.

These objects are stored within `.git/objects/` and are vital for Git's version control functionalities.

## The Mechanics of `git add`

Adding a file to Git involves recording its details into the `.git/index` file. To understand what happens under the hood, consider the following commands:

```sh
$ cat .git/index
# Output: Garbled content representing the index file’s binary data

DIRCf:��f:����`
               ��5�57�t���H��.A��_v
                                   ��K  README.mdTREE-1 0
�B1�l�s�
```

```sh
$ git ls-files -s
100644 cb74891f9de548b5d52e41e2e15f760cb9e9904b 0       README.md
```

The `git ls-files -s` command lists the staged versions of files, showing file permissions, the blob hash, and the file name.

## Blob and Tree Objects: The Core of Git

Each file in Git is stored as a blob object, identified by a unique SHA-1 hash. When you stage a file using `git add`, a blob object is created, and its hash is recorded in the index. This blob hash is pivotal as it connects the indexed file to its content stored in the object database.

```
$ git cat-file -p 8178c76d627cade75005b40711b92f4177bc6cfc
# Output:

Git Internals and Objects
$ git cat-file -t 8178c76d627cade75005b40711b92f4177bc6cfc
blob
```

## The Role of `git commit`

Committing in Git involves capturing a snapshot of the project's current state. This process includes:

1. **Creating a Tree Object**: This aggregates all current blobs and trees.
2. **Creating a Commit Object**: This encapsulates the metadata about this snapshot, including a reference to the tree object and parent commits.

```sh
$ git commit -m "Add README.md"
```

This command updates the HEAD to reflect the new commit, encapsulating all current project changes.

### Security Through Hash Values

Git enhances security by embedding hash values within commit objects. Each commit includes the hash of its parent commit, necessitating the alteration of all subsequent hashes for any change in history, a computationally intensive task that secures your history against tampering.

## Understanding Git Tags and Tag Objects

In Git, tagging is a method used to mark specific points in a repository's history as significant, often used for releases. However, not all tags are created equal; Git distinguishes between lightweight tags and annotated tags, each serving different purposes.

### Refs (References)

Refs are essentially pointers to commit objects, storing only the hash value of the commit they point to. This makes switching between different commits and branches swift and resource-efficient. The primary types of refs are:

- **Branch**: Points to the tip of a branch in your repository.
- **HEAD**: Points to the current branch or commit you're working on.
- **Tag**: Points to a specific commit, useful for marking release points like v1.0 or v2.0.

The structure of the `.git/refs` directory is organized as follows:

```
.git/refs
├── heads
│   ├── branch1
│   └── main
└── tags
    └── v1.0
```

### Tag Objects

A tag object in Git is more than just a reference to a commit. It is created when an annotated tag is used, and unlike a lightweight tag, it is a full-fledged object in the Git database. Tag objects include metadata such as the tagger's name, the date the tag was created, and a message describing the tag.

### REVS: A Quick Primer

Before diving deeper into tags, it's crucial to understand `REVS`, a term that refers to revisions in Git. In Git, revisions are pointers to specific states in the repository's history, which can be commits, heads, tags, and more. Understanding revisions is fundamental to navigating and manipulating a repository's history effectively.

### Types of Tags

Git supports two main types of tags:

- **Lightweight Tags**: These are essentially bookmarks to a specific commit. A lightweight tag is a simple pointer to a commit; it does not contain any additional information or metadata. It is useful for private or temporary markers that do not need to be shared.

- **Annotated Tags**: These are stored as full objects in the Git database, which includes the tagger's information, a date, and a message. Annotated tags are intended for public use, such as marking release versions where additional information about the release is beneficial.

### Creating and Examining a Tag Object

When you create an annotated tag, Git generates a tag object. This can be demonstrated with the following commands:

```sh
$ git tag annotated_tag -m "Tag with annotation"

$ cat .git/refs/tags/annotated_tag
8acd58421b7e499c34badb097083986e3c5c33a1
```

To examine the details of this tag object:

```sh
$ git cat-file -t 8acd58421b7e499c34badb097083986e3c5c33a1
tag
```

```sh
$ git cat-file -p 8acd58421b7e499c34badb097083986e3c5c33a1
object 2fc011659b49d7eec0d6c6ce3cf208ebb4bff3f6
type commit
tag annotated_tag
tagger Siddhant Khare <Siddhantxxxxxxx@gmail.com> 1715105861 +0000

Tag with annotation
```

This output shows that the tag object contains detailed metadata, linking it directly to a commit but also providing additional contextual information.

### Visual Representation

To clarify, here's how the two types of tags are represented in Git:

- **Simple Tag**:
  - Commit Object → Tree → Blob

- **Annotated Tag**:
  - Tag Object → Commit Object → Tree → Blob

By understanding the difference between lightweight and annotated tags and how they are used in Git, developers can better manage their project milestones and releases, choosing the right type of tag based on the context and needs of their project.


## Conclusion

This guide has taken you on a detailed tour from `git add` to `git commit`, illustrating the internal mechanisms of Git that handle these commands. By understanding these processes, you gain deeper insights into Git's efficient, secure management of your code repository, empowering you to use Git more effectively in your projects.


## References

- [GitHub blog: Commits are snapshots, not diffs](https://github.blog/2020-12-17-commits-are-snapshots-not-diffs/)
- [10.2 Git Internals - Git Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)