git commit

The git commit command is how you save a snapshot of your staged changes into your project’s history. Every time you run it, Git creates a new, permanent, uniquely-identified object that records exactly what your files looked like at that moment, who made the change, and why. Commits are the building blocks of everything else in Git — branches, merges, and the entire history you can browse, diff, and roll back to are all just chains of these snapshot objects.

Overview: How Commits Work

To understand git commit, you first need to understand Git’s three-tier workflow: the working directory (the files you see and edit), the staging area (also called the index), and the repository (the committed history). When you edit a file, the change exists only in your working directory. Running git add copies that change into the staging area — a snapshot-in-progress that Git builds up file by file. Running git commit takes whatever is currently staged and permanently records it as a new commit object in the repository.

Internally, Git is a content-addressable object store. Every piece of content it tracks is hashed with SHA-1 (the default in most repositories today) into a 40-character hex identifier. Three object types matter here:

  • Blob — the raw contents of a single file, with no filename or metadata attached.
  • Tree — a directory listing: pointers to blobs (files) and other trees (subdirectories), each with a filename and file mode.
  • Commit — a pointer to one tree (the complete snapshot of your project at that moment), plus a pointer to its parent commit (or commits, for a merge), the author, the committer, a timestamp, and the commit message.

When you run git commit, Git builds a tree object from exactly what is currently staged in the index, wraps it in a new commit object that points at that tree and at the current commit as its parent, and then moves the branch pointer — a small file that just holds a commit’s SHA — forward to the new commit. HEAD, in the normal case, points at the branch rather than directly at a commit, so it automatically “follows” the branch pointer as it advances. This is also why a branch is so cheap in Git: it is not a copy of anything, just a tiny reference to a commit SHA.

Because each commit stores the ID of its parent, a series of commits forms a chain (a directed graph once merges are involved) that Git can walk backward through with commands like git log. And because the tree/blob structure is content-addressed, if you commit the exact same file content twice — even in different commits — Git stores the underlying blob only once.

Syntax

The general form of the command is:

git commit [-m "<message>"] [-a] [--amend] [-v] [--no-verify] [--allow-empty]
Flag What it does
-m "<message>" Supplies the commit message inline, skipping the editor.
-a, --all Automatically stages modifications and deletions of files Git already tracks before committing. Does not stage new, untracked files — those still need git add.
--amend Replaces the most recent commit with a new one that combines the staged changes with the previous commit’s snapshot. Rewrites history — never use it on a commit others have already pulled.
-v, --verbose Shows the staged diff inside the commit message editor, so you can review exactly what you’re about to record.
--no-verify Skips pre-commit and commit-msg hooks. Fine for a quick work-in-progress commit, but don’t make it a habit if your team relies on hooks for linting or checks.
--allow-empty Creates a commit even if nothing changed since the last one — occasionally used to trigger CI or mark a milestone.
-e, --edit Forces the commit message editor to open even when -m is also given, letting you tweak the message before finalizing.

If you omit -m, Git opens your configured text editor (set with git config --global core.editor) so you can write a longer, multi-line commit message.

Examples

Example 1: A first commit

git status
git add app.py
git commit -m "feat: add initial CLI entry point"

Output:

On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	app.py

nothing added to commit but untracked files present (use "git add" to track)
[main (root-commit) 3f2a1c9] feat: add initial CLI entry point
 1 file changed, 12 insertions(+)
 create mode 100644 app.py

git status shows app.py as untracked, meaning Git sees the file but nothing about it is staged. After git add app.py, its content is copied into the index. git commit -m "..." then wraps that staged snapshot in a new commit object. The bracketed output [main (root-commit) 3f2a1c9] tells you the branch, that this is the very first commit in the repository (no parent), and the first seven characters of the new commit’s SHA-1 hash.

Example 2: Committing a tracked-file change with -a

git status
git commit -a -m "fix: correct off-by-one error in pagination"

Output:

On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
	modified:   pagination.py

no changes added to commit (use "git add" and/or "git commit -a")
[main 9b7e21f] fix: correct off-by-one error in pagination
 1 file changed, 3 insertions(+), 1 deletion(-)

pagination.py was already tracked from an earlier commit, so git status reports it as “modified” rather than “untracked.” Instead of running git add pagination.py separately, -a stages every already-tracked modified or deleted file automatically, right before building the commit. This only works for files Git already knows about — a brand-new file still needs an explicit git add.

Example 3: Fixing the last commit with –amend

git commit --amend -m "docs: update README with installation steps"

Output:

[main 5c8f3a2] docs: update README with installation steps
 Date: Mon Aug 3 10:15:22 2026 -0400
 1 file changed, 8 insertions(+), 2 deletions(-)

Here the previous commit’s message had a typo. --amend discards the old commit and creates a brand-new one — with a new SHA — that reuses the old commit’s parent and combines any currently staged changes with the previous snapshot. The commit it replaces still exists in Git’s internal object database for a while (reachable through the reflog), but the branch pointer now points only at the new commit. Because the old commit’s SHA disappears from the branch’s history, never amend a commit that has already been pushed and could be in use by someone else unless you coordinate and force-push.

How It Works Step by Step

You can watch the object model in action with git cat-file, which prints the raw contents of any object by its SHA:

git commit -m "feat: add login form"
git cat-file -p HEAD

Output:

tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 8f14e45fceea167a5a36dedd4bfefb1f0d5c9e21
author Priya Shah <priya@example.com> 1786000000 -0400
committer Priya Shah <priya@example.com> 1786000000 -0400

feat: add login form

This is the literal, uncompressed content of a commit object. Reading it top to bottom shows what git commit actually did:

  1. Git read the current contents of the index (the staging area) and, for each staged file, checked whether an identical blob already exists in the object database. If not, it created one, hashed by its content.
  2. Git assembled a tree object from those blobs plus any unchanged blobs carried over from the previous commit’s tree, recording each entry’s filename, file mode, and blob SHA.
  3. Git created a new commit object containing: the tree’s SHA, the SHA of the current commit as parent (omitted entirely for a repository’s very first commit), your name and email as configured with git config user.name / user.email as both author and committer, a timestamp, and your commit message.
  4. Git computed the SHA-1 hash of that whole commit object’s content — this becomes the commit’s permanent ID.
  5. Git updated the ref file for the current branch (for example .git/refs/heads/main) to point at the new commit’s SHA. Since HEAD normally points at that branch (a state called being “attached”), HEAD now resolves to the new commit too.
  6. Git appended an entry to the reflog (git reflog), a local, personal safety log of every place HEAD and your branches have pointed, which is how commits stay recoverable for a while even after an --amend or a reset.

Note what git commit never touches: your working directory files themselves are untouched, and files you never staged are simply carried forward unchanged in the new tree by reusing their existing blob and tree entries from the parent commit.

Common Mistakes

Mistake 1: Forgetting to stage changes before committing

# You edited pagination.py in your editor, then ran:
git commit -m "fix: correct off-by-one error in pagination"

Output:

On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
	modified:   pagination.py

no changes added to commit (use "git add" and/or "git commit -a")

Editing a file does not automatically stage it. Git refused to commit because the index still matches the previous commit, not your working directory. The fix is to stage the change first, or use -a for already-tracked files:

git add pagination.py
git commit -m "fix: correct off-by-one error in pagination"

Mistake 2: Force-pushing an amended commit that others already have

git commit --amend -m "fix: correct off-by-one error in pagination"
git push --force

--amend replaces the commit and its SHA. If a teammate already pulled the original commit, a bare git push --force silently overwrites the shared branch with your rewritten history, potentially discarding commits they pushed in the meantime without any warning. Prefer --force-with-lease, which refuses the push if the remote branch has moved since you last fetched it:

git commit --amend -m "fix: correct off-by-one error in pagination"
git push --force-with-lease

Better still, only amend commits that exist solely on your local machine and have never been pushed.

Mistake 3: Vague messages and unrelated changes crammed into one commit

git add .
git commit -m "stuff"

A message like “stuff” or “fix” tells nobody — including future you — what changed or why, and git add . here bundles every modified file into one undifferentiated commit, making it hard to review, revert, or git bisect later. Split unrelated work into separate, clearly described commits:

git add pagination.py
git commit -m "fix: correct off-by-one error in pagination"
git add README.md
git commit -m "docs: update installation instructions"

Best Practices

  • Write commit messages in the imperative mood (“add”, “fix”, “remove”) and consider the Conventional Commits style — a short type prefix like feat:, fix:, docs:, refactor:, or chore: followed by a concise summary.
  • Keep the first line under about 50 characters and, for anything non-trivial, leave a blank line followed by a longer explanation of why the change was made, not just what changed.
  • Commit small, logically complete units of work. A commit should represent one coherent change that could be reverted on its own without breaking something unrelated.
  • Run git diff --staged (or use git commit -v) before committing to double-check exactly what you’re about to record.
  • Never put secrets — API keys, passwords, private tokens — into a commit. Once pushed, they live in history forever unless you rewrite it with tools like git filter-repo.
  • Prefer git push --force-with-lease over bare --force whenever you must push a rewritten commit.
  • Use --amend freely on commits that only exist locally; treat it as off-limits for anything already shared.

Practice Exercises

  1. Initialize a new repository, create a file called notes.txt with a line of text, and make your first commit. Then run git cat-file -p HEAD and identify the tree SHA it references — use git cat-file -p <tree-sha> to see the tree’s own contents too.
  2. Make a commit with the message "fix typo", then use git commit --amend to give it a proper Conventional Commits message instead, without changing any file content. Confirm with git log -1 that the SHA changed.
  3. Edit two unrelated files in the same working session — for example a bug fix in one file and a documentation update in another. Practice committing them as two separate, well-described commits instead of one combined commit.

Summary

  • git commit takes whatever is staged in the index and permanently records it as a new commit object.
  • A commit is a pointer to a tree (a full snapshot built from blobs) plus a pointer to its parent commit, author/committer info, and a message.
  • A branch is just a movable pointer to a commit SHA; committing moves the current branch’s pointer forward, and HEAD follows along because it points at the branch.
  • -m supplies the message inline; -a stages tracked-file changes automatically; --amend replaces the last commit entirely.
  • Amending or force-pushing history you’ve already shared can overwrite other people’s work — use --force-with-lease and only amend local, unpushed commits.
  • Small, focused commits with clear, imperative-mood messages make history far easier to read, review, and revert later.