Table of Contents

Introduction

A commit is the unit everything else in Git is built from. Branches point at commits, merges join them, rebase replays them, and the reflog remembers where they were. So it's worth being precise about what one actually is and what git commit does when it makes one.

In this article, we'll:

  1. Watch git commit create a new commit on a small repository
  2. Check which of our three pending changes made it in and which didn't
  3. Look at what a commit object contains

What is git commit?

git commit takes the contents of the staging area, writes them into the object database as a tree, and creates a commit object that points at that tree, at the previous commit (its parent), and carries your name, the date and a message. Then it moves the current branch to point at the new commit.

Only what's in the staging area goes in. That's the whole reason git add exists as a separate step.

Watch it happen

Our sample repo has one file staged, one modified but not staged, one untracked. Here's git commit -m "Describe the project in the README":

  1. Before: HEAD is attached to main at 8c02d5b, with README.md staged.
  2. Git creates a new commit, 9d76363, from the staged README.md, with the message we gave it and 8c02d5b as its parent.
  3. main moves to the new commit, and HEAD moves with it because it's attached to main.
  4. The feature branch is unaffected. A commit only ever moves the branch you're on.

Before and after

The commit graph gained one commit, 9d76363. Now compare git status before and after:

M  README.md
 M app.py
?? notes.txt
 M app.py
?? notes.txt

Only README.md disappeared from the list, because it was the only staged change. The unstaged edit to app.py and the untracked notes.txt are still sitting in the working directory, exactly as they were.

The raw git output, if you want to read along in text

git log --oneline --graph --all

before

* 1117a34 (feature) Add search tests
* e5869f0 Fix typo in search box
* fc19889 Add search box
| * 8c02d5b (HEAD -> main) Update dependencies
| * a0b2db3 Add user settings page
|/  
* 96c4fc2 Fix header layout
* ae65976 Add login page
* 3e1ffe4 Add project skeleton
* 4114b2c Initial commit

after

* 9d76363 (HEAD -> main) Describe the project in the README
* 8c02d5b Update dependencies
* a0b2db3 Add user settings page
| * 1117a34 (feature) Add search tests
| * e5869f0 Fix typo in search box
| * fc19889 Add search box
|/  
* 96c4fc2 Fix header layout
* ae65976 Add login page
* 3e1ffe4 Add project skeleton
* 4114b2c Initial commit

what git printed

[main 9d76363] Describe the project in the README
 1 file changed, 3 insertions(+), 3 deletions(-)

What's inside a commit

Run git cat-file -p 9d76363 on the new commit and you get something like this (Pro Git's chapter on Git objects builds one by hand):

tree cb7fb79...
parent 8c02d5b...
author Jacob Stopak <jacob@initialcommit.io> 1714597200 +0000
committer Jacob Stopak <jacob@initialcommit.io> 1714597200 +0000

Describe the project in the README

Four things: a tree (the snapshot of every file), a parent, two identity lines and the message. The hash is computed from all of it, which is why changing any one of them, even a character of the message, produces a different commit. (I wrote about the tree object separately if you want to follow that pointer down.)

A few years ago I pulled the commit messages from a few million GitHub repositories to see how many use the imperative mood, as Git's own guidelines ask. The answer was roughly 44%, and I suspect the real number is higher once you account for ticket numbers at the start of messages. Either way, "Describe the project in the README" is the form I try to stick to.

Is it safe?

Safe git-sim pre-flight

Creates a new commit; nothing at risk.

commit adds a commit and moves a branch forward by one. It doesn't touch the working directory and it can't remove anything. The only way it surprises people is by not including changes they forgot to stage.

Useful forms

  • git commit -a stages every tracked, modified file first, then commits. New files are still left out.
  • git commit -m "message" gives the message inline instead of opening an editor.
  • git commit --amend replaces the last commit instead of adding one. It has its own page, because it rewrites history.
  • git commit -v shows the diff in the editor while you write the message. Signing (-S), fixups (--fixup) and the other flags are in the git commit documentation.

How to undo it

To take the commit back but keep the changes staged:

git reset --soft HEAD~1

The commit object still exists in .git and in the reflog. Only the main label moved back. If the commit has already been pushed, use git revert instead so you don't rewrite history others have.

Try it on your repository

pip install git-sim
git-sim commit -m "Describe the project in the README"

git-sim shows where the new commit would land and which staged files it would contain, before anything is written.

Common questions

What does git commit do?

It writes the staging area to the object database as a tree, creates a commit object pointing at that tree and the previous commit, and moves the current branch to the new commit.

Does git commit include unstaged changes?

No. Only staged changes are recorded. git commit -a stages modified tracked files first, but never new files.

Can I change a commit message after committing?

Yes, with git commit --amend -m "new message". That creates a replacement commit with a new hash, so do it before pushing.

How do I undo a commit?

git reset --soft HEAD~1 removes it from the branch and keeps the changes staged. git revert <sha> adds a new commit that undoes it, which is the right choice once it has been pushed.

Summary

In this article, we watched git commit create one commit from a single staged file, confirmed that the unstaged and untracked changes stayed behind, and looked at the four things a commit object contains.

Next steps

Read git commit --amend for fixing the commit you just made, and git log for reading the ones before it.