Working with Changes

Advanced · 17 min read · ▶ live playground · ✦ checkpoint

A commit is only as useful as the change you meant to save. A diff is Git's line-by-line view of what changed, and this lesson gives you the habit professionals rely on: inspect first, then stage, then commit.

You'll also learn how to undo changes by where they live. Git's undo commands are not one magic eraser; they are different tools for the workbench, the packing tray, and the archive cabinet.

Read the Diff Before You Commit

Maya is working in a fresh prepared repo where snack_journal.py already has one committed snack. Now she edits the file in her editor so the snack list includes raisins. Before staging anything, she asks Git to show the exact change:

$ git diff -- snack_journal.py
diff --git a/snack_journal.py b/snack_journal.py
index 1d47352..898c37c 100644
--- a/snack_journal.py
+++ b/snack_journal.py
@@ -1,3 +1,3 @@
-snacks = ["cashews"]
+snacks = ["cashews", "raisins"]
 for snack in snacks:
     print(snack)
 
$ git add snack_journal.py
$ git diff --staged -- snack_journal.py
diff --git a/snack_journal.py b/snack_journal.py
index 1d47352..898c37c 100644
--- a/snack_journal.py
+++ b/snack_journal.py
@@ -1,3 +1,3 @@
-snacks = ["cashews"]
+snacks = ["cashews", "raisins"]
 for snack in snacks:
     print(snack)

The - line is what the older snapshot had. The + line is what your next snapshot would have if you committed this staged change. A hunk is one nearby group of changed lines inside a diff, like one circled correction on a printed page.

Use git diff for unstaged changes on the workbench. Use git diff --staged for changes already on the packing tray. That pair answers the two questions that matter before a commit: "What did I change?" and "What am I about to save?"

The -- snack_journal.py tail means "show only this file." Leave it off when you want to inspect every changed file.

Undo by Location

Git feels scary when "undo" is treated as one button. Use the three-state model from 19.1 instead: working tree, staging area, repository history. The command depends on where the change currently lives.

git restore is a command that restores files in your working tree from Git's saved version. If Maya adds a temporary debug line and has not staged it, she can throw away that local edit:

$ git status --short
 M snack_journal.py
$ git restore snack_journal.py
$ git status --short

No output from git status --short means the workbench is clean. git restore snack_journal.py discards unstaged edits in that file, so read your diff first. This is like wiping pencil notes off the workbench copy, not changing the archive cabinet.

HEAD is Git's name for the current commit your working tree is based on. git reset is a command that can move staged changes back out of the staging area; in this beginner-safe shape, it does not delete the file edit:

$ git add snack_journal.py
$ git status --short
M  snack_journal.py
$ git reset HEAD snack_journal.py
Unstaged changes after reset:
M	snack_journal.py
$ git status --short
 M snack_journal.py

Notice the space moved. M snack_journal.py means the modification is staged. M snack_journal.py means the modification is back on the workbench but no longer staged. Use this when you added the wrong file, or when one big staged pile needs to be split into cleaner commits.

git revert is a command that creates a new commit which cancels an older commit. It is the safest undo for shared history, which is commit history another person or GitHub may already have copied:

$ git log --oneline -2
d79562d Add raisin snack
2873a2a Start snack journal
$ git revert --no-edit d79562d
[main 8aa9dcd] Revert "Add raisin snack"
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -3
8aa9dcd Revert "Add raisin snack"
d79562d Add raisin snack
2873a2a Start snack journal

Revert does not pretend the old commit never happened. It adds a new snapshot that visibly says, "we backed that out." That honesty is exactly why it works well once other people may have seen the history.

Keep Python Junk Out With .gitignore

A .gitignore file is a plain text file that tells Git which untracked files and folders to ignore. It belongs at the project root, beside files like pyproject.toml or README.md.

For a Python project, start with this shape:

# Local environments and secrets
.venv/
.env
.env.*
!.env.example
 
# Python cache and test output
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
 
# Build and packaging output
dist/
build/
*.egg-info/

An ignored file is an untracked file that Git hides from normal status output because a .gitignore pattern matches it. The goal is not secrecy by magic. The goal is to keep noisy, machine-specific, or private files from entering history in the first place.

Commit .gitignore early, ideally before creating virtual environments, coverage reports, or local .env files. Your repo should preserve source code, tests, configuration examples, and documentation - not caches, installed packages, build output, or real secrets.

The !.env.example line is an exception rule: ignore real .env files, but keep the example file tracked so teammates know which settings exist.

Here is the safe shape when Git already tracks a file that should become local-only:

$ git status --short
 M .env
?? .gitignore
$ git rm --cached .env
rm '.env'
$ git status --short
D  .env
?? .gitignore

The D .env means the next commit removes .env from future snapshots. Your local .env file still exists on disk, and future changes to it will be ignored because .gitignore now matches it.

Checkpoint

Answer all three to mark this lesson complete

Build this habit into every project: check the diff, stage only the intended change, and keep generated files out of the snapshot. Next you'll use branches to keep whole lines of work separate before merging them back together.

+50 XP on completion