# Inside Git: How It Works and the Role of the .git Folder

In today’s blog, we basically try to understand how git is actually working under the hood. Before starting this blog, if you’re unfamiliar with Git, please follow these two introductory blogs first, then return to read this blog.

[Blog 1: Why We Need A Version Control System](https://blog.studyhex.in/as-a-programmer-why-do-we-need-a-version-control-system)

[Blog 2: Basic Command of Git](https://blog.studyhex.in/git-for-beginners-basics-and-essential-commands)

## **So let’s start exploring how Git works internally.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768295474157/cc5b4f4d-d16e-4644-b805-16283f9c1682.png align="left")

**As you can see, we initialized an empty Git repository, which created the** `.git` folder—now let’s look inside and see what it actually contains.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768295567187/e10b1103-33ea-43ca-a478-0865b547ee69.png align="left")

### Let’s look at the .git folder

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768295608777/0cde361e-e4f3-4a99-b5fc-a5cfb24731cf.png align="left")

As you can see, it contains a few folders like `hooks`, `info`, `objects`, and `refs`, along with some files such as `config`, `description`, and `HEAD`. Now we’ll go through each of them one by one and try to understand how they work. Let’s start by opening the `HEAD` file.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768295705914/ec21af57-10a2-430a-8424-c10524dcf135.png align="left")

As you can see, it contains a ref (reference) that points to a folder path: `refs/heads/master`. Let’s take a look at that now. Inside the `refs` folder, we can see two folders: `heads` and `tags`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768295781783/9832ea59-d8f9-4f18-9a8d-ff2021ac6a7b.png align="left")

For now, both folders are empty. If we look at the `description` file, it contains the repository name. To change the repository name, we need to edit this file.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768295875480/1da1de0b-4db0-45c0-bd17-7c3fa1b085f2.png align="center")

Next, we have the `info` folder, which contains a file called `exclude`. For now, it’s empty, but this file is used to tell Git to ignore certain files—we’ll definitely look at it later. After that, there’s the `hooks` folder, which contains some sample scripts that we can use if we need any special use cases.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768296805359/60b47594-0b01-48f0-8042-e18772a58513.png align="left")

As you can see, there’s a `pre-commit.sample` script where you can write your own custom logic. If you remove the `.sample` extension, that logic will run automatically every time you commit your codebase.

Now, to understand how this works, let’s create a file called `text.txt`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768296958653/aaea29d4-651f-4f82-8bf9-78225a7aeeaa.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768296997726/fc45d9d1-5103-4a6d-a5f2-d45b7668e0fb.png align="left")

We run `git add .` and `git commit`, which adds all files to the staging area and creates a commit. After doing this, we can immediately see changes inside the `.git` folder. First, a `index` file is created, and inside the `objects` folder, a new directory `55` appears.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768297054423/1c09a2ef-ae4b-4969-8938-8c65c686e678.png align="left")

## **Now, to understand these changes, we first need to understand what Git** `objects` The folder actually is.

Git has three objects

1. Blob
    
2. Tree
    
3. Commit
    

Let’s understand the blob first.

Git uses a special technique to store file changes. It uses a SHA-1 hash to identify a file—almost like a tag—and the actual file content is stored as a blob. In simple terms, a blob represents the actual content of a file.

A **blob stores raw file content**

* No filename
    
* No permissions
    
* No path
    
* It stores the just byte
    

If `text.txt` contains:

```bash
this is a plain text file. 
Give it a number 1
```

### How blob content is stored

Internally Git stores:

```bash
blob <size>\0<content>
```

Then a SHA-1 hash is calculated from that content. If the content is exactly the same, it generates the same SHA-1 hash because it’s a mathematical function. But even a single space or a new line will change the content and produce a completely different SHA-1 hash.

**In Git, to see the staged blobs, we can run this command:**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298012908/59833619-cc9c-4f63-98da-76c1c7d962f8.png align="center")

It returns a hash—in our case, `559b906315547fcdf98e1b77dbe23f9969abb935`. If you look closely, the first two characters are `55`, which matches the folder name created inside the `objects` directory during the `git add .` command. The remaining characters are used as the filename, and that file stores the actual content.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298099472/197f9557-9f8c-4feb-b79a-d28695b2763c.png align="left")

**To see the hash file content, we can use this command:**

```bash
git cat-file -p <sha1-hash> 
# here -p stands for pretty-print ( to see the binary content we use this)
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298141680/37855c41-a419-49f4-b9f4-86bcd9372aa2.png align="center")

**To extend our discussion, let’s create a copy of the text file—both files have the same content.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298325643/ec8e2f0e-c6a0-4624-ba68-94c0b9b94ee9.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298365763/37e6f312-f895-4f93-a282-1f37c179362a.png align="center")

**And then, let’s stage the new file as well.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298392260/1138c9a4-027a-4f95-9159-2a2115453064.png align="left")

**And in the** `objects` folder, no new folder is created.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298416190/546bdda5-d6df-4543-bbe6-848ea5a621cf.png align="left")

Git actually saves storage this way. When the content is the same, Git does not create any duplicate data—it reuses the same SHA-1 hash until the file content changes. As you can see in the output, we have two files in the staging area, but both of them point to the same hash.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298520408/cb52cb90-18e7-4a2b-a3ef-e80a9686cdab.png align="center")

To see the type of object hash, we can use this command

```bash
git cat-file -t <sha-1 hash>
# -t stands for type
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768298649978/af7b8d18-b96e-4c3a-8176-eee076fc1d98.png align="left")

Now you may have what is inside the index file we have here

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768299125284/809f3ed6-fb93-4968-b83c-dd10d41e8777.png align="left")

To see the index file conent we already use that command

```bash
git ls-files --stage
# output 
# 100644 559b906315547fcdf98e1b77dbe23f9969abb935 0       text-copy.txt
# 100644 559b906315547fcdf98e1b77dbe23f9969abb935 0       text.txt
```

## Column 1 → `100644` (THIS IS IMPORTANT)

### What it means in plain English

`100644` = \*\*file type + permissions (\*\*Git borrows this from Unix file system rules.)

```bash
100 | 644
```

#### File type

| Code | Meaning |
| --- | --- |
| `100` | Regular file |
| `040` | Directory |
| `120` | Symbolic link |
| `160` | Git submodule |

#### File permissions

This part decides **who can read/write/execute the file**.

`644` = `rw-r--r--`

| Who | Permission |
| --- | --- |
| Owner | Read + Write |
| Group | Read only |
| Others | Read only |

Let’s try to figure out how this tells the permission for each file  
Computers think in **bits**

A bit can be:

```bash
0 = off
1 = on
```

Permissions need **3 independent switches**:

* Read
    
* Write
    
* Execute
    

Assume Switch Position is

```bash
Read    Write   Execute
  2       1        0

# Bit position:   2²   2¹   2⁰
# Decimal value:   4    2    1
```

| Permission | Bit | Value |
| --- | --- | --- |
| Read | 2² | **4** |
| Write | 2¹ | **2** |
| Execute | 2⁰ | **1** |

Because you can **add them** and always know what’s enabled.

| Permissions | Binary | Decimal |
| --- | --- | --- |
| r-- | 100 | 4 |
| \-w- | 010 | 2 |
| \--x | 001 | 1 |
| rw- | 110 | 6 |
| r-x | 101 | 5 |
| rwx | 111 | 7 |

What if values were random? (why NOT 3, 5, 6?)

```bash
read = 3
write = 5
execute = 6
# 3 + 5 = 8 ❌ what is 8? Not Understandable
```

Binary weights guarantee: Every combination is unique and reversible.

Now, let’s understand the Second and Third objects of Git, which are the tree and the commit together

Tree:

A **tree** is Git’s snapshot of a **folder**.

* Not a file.
    
* Not a commit.
    
* A **directory structure at a specific moment**.
    

Make a commit of my Two Created File Text and text copy both

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467120790/97a22636-09a8-4593-98b3-1a6b8fd248ae.png align="center")

Note: After Running This Command It creates a new File Which is COMMIT\_EDITMSG

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467207297/84bef56e-ff1a-49e6-a4f7-e463079fab5a.png align="left")

This file stores the actual commit mesage temporary to save it further we provide during the commit command

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467271116/7f22da6b-8162-433b-835f-0a2a69277ba4.png align="left")

Now, to understand the Tree and Commit Let’s run This Command

```bash
git log
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467476870/52ac01dc-a4c5-46d8-9071-09524da30340.png align="left")

**As you can see, it also gives a SHA-1 hash. Let’s check the content of that first.**

```bash
git cat-file -p <sha-1 hash>
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467521971/37a6f637-55ef-4b0e-8349-adb72aad0a49.png align="left")

As you can see, it shows a tree with the hash `ad031163ef52fb6ab5e1e`, along with the author name, committer name, and the commit message.

**Now, let’s copy the tree hash and try to view its content using this command.**

```bash
git cat-file -p <sha-1 hash>
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467670010/4f449cb7-bc17-42ea-afb2-9ee581772c7c.png align="left")

Let’s print the type of it using this command

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467739881/21ea98c6-e92b-4c50-97ce-0cd9c270cb90.png align="left")

**So we can think of a tree as the directory structure for our code files. Here, we can see that it contains two blobs. To move our understanding forward, let’s create another folder with some content.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467874050/09507961-79f6-4a67-be1b-e0d194ca6851.png align="left")

**And commit it with the message “z feature added.”**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768467917430/dfbc8962-e636-4d9f-9e15-c01b17a5ebe2.png align="left")

**As a result, we can see a few changes.**

**The first change we notice is in the** `COMMIT_EDITMSG` file.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768468044709/38f111ac-e22d-471a-9019-85164bb4c8f1.png align="left")

**As you can see, we now have two commits.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768468145974/5c1c7f3b-205a-46b2-b931-3011c8a72ed6.png align="left")

**Now, everything should be clear.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768468195812/66d1083f-c77f-4e04-9a9f-5f9719f7ea2c.png align="left")

**The object type of this hash is**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768468228251/2cefcd1e-46bc-4047-a28b-ce2344b0b107.png align="left")

Let’s understand the output here,

```bash
$ git cat-file -p 379a752
tree 2b7a20fe16cb153fc71a4ca8598ec1208fafecc2 # this is the current tree sha 1 hash 
                                              # were current commit is poiting
parent cc2a85709f2cf0a75b8fe6e8fa075c3b2b8a1550 # This is the Parrent tree sha 1 hash 
author Pallab Karmakar <ookarmakarpallab1212@gmail.com> 1768467910 +0530 # Author Metadata
committer Pallab Karmakar <ookarmakarpallab1212@gmail.com> 1768467910 +0530

adding z feature # Actual Commit Message
```

## **The complete workflow actually looks like this.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768469833518/f6f07745-3d2c-4832-8acd-c34d26fe0cc3.png align="center")

## Let’s try to break down the Staging and Commit Internals working mechanisms

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768471795589/6197c9aa-f3c1-4c14-b1ec-711cd4c6f5e5.png align="center")

## What happens when you run `git add .`

* Git **does NOT store files by name or folder**
    
* It only cares about **file content**
    

### Step-by-step:

* Git reads the content of each modified/new file
    
* For **each file**, Git:
    
    * Takes the raw file content
        
    * Prepends a header like:
        
        ```bash
        blob <size>\0
        ```
        
    * Calculates a **SHA-1 hash** of this data
        
* This hash uniquely identifies the file content
    

### Object storage:

* Git stores the content as a **blob object**
    
* Blob is saved inside:
    
    ```bash
    .git/objects/xx/yyyyyyyyyyyy...
    ```
    
    where:
    
    * `xx` = first **2 characters** of the SHA-1
        
    * `yyyy...` = remaining **38 characters**
        
* If two files have **identical content**, Git:
    
    * Creates **only one blob**
        
    * Both files point to the same SHA-1
        

### Staging area (index):

* Git updates the **index file**
    
* Index maps:
    
    ```bash
    filename → blob SHA-1
    ```
    
* At this point:
    
    * Files are **staged**
        
    * No commit exists yet
        

## What happens when you run `git commit`

### Tree creation:

* Git reads the **index**
    
* It creates a **tree object**
    
* The tree:
    
    * Represents a **directory snapshot**
        
    * Maps:
        
        ```bash
        filename → blob SHA-1
        ```
        
* Each folder becomes its own tree (nested trees if needed)
    

### Commit object creation:

* Git creates a **commit object**
    
* The commit contains:
    
    * Tree SHA-1 (root directory snapshot)
        
    * Parent commit SHA-1 (if any)
        
    * Author name & email
        
    * Committer info
        
    * Commit message
        

### Relationships:

```bash
Commit → Tree → Blobs
```

* Commit **points to the tree**
    
* Tree **points to blobs**
    
* Blobs store **actual file content**
    

### After the commit is done

* A new file appears:
    
    ```bash
    .git/COMMIT_EDITMSG
    ```
    
    (temporary, for commit message)
    
* `HEAD` now points to:
    
    ```bash
    refs/heads/master → commit SHA-1
    ```
    
* Git history is updated
    
* Working directory is now **clean**
    

## Conclusion:

* Git stores everything as **objects**, not as plain files.
    
* **Blobs** store the actual content of files.
    
* **Trees** represent directory structures and map filenames to blobs or other trees.
    
* **Commits** store metadata and point to a single root tree.
    
* Git uses **SHA-1 hashes** to uniquely identify every object.
    
* If two files have the same content, Git stores **only one blob** and reuses the same hash.
    
* Any small change in content creates a **new SHA-1 hash**.
    
* The **index (staging area)** links files to blobs before committing.
    
* The **objects folder** stores all blobs, trees, and commits in a compressed form.
    
* The **HEAD** reference points to the current commit.
    
* Each new commit creates a new tree if the directory structure changes.
    
* This design makes Git **fast, storage-efficient, and reliable**.
