git commit

TL;DR: git commit takes a snapshot of your Staging Area (index) and writes it permanently into Git’s object database. Behind the scenes, Git packages your directory structure into tree objects, writes a commit object listing metadata (author, committer, parent commit hashes, message), and updates the active branch ref to point to the new commit’s hash.


What Does git commit Actually Do?

To understand git commit, you must unlearn the common misconception that Git tracks changes as “diffs” or “deltas.” In Git, every commit is a complete snapshot of the entire project.

When you commit, you are not saving a list of line edits. Instead, you are taking a photo of the staging area and assigning it a unique, permanent identifier. If a file has not changed in a commit, Git does not duplicate it; it simply points to the existing blob object from a previous commit. This is known as structural sharing.

graph TD
    subgraph "A Git Commit Object"
        C[Commit Object<br>Hash: 8a4c10...] -->|Points to| T[Root Tree Object<br>Hash: 4a2b99...]
        C -->|Points to| P[Parent Commit Hash<br>Hash: 55a420...]
        C -.->|Metadata| M[Author, Committer, Timestamp, Message]
    end

The Structure of a Commit Object

A commit is represented internally as a simple plaintext file containing structured metadata. When Git hashes this plaintext using SHA-1, it generates the commit’s 40-character hex identifier.

A raw commit object contains the following fields:

  1. tree: A pointer (hash) to the top-level (root) tree object representing the repository’s root directory.
  2. parent: The hash of the previous commit (or commits).
    • An initial commit has zero parents.
    • A regular commit has one parent.
    • A merge commit has two or more parents.
  3. author: The name, email, and Unix timestamp of the person who wrote the code.
  4. committer: The name, email, and Unix timestamp of the person who committed the code (e.g., in cases where a patch was applied by a maintainer).
  5. gpgsig: (Optional) Cryptographic signature if the commit was signed.
  6. message: The text description explaining why the changes were made.

What Happens Inside .git/ (Hands-on exploration)

Let’s look under the hood of a repository to trace how Git constructs these tree and commit objects step-by-step.

Step 1: Stage a File and Check the Index

First, let’s make sure we have a repository initialized and a file staged using git add.

$ mkdir git-commit-demo && cd git-commit-demo
$ git init
Initialized empty Git repository in /git-commit-demo/.git/

$ echo "console.log('hello world');" > app.js
$ git add app.js

At this point, the file is compressed into a blob object and recorded in the .git/index file. Let’s verify:

$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0	app.js

Step 2: Create a Commit

Now, let’s write our first commit.

$ git commit -m "feat: introduce logging script"
[main (root-commit) d4a123f] feat: introduce logging script
 1 file changed, 1 insertion(+)
 create mode 100644 app.js

Let’s check the branch references inside .git/refs/heads/. The active branch file main should now contain the commit’s full 40-character hash:

$ cat .git/refs/heads/main
d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9

And .git/HEAD tells Git which branch is active:

$ cat .git/HEAD
ref: refs/heads/main

Step 3: Deconstructing the Commit Object

Let’s inspect the created commit object using the plumbing command git cat-file:

# Print type of the object
$ git cat-file -t d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9
commit

# Print raw content of the commit object
$ git cat-file -p d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9
tree b8a27d0ef093ab7e20cf7b68a417534431e21b7b
author Purna Pattela <purna@example.com> 1772960000 +0000
committer Purna Pattela <purna@example.com> 1772960000 +0000

feat: introduce logging script

Notice that the commit object contains no file content and no diff. It points to a tree object with hash b8a27d0.... Let’s inspect that tree:

$ git cat-file -p b8a27d0ef093ab7e20cf7b68a417534431e21b7b
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	app.js

The tree object acts like a directory listing. It maps the filename app.js to the file permissions (100644) and the zlib-compressed content blob (e69de29...).

Step 4: Adding Subdirectories and Verifying Parents

Let’s add a subdirectory to see how Git handles nested folders.

$ mkdir src
$ echo "const port = 3000;" > src/config.js
$ git add src/config.js
$ git commit -m "chore: add source config"
[main 7f2b58c] chore: add source config
 1 file changed, 1 insertion(+)
 create mode 100644 src/config.js

Let’s read this second commit:

$ git cat-file -p 7f2b58c
tree a832dbf521b4a56c071d2b512c98a3e74211a512
parent d4a123f4c7b8d80c0ef14815a5bb1d6a362bf2c9
author Purna Pattela <purna@example.com> 1772960100 +0000
committer Purna Pattela <purna@example.com> 1772960100 +0000

chore: add source config

Notice the new line: parent d4a123f.... This points back to the first commit, linking them together in a history chain.

Let’s look at the tree of this new commit:

$ git cat-file -p a832dbf521b4a56c071d2b512c98a3e74211a512
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	app.js
040000 tree f3b3a3c9e6db5832a875a6c11d2b270a2936274b	src

Instead of listing a flat file list, the root tree object points to another tree object (representing the src/ directory, indicated by mode 040000). If we peek inside the nested src tree:

$ git cat-file -p f3b3a3c9e6db5832a875a6c11d2b270a2936274b
100644 blob c8384df24a5611dfd9a1db4b86270a1a8a25c156	config.js

This tree-within-a-tree architecture mirrors your filesystem perfectly.


Detailed Command Options & Advanced Usage

1. Amending the Last Commit (git commit --amend)

If you make a typo in your commit message, or forget to stage a change before committing, you can amend the last commit:

$ git add forgotten_file.txt
$ git commit --amend -m "chore: add source config and settings"

Under the Hood: Because commits in Git are immutable (their SHA-1 hash is computed from their contents), Git cannot edit the existing commit. Instead, --amend creates a brand-new commit object pointing to a new tree and a modified message, setting its parent to the parent of the old commit. Git then updates the branch pointer to the new commit, leaving the old commit orphaned.

graph TD
    subgraph "How Amend Works"
        C1[Commit 1] --> C2[Commit 2: Original]
        C1 --> C3[Commit 3: Amended]
        Branch[Branch Ref: main] -->|Moved from C2 to C3| C3
    end
    style C2 fill:#faa,stroke:#333
    style C3 fill:#afa,stroke:#333

2. Skipping the Staging Area (git commit -a)

You can skip staging by using the -a (or --all) flag. This tells Git to automatically stage all modified and deleted files, then immediately write a commit:

$ git commit -am "refactor: simplify logger interface"

[!WARNING] This command will not stage new untracked files. If you create a brand-new file, you must run git add before running git commit -a.

3. Signing Commits with GPG or SSH (git commit -S)

In production environments, it is critical to verify that commits actually come from the author listed. You can cryptographically sign your commits:

$ git commit -S -m "feat: release critical payment gateway integration"

This signs the commit using a local private key. GitHub or GitLab can then display a “Verified” badge next to the commit by matching the signature against your public key.


Real-World Scenario: Writing Atomic Commits

A healthy codebase relies on atomic commits. An atomic commit is a commit that does one, and only one, logical thing. It should contain all the changes necessary to implement a single feature or resolve a single bug, and nothing else.

For example, if you are building an authentication system, do not write a commit message like implemented login and fixed layout issues on homepage. If the homepage layout fix breaks something else, reverting that commit will also delete the login system!

Instead, break the work down using git add and separate commits:

# First, stage only the layout changes
$ git add src/styles.css
$ git commit -m "fix(styles): repair home page navigation alignment"

# Next, stage the login files
$ git add src/auth/
$ git commit -m "feat(auth): implement user authentication mechanism"

This clean structure makes auditing history using git log painless and allows developers to safely isolate problems.


Common Pitfalls and Mistakes

1. Amending Pushed Commits

Never run git commit --amend on a commit that has already been pushed to a remote repository (like GitHub) shared with other developers.

  • Why: Amending changes the commit hash. If you force-push this amended commit, it will overwrite the history for everyone else, leading to painful merge conflicts when they pull.
  • Rule of Thumb: Only amend commits that exist solely on your local computer.

2. The Empty Commit Error

If you try to run git commit when there are no changes staged, Git will reject the command:

$ git commit -m "nothing changes"
On branch main
nothing to commit, working tree clean
  • Fix: If you explicitly need to create a commit without any file modifications (for example, to trigger a CI/CD build runner), use the override flag:
$ git commit --allow-empty -m "trigger: run test suite in pipeline"

3. Author vs. Committer Confusion

Sometimes in project histories, you will notice a commit lists one person as the Author and another as the Committer.

  • Author: The person who wrote the actual code changes.
  • Committer: The person who applied the commit (e.g., using git rebase or applying a patch).
  • Normally, these are the same person. However, if you rebase or squash someone else’s pull request on GitHub, you become the Committer, while they remain the Author.

Command Quick Reference

CommandAction
git commitOpens your terminal editor to write a multi-line commit message.
git commit -m "msg"Commits staged changes with a short inline message.
git commit -a -m "msg"Stages modified/deleted files and commits them in one step.
git commit --amendAmends the last commit (adds new staged files and/or updates message).
git commit --amend --no-editAmends the last commit with new files but keeps the exact same message.
git commit --allow-empty -m "msg"Forces a commit even if no changes are staged or modified.
git commit -S -m "msg"Cryptographically signs the commit.

FAQ (People Also Ask)

What is the difference between git commit and git push?

git commit is a local command. It records a snapshot in your local directory database (.git/objects/). It does not contact any external servers. git push is a network command that uploads these local commits, trees, and blobs to a remote repository (such as GitHub, GitLab, or a self-hosted server) so they can be shared with others.

How does Git compute the 40-character commit hash?

Git hashes the header and content of the commit object using SHA-1. The content consists of the tree hash, parent hash(es), author info, committer info, and the commit message. Because the hash calculation includes the current timestamp, two commits created with the exact same files and message will still have different hashes.

Can I revert or delete my last commit?

Yes. If you committed changes but want to undo the commit while preserving your changes in the working directory (unstaging them), run:

git reset --soft HEAD~1

If you want to completely erase the last commit and discard all modifications, run:

git reset --hard HEAD~1

(Warning: git reset --hard cannot be undone easily; verify your work before running it!)

What is a detached HEAD state?

If you checkout a specific commit hash (e.g., git checkout a1b2c3d - link git-checkout) rather than a branch, your HEAD pointer points directly to a commit object rather than a branch reference. If you commit in this state, the new commit will have no branch pointing to it. If you switch branches, your new commit will become orphaned and eventually deleted by Git’s garbage collection. Always ensure you are on a branch (link git-branch) before committing.


Test Your Knowledge

Loading quiz…