insideout Get Pro

03 · Commits and the staging area

Free

Engineering Toolkit, lesson 3

Commits and the staging area

Save work in small, named steps you can always go back to.

Git keeps every saved version of a project, with who changed what and why, so you can go back to any of them. This lesson is the daily loop: start a repository, save changes as commits, read the history, and keep out the files that don't belong. You need the shell from "Files and folders in the shell".

By the end, you can:

  1. Turn a folder into a repository and save its first commit.
  2. Split your changes into separate commits with git add.
  3. Read the history, and name earlier commits with HEAD~1 and HEAD~2.
  4. Keep secrets, environments and data out with a .gitignore.

Part 1. A repository and its first commit

Turn a folder into a repository and save its first commit.

Maybe you've had a file called final_v2_really_final.py. Git replaces that habit. It keeps a project's whole history in a hidden .git folder inside it. Here's a small script to put under git for the next few lessons: it prints how many words a text file has, and its three most common words.

textstats/count.py
import sys
from collections import Counter

text = open(sys.argv[1]).read().lower()
words = text.split()
print("words:", len(words))
for word, n in Counter(words).most_common(3):
    print(word, n)

Tell git who you are, once per computer, using your own name and email. They go into every commit you make. The last line names the first branch main; branches are the next lesson.

Once per computer
$ git config --global user.name "Ana Lima"
$ git config --global user.email "[email protected]"
$ git config --global init.defaultBranch main
A repository and its first commit
$ cd textstats
$ git init
Initialized empty Git repository in /home/ana/textstats/.git/
$ git status --short
?? README.md
?? count.py
$ git add count.py README.md
$ git commit -m "Count words in a text file"
[main (root-commit) 26f2494] Count words in a text file
 2 files changed, 11 insertions(+)
 create mode 100644 README.md
 create mode 100644 count.py
$ git log --oneline
26f2494 (HEAD -> main) Count words in a text file

Example 1.1

The first commit

The textstats folder holds count.py and README.md. Make it a repository and save both files as its first commit.

  1. Make the repositorygit init

    Creates the hidden .git folder. Nothing is saved yet.

  2. See what git noticesgit status --short → ?? twice

    ?? means untracked: git sees the files but isn't keeping them.

  3. Choose what goes ingit add count.py README.md

    Puts both files, as they are right now, into the next commit.

  4. Save the commitgit commit -m → 26f2494

    Saves a snapshot with your message, and names it with an id.

  5. Read the historygit log --oneline

    One commit so far. HEAD -> main marks where you are.

AnswerOne commit, 26f2494, holding both files.

A commit's id is a hash of everything in it: the files, your name and email, the time, the message and the commit before it. Change any of those and the id changes, so yours will differ from 26f2494. Git shows the first seven characters, and more if that's needed to keep them unique.

Repository and commit

A repository is a project whose history git keeps, in a .git folder inside it. A commit is a saved snapshot of the files you added, with an author, a time, a message and a link to the commit before it.

git add the files, then git commit -m "message"

git add . adds everything in the current folder that git isn't told to ignore. It's quick, and it's also how secrets and data files end up in a repository, as part 4 shows. Name the files instead, at least until the project has a .gitignore.

Your turn

In a brand-new repository, you run git commit -m "First version" straight away. What happens?

Git only commits what you've put into the next commit with git add. Until then your files are untracked, and git commit has nothing to save. That extra step is there on purpose, as part 2 shows.

Part 2. The staging area

Split your changes into separate commits with git add.

You fix a bug in count.py: "the" and "the," were counted as different words. While you're there, you add a usage line to the README. Those are two separate changes. If one turns out wrong you'll want to undo it alone, so each gets a commit of its own.

count.py, now ignoring punctuation
import re
import sys
from collections import Counter

text = open(sys.argv[1]).read().lower()
words = re.findall(r"[a-z']+", text)
print("words:", len(words))
for word, n in Counter(words).most_common(3):
    print(word, n)
What changed
$ git status --short
 M README.md
 M count.py
$ git diff README.md
diff --git a/README.md b/README.md
index 737ddaf..6cbb648 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
 # textstats
 
 Count the words in a text file.
+
+Usage: python3 count.py notes.txt
Two changes, two commits
$ git add count.py
$ git status --short
 M README.md
M  count.py
$ git commit -m "Ignore punctuation when counting"
[main 0f2361e] Ignore punctuation when counting
 1 file changed, 2 insertions(+), 1 deletion(-)
$ git add README.md
$ git commit -m "Show how to run it in the README"
[main ba52d05] Show how to run it in the README
 1 file changed, 2 insertions(+)

Example 2.1

Two changes, two commits

count.py and README.md have both changed. Save them as two commits, the bug fix first.

  1. Check what changedgit status --short

    An M in the right-hand column for both files: changed on disk, not staged. git diff shows the lines, + added and - taken out.

  2. Stage the fix onlygit add count.py

    count.py's M moves to the left-hand column: it's staged for the next commit.

  3. Commit it→ 0f2361e

    Only what's staged goes in. README.md is still changed and unstaged.

  4. Stage and commit the README→ ba52d05

    The second change gets a commit of its own.

AnswerTwo commits: 0f2361e for the fix, ba52d05 for the README.

git add copies a file, as it is at that moment, into a holding area for the next commit. git commit saves what's in that area, and nothing else. So you decide what each commit holds, whatever else you've changed on disk. git diff --staged shows what's waiting there.

working treethe files on diskstaging areathe next commitrepositoryevery commitcount.pyREADME.mdnot stagedgit add count.pycount.pygit commit0f2361e
Figure 1git add count.py copies count.py into the staging area, and git commit saves it as 0f2361e. README.md stays changed in the working tree, ready for a commit of its own.

Staging area

The changes that go into the next commit, also called the index. git add puts a file's current version there, and git commit saves it. The files on disk, with all your edits, are the working tree.

working tree → git add → staging area → git commit → repository

Read git status --short as two columns: the left one is the staging area, the right one the working tree. An M on the right is changed but not staged, an M on the left is staged, and ?? is untracked. A file can be in both columns at once, as the next question shows.

Your turn

You edit count.py and README.md, run git add count.py, edit count.py once more, then run git commit. What does the commit contain?

git add copies the file as it is at that moment. Later edits stay in the working tree until you add them again, and README.md was never added. git status --short would show count.py as MM: staged, and modified again since.

Part 3. Reading the history

Read the history, and name earlier commits with HEAD~1 and HEAD~2.

History is only worth keeping if you can read it. git log lists commits, newest first, and git show shows one in full.

Looking back
$ git log --oneline
ba52d05 (HEAD -> main) Show how to run it in the README
0f2361e Ignore punctuation when counting
26f2494 Count words in a text file
$ git show --stat HEAD~1
commit 0f2361ebe6c44f6cfb3082886bafb79ca2a4a721
Author: Ana Lima <[email protected]>
Date:   Wed Nov 4 09:31:00 2026 +0000

    Ignore punctuation when counting

 count.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

Example 3.1

Naming a commit by counting back

Which commit is HEAD~1, and what did it change?

  1. Find HEAD(HEAD -> main) → ba52d05

    HEAD is the commit you're on, here the newest on main.

  2. Count back oneHEAD~1 → 0f2361e

    ~1 is the commit before, its parent. HEAD~2 would be 26f2494.

  3. Show itgit show --stat HEAD~1

    The full id, the author, the time and the message, then the files it changed.

  4. Read the changecount.py | 3 ++-

    Three lines touched in count.py: 2 added and 1 taken out.

AnswerHEAD~1 is 0f2361e, the punctuation fix, which changed count.py.

Each commit records the one before it, so history is a chain you walk back from HEAD. ~1 takes one step back and ~2 two. They work wherever git expects a commit: git diff HEAD~2 HEAD shows everything the last two commits changed.

26f2494HEAD~2first version0f2361eHEAD~1punctuation fixba52d05HEADREADME usagemainnewest on the right; each commit points back to its parent
Figure 2Each commit points back to its parent. HEAD is where you are, and HEAD~1 and HEAD~2 count back from it.

HEAD

Git's name for the commit you're on now, usually the newest commit of your branch. HEAD~1 is its parent, and HEAD~2 the commit before that.

HEAD~n = n commits back from where you are

A message is for whoever reads the history a year from now. The diff already shows what changed, so say what the commit does and why. Git's own manual suggests a first line of at most 50 characters, then a blank line and the details. git commit without -m opens your editor for exactly that.

Your turn

git log --oneline lists five commits, newest first: 7c1e2a9 (HEAD -> main), 41b09fd, c3d8e20, 9a0f5b1 and 2e7714c. Which commit does git show HEAD~3 show?

HEAD is 7c1e2a9. One step back is 41b09fd, two is c3d8e20 and three is 9a0f5b1. Picking c3d8e20 counts HEAD itself as the first step back.

Part 4. What to leave out

Keep secrets, environments and data out with a .gitignore.

Some files should never be committed. Virtual environments are big and can be rebuilt. Python's cache files are rebuilt on every run. Data and model files are big and often private. And anything holding a password or an API key must stay out.

Files git has noticed
$ git status --short
?? .env
?? .venv/
?? __pycache__/
?? data/
.gitignore
# Python
.venv/
__pycache__/
*.pyc

# Secrets and local settings
.env

# Data and model files: too big for git, and often private
data/
*.pt
After the .gitignore
$ git status --short
?? .gitignore
$ git check-ignore -v .env
.gitignore:7:.env	.env
$ git add .gitignore
$ git commit -m "Leave out environments, secrets and data"
[main a24cc41] Leave out environments, secrets and data
 1 file changed, 11 insertions(+)
 create mode 100644 .gitignore

Example 4.1

A .gitignore for a Python project

git status lists .env, .venv/, __pycache__/ and data/. Hide all four from git, and find out what hides .env.

  1. Write the patterns.gitignore

    One pattern per line, and # starts a comment. .venv/ matches that folder, and *.pyc any name ending in .pyc.

  2. Check againgit status --short → ?? .gitignore

    Only the .gitignore itself is left, and it belongs in the repository.

  3. Ask what hides a filegit check-ignore -v .env

    Line 7 of .gitignore, the pattern .env, is hiding it.

  4. Commit the rules→ a24cc41

    Now everyone with a copy of the project leaves out the same files.

AnswerAll four are hidden, and the .gitignore is committed.

A .gitignore lists untracked paths for git to act as if it can't see, so git status stays readable and git add . can't pick them up. A pattern that ends in / matches only folders. A pattern with no / at its start or in its middle matches at any depth, so *.pt hides model files in every folder.

.gitignore

A file of patterns, one per line, for paths git should leave out. It only affects files git isn't tracking yet. A file that's already committed stays tracked until git rm --cached takes it out of the index.

So a .gitignore can't hide a secret that's already committed. git rm --cached .env stops tracking the file and keeps your copy, but the old commit still holds the key, and anyone with a copy of the repository can read it. Treat the key as leaked: revoke it and make a new one.

Your turn

A .gitignore holds two lines, data/ and *.pt. Which of these files does git status still list?

data/ matches folders called data at any depth, so data/raw/prices.csv and notes/data/old.csv are hidden, and *.pt hides models/best.pt. notes/data.md is a file whose name only starts with data, so no pattern matches it.

Where you'll meet it

In interviews

"What's the difference between git add and git commit?" is a fair opening question. Answer with the three places: add copies a change into the staging area, and commit saves what's staged as a snapshot in the repository. Then say why it helps: one commit per change, so each can be read, reviewed or undone alone.

On the job

GitHub's secret scanning checks a repository's whole history, on every branch, for keys and passwords. Push protection, on by default for your account, stops you pushing known kinds of key to a public repository. Its docs give part 4's advice: revoke a leaked key, since scrubbing it from the history is slow and, once it's revoked, often unneeded.

The history of one file
$ git log --oneline -- count.py
0f2361e Ignore punctuation when counting
26f2494 Count words in a text file

A path after -- limits the log to the commits that changed that file: the README and .gitignore commits drop out.

Check yourself

Four questions from across the lesson. Try each one before you look back.

1

git status --short shows MM count.py. What does that mean?

The left M is the staging area: a version of count.py is staged. The right M is the working tree: the file changed again after git add. A commit now takes the staged version; run git add again to include the rest.

2

Which commit message helps most when someone reads the history a year from now?

The diff already shows which file changed and how. The message is there for the why. A good first line says what the commit does, as an instruction ("Ignore", "Add", "Fix"), short enough to read in git log --oneline. A longer reason can follow after a blank line.

3

Last week you committed .env, with an API key in it. Today you add .env to .gitignore and commit that. What's true now?

.gitignore only affects files git isn't tracking yet. git rm --cached .env stops tracking it and keeps your copy on disk. The old commit still holds the key, and anyone with a clone can read it, so treat the key as leaked: revoke it and make a new one.

4

A nightly script runs under set -e. One line is git commit -m "Nightly results", and on some nights nothing has changed. What happens on those nights?

With nothing to commit, git commit says so and returns 1, and set -e stops at any code that isn't 0. If a quiet night is normal, say so: git commit -m "Nightly results" || true.

What to remember

  1. 1A repository and its first commit

    git init makes a repository; git add picks the files and git commit -m saves them as a commit with an id, like 26f2494.

  2. 2The staging area

    git commit saves only what's staged, and git add stages a file as it is at that moment; in git status --short, an M on the left is staged and an M on the right isn't.

  3. 3Reading the history

    HEAD is the commit you're on; HEAD~1 is the one before it and HEAD~2 the one before that.

  4. 4What to leave out

    A .gitignore hides only untracked files; a secret that's already committed stays in the history, so revoke it.

Recap and going further

The lesson on one page

Set up once
  • git config --global user.name
  • init.defaultBranch main
Three places
  • Working tree: your files
  • Staging area: the next commit
  • Repository: every commit
The daily loop
  • git status --short and git diff
  • git add what belongs together
  • git commit -m
Save work in small, named steps
Reading history
  • git log --oneline
  • HEAD, HEAD~1, HEAD~2
  • git show --stat
Leave out
  • Environments, caches, data, models
  • Secrets, always
  • Committed already? git rm --cached, and revoke the key

Try this

Put one of your projects under git

Pick a folder you work in, even a folder of notebooks. Run git init, write a .gitignore before your first git add, and check with git status --short that only the files you mean are listed. Then make three small commits that each do one thing, and read them back with git log --oneline and git show --stat HEAD~1.

You need: Git. git --version checks; a Mac offers to install it the first time you type git, and on Windows it comes with Git for Windows.

Look for

  • Files git status lists that you didn't expect, such as .DS_Store or .ipynb_checkpoints.
  • What git diff shows for a notebook (.ipynb) file, and why notebooks make noisy diffs.
  • How git status --short changes after git add and after git commit.
  • Whether each of your messages says why, and not only what.

The words, in plain language

Repository and commit
A repository is a project whose history git keeps, in a .git folder inside it. A commit is a saved snapshot of the files you added, with an author, a time, a message and a link to the commit before it.
Staging area
The changes that go into the next commit, also called the index. git add puts a file's current version there, and git commit saves it. The files on disk, with all your edits, are the working tree.
HEAD
Git's name for the commit you're on now, usually the newest commit of your branch. HEAD~1 is its parent, and HEAD~2 the commit before that.
.gitignore
A file of patterns, one per line, for paths git should leave out. It only affects files git isn't tracking yet. A file that's already committed stays tracked until git rm --cached takes it out of the index.

Engineering Toolkit overviewSpotted a mistake? Tell us.