Skip to content

The commit loop

docs.scrimba.com

Say you have been working on weather-app for the last twenty minutes. You fixed a real bug in src/index.js, the temperature conversion was off, and while you were in there you also started restyling styles.css, but that change is half finished and not ready for anyone else to see. You want to save the bug fix as its own checkpoint right now, without dragging the unfinished styling along with it.

That is exactly what the commit loop is for. If you have not set up a repository yet, Your first repository walks through git init and cloning first. From here on, this chapter assumes you have one open.

The three stops: working directory, staging area, commit

Every change you make in Git passes through three stops before it becomes permanent history. Your working directory is the project as it sits on disk right now, the files you are actively editing. The staging area is a holding spot where you place exactly the changes you want to save next. A commit is the saved snapshot itself, once you write it, plus the message describing it.

Picture packing for a move. Your whole house is the working directory: everything you own, in whatever state it is in. The boxes you have packed and taped by the front door are the staging area: only what you have deliberately chosen to take. The moving truck driving away with those boxes is the commit: a record of exactly what left, at that moment, with a label on the outside saying what is inside.

Here is that mental model showing up in a real terminal. After editing both files in weather-app, git status reads the current state without changing anything:

bash
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/index.js
        modified:   styles.css

no changes added to commit (use "git add" to commit)

Both files show up as "not staged" because editing a file only changes your working directory. Nothing moves into the staging area until you tell Git to put it there with git add, which is the next section.

This is where the staging area's whole reason for existing becomes clear. The staging area lets you choose exactly what goes into your commit, independent of what else is still sitting edited in your working directory. Without it, saving a checkpoint would force you to take every change on disk at once, ready or not.

With it, you can stage the finished bug fix in src/index.js, leave the half-done styles.css out, and commit only the piece that is actually done. This one detail, that staging and your working directory can hold different things at the same time, trips up nearly everyone the first week. Once it clicks, the rest of Git's daily workflow makes a lot more sense.

In day-to-day work, make git status a routine step before every add and every commit, whether or not something feels off. It costs nothing, it never changes your project, and it stops the common mistake of committing more than you meant to. Get in the habit of checking status, staging, then checking status again before you commit: it takes seconds and it means the message on your commit always matches what actually went into it.

The staging area is a real file, called the index, sitting at .git/index inside your repository. Every time you run git add, Git updates that file to record which version of which file is staged. When you run git commit, Git builds the new commit purely from what the index says, without checking your working directory again. That is the whole mechanism behind the split you saw a moment ago in git status: two files edited on disk, but the index tracking neither of them yet, so the commit that follows would currently contain nothing.

JunoThe three areas of Git Your working directory is your files as they sit right now. The staging area is where you place exactly the changes you want to save next, and a commit is the saved snapshot once you write it. Editing a file never stages it by itself, you choose what goes in with git add, and that is what lets you save one finished change while leaving a half-done one out.
JunoThe three areas of Git Working directory, staging area, commit: edits live in the first, git add moves exactly what you choose into the second, and git commit saves the second as a permanent snapshot. Run git status before you stage and again before you commit, it is free and it keeps your commits matching what you meant to save.
JunoThe three areas of Git The staging area is a real file, the index at .git/index, and git commit builds its snapshot from the index, never straight from your working directory. That is why two edited files can both show as unstaged: the index has not been told about either one yet. Understanding the index this way makes every staging command feel less like magic and more like reading and writing one file.

Staging changes: git add

git add moves a change from your working directory into the staging area. Point it at the file whose changes you want in the next commit:

bash
$ git add src/index.js
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/index.js

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   styles.css

src/index.js moved to "Changes to be committed", the bug fix is staged and ready. styles.css is still listed under "not staged", exactly where you want it while the restyle is unfinished. Running git add styles.css too would stage it as well; running git add . stages every changed file in the current folder at once, which is convenient once you trust that everything on disk is actually ready to go.

Sometimes a single file has both a change you want to save and one you do not, and splitting them into two files is not practical. git add -p <file> (short for --patch) walks through the file's changes in small chunks, called hunks, and asks you to decide on each one:

bash
$ git add -p src/index.js
@@ -12,7 +12,7 @@ function celsiusToFahrenheit(celsius) {
-  return celsius * (9 / 5 + 32)
+  return (celsius * 9) / 5 + 32
Stage this hunk [y,n,q,a,d,e,?]?

Answer y to stage that hunk, n to leave it for later, and q to stop. This gives you commit-sized control even inside a single file, which is worth reaching for whenever "half this file is done" describes your situation.

git add does two things at once, and knowing both explains a gotcha you will eventually hit. First, it writes the file's current content into a new blob, an object Git stores permanently, identified by a hash the same way commits are, that holds one file's exact content at one moment. Second, it updates the index to point at that blob for that file. Nothing about add looks at the file again afterward.

That is why, if you git add src/index.js and then keep editing the same file before committing, git status shows it as staged and modified at the same time: the index still points at the blob from when you ran add, while your working directory has moved on. git commit only sees the staged blob, so a second git add is what pulls the newer edits in before you save.

JunoStaging changesgit add <file> moves that file's current changes into the staging area, ready for the next commit. Files you have not added yet stay out, so you can finish one thing and leave another mid-edit. git add . stages every changed file at once, handy once everything is actually ready.
JunoStaging changesgit add <file> stages a whole file, and git add -p <file> lets you stage it hunk by hunk when only part of the file is ready. That second one is the tool to reach for the moment "half this file is done" is true.
JunoStaging changesgit add writes your file's current content into a blob and points the index at it, it does not watch the file afterward. Edit the file again before committing and status shows it as both staged and modified, because the index still holds the older blob. Re-run add to pull the newer edit in. This one cost me a confused ten minutes the first time I hit it.

Saving a snapshot: git commit

Once the staging area holds what you want, git commit saves it as a permanent snapshot, with the -m flag (short for "message") supplying the description that travels with it:

bash
$ git commit -m "Fix temperature conversion in Celsius-to-Fahrenheit formula"
[main 4f2a1c9] Fix temperature conversion in Celsius-to-Fahrenheit formula
 1 file changed, 1 insertion(+), 1 deletion(-)

Only src/index.js went in, because that is all you staged. styles.css is still sitting edited in your working directory, untouched, waiting for whenever the restyle is finished. The short id, 4f2a1c9, names this exact commit, and the History chapter covers reading a project's full list of commits like this one.

Write commit messages in the imperative mood, as an instruction. "Fix temperature conversion" matches the wording Git itself uses ("this commit will...") and reads cleanly in tools that list commits one per line. For anything more involved than a one-line fix, add a body that explains why the change was needed. The diff already shows what changed, so save the body for the reasoning behind it:

bash
$ git commit -m "Fix temperature conversion in Celsius-to-Fahrenheit formula

The formula was applying operator precedence in the wrong order,
which rounded warm temperatures down by a degree in the UI."

If you notice a typo or a missed detail right after committing, git commit --amend replaces your most recent commit with a new one instead of adding another on top:

bash
$ git commit --amend -m "Fix temperature conversion in Celsius-to-Fahrenheit formula"

Only amend a commit you have not shared anywhere yet. Once you have pushed it (sent it to a shared copy of the project), amending rewrites history other people may already have, and Undoing things covers the safe ways to fix a commit after that point.

A commit itself is a small object with three parts: a pointer to a tree, the author and message, and a pointer to its parent commit. The tree is a snapshot of your whole project's directory structure at that moment: for every file and folder, it records a name and a pointer to either a blob (a file's content) or another tree (a subfolder). Running git add writes blobs and updates the index one file at a time; git commit is the moment Git turns the current index into one finished tree and wraps it in a commit object.

Two files with identical content point at the exact same blob, even sitting in different folders, so a commit never stores those bytes twice. That structure also makes comparing two commits efficient: Git walks their two trees side by side and only opens the blobs whose pointers actually differ, skipping every file that stayed the same.

JunoSaving a commitgit commit -m "message" saves whatever is currently staged as a permanent snapshot, with the message you give it. Anything you did not stage stays out and stays editable. Keep the message short and clear about what the commit does, future you will read it back more often than you expect.
JunoSaving a commit Write commit messages as an instruction, "Fix" rather than "Fixed", and add a body whenever the diff alone would leave someone guessing why you made the change. git commit --amend replaces your last commit instead of stacking a new one, which is perfect for a message typo, as long as you have not pushed that commit anywhere yet.
JunoSaving a commit A commit points at a tree, and a tree maps names to blobs and further trees, a full nested snapshot of the project built from whatever the index held at commit time. add writes the blobs and commit turns the index into a finished tree, and identical content anywhere in the project shares one blob instead of being stored twice. Picture that shape and two things stop feeling like magic: why nothing gets duplicated, and why comparing two commits means walking two trees side by side instead of rereading every file.