Ana has her repository and Bruno has his clone. From here on, both of them do exactly the same thing every day, dozens of times over: they edit files, decide which changes belong together and record them in the history. That three-step cycle — edit, stage, commit — is Git's heartbeat. Everything else in the course (branches, merges, rebase, remotes) is built on top of it.
In module 1 you learned the theory: the three areas (working tree, staging area and repository) and the three states (modified, staged, committed). This lesson sets that theory in motion. We are going to watch a file travel from one area to another, see which states it passes through from the moment Git knows nothing about it until it is committed, and learn how git status tells you at every moment exactly where you stand.
The goal is that, by the end, you can look at the output of git status in any repository in the world and know within a second what is going on and what the sensible next command is.
Contents
- The three-step cycle
- The three areas in motion
- The life cycle of a file
git status: the compassgit status --short: the short form and its table of codes- Files you do not want to version:
.gitignorein two minutes - A complete working session of Ana's, narrated
- The cycle in daily practice
- The three-step cycle
Git's basic workflow has this shape:
graph LR
E["1 · EDIT<br/>You change files<br/>in your folder"] --> P["2 · STAGE<br/>git add<br/>You choose what goes in"]
P --> C["3 · COMMIT<br/>git commit<br/>It is recorded for good"]
C -.->|round again| E
Three steps, two commands. What throws people at first is the middle step: why is "saving the changes" not enough? What is the staging area for?
The answer is that it separates two different decisions:
- Editing answers "what have I changed?". That is work.
- Staging answers "which of the things I changed form one meaningful unit?". That is judgement.
Imagine Ana spends the morning fixing a bug in app.js and, along the way, corrects a spelling mistake in README.md and tweaks a colour in styles.css. Those are three unrelated things. Without a staging area she would have two poor options: dump everything into one jumbled commit, or undo changes by hand so as to commit them separately. With the staging area she chooses: stage app.js alone, commit, stage README.md, commit, and so on.
The result is a history where each commit tells one single story. When somebody comes looking six months from now for why task deletion broke, they will find a commit that talks about exactly that and not about three tangled matters.
This idea — the atomic commit — matters so much that we develop it fully in the next lesson, Staging and Committing Changes.
- The three areas in motion
Let us bring back the diagram from Basic Git Terminology, this time with the commands that move information from one area to another:
graph LR
WT["WORKING<br/>TREE<br/>Your files<br/>on disk"]
IDX["STAGING<br/>AREA<br/>.git/index<br/>The draft of the<br/>next commit"]
REPO["REPOSITORY<br/>.git/objects<br/>The permanent<br/>history"]
WT -->|git add| IDX
IDX -->|git commit| REPO
IDX -->|git restore --staged| WT
REPO -->|git checkout / git restore --source| WT
Three rules worth committing to memory:
- Only what is staged gets committed.
git commitdoes not look at the working tree: it photographs the staging area. A change you have not staged does not go in, however safely it sits on disk. - The staging area is a state, not a queue. It is not a list of pending commands; it is a complete version of the project. It holds the entire content of every file, not "the lines you added".
- The three areas can hold different content for the same file at the same time. This is beginners' confusion number one, and it is also the source of Git's power. We will see it in action in the narrated session.
- The life cycle of a file
Every file in your folder is, at any given moment, in one of four states. This diagram gathers them all and shows which command produces each transition:
stateDiagram-v2
[*] --> Untracked: you create the file
Untracked --> Staged: git add
Staged --> Unmodified: git commit
Unmodified --> Modified: you edit the file
Modified --> Staged: git add
Staged --> Modified: you edit it again
Staged --> Untracked: git rm --cached
Modified --> Unmodified: git restore
Unmodified --> [*]: git rm
Untracked: Untracked (new to Git)
Unmodified: Unmodified (matches the last commit)
Modified: Modified (changed, not staged)
Staged: Staged (sitting in the index)
Let us take the four states one by one:
| State | Tracked? | What it means | How you see it in git status |
|---|---|---|---|
| Untracked | No | Git can see the file but has never recorded it. It is not part of the project | In the Untracked files section |
| Unmodified | Yes | Identical to the last commit. There is nothing to do with it | It does not appear: Git only reports what has changed |
| Modified | Yes | It has changed since the last commit, but has not been staged | In Changes not staged for commit |
| Staged | Yes | Its current version will go into the next commit | In Changes to be committed |
There is an underlying distinction worth nailing down: tracked versus untracked. A file is tracked if it appears in the last commit or in the staging area — that is, if Git knows about it. The three states "unmodified", "modified" and "staged" are all variants of tracked. "Untracked" is the only category outside.
This distinction matters because many commands act only on tracked files. git commit -a, for instance, automatically stages modified files, but not untracked ones; those have to be added by hand at least once.
A key detail: a file can be in two states at once
Look at the Staged --> Modified transition in the diagram. It is real and it happens constantly:
- Ana modifies
app.jsand stages it withgit add app.js. The staging area holds that version. - Ana keeps working and touches
app.jsagain. Now the disk holds a newer version than the staging area does.
The result: app.js shows up in both sections of git status, the staged one and the unstaged one. That is neither an error nor an anomaly: there are two different versions saved in two different areas, and if Ana committed now, the staged one (the older) would go in, not the one on disk.
git status: the compass
git status: the compassIf you had to keep just one Git command, this would be it. It changes nothing, it is instant, and it always answers the same four questions: which branch you are on, how you stand against the remote, what is staged and what is outstanding.
Let us read a full output, with every case present at once:
On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: styles.css new file: .gitignore 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: app.js deleted: old.js Untracked files: (use "git add <file>..." to include in what will be committed) notes.txt
Let us dissect it block by block:
The header.
On branch main: the current branch. It is the first thing to check; being on the wrong branch is a classic.Your branch is up to date with 'origin/main': a comparison against the remote-tracking branch. It only appears when a remote is configured, as in Bruno's clone. It can also sayahead by N commitsorbehind by N commits. We will develop this in module 4.
Changes to be committed — the staging area. Everything listed here will go into the next git commit. Each line carries a verb telling you the kind of change: modified, new file, deleted, renamed.
Changes not staged for commit — changes in the working tree to files Git already tracks, but which will not go into the next commit. Careful: only tracked files show up here.
Untracked files — files Git is seeing for the first time. They will never enter any commit until you run git add explicitly.
The hints in brackets. In every section Git tells you how to move forward and how to step back. They are worth reading: they are the best in-line documentation there is.
The outputs you will see most often
Everything committed, nothing outstanding. The three areas agree. This is the ideal state in which to switch to another task.
On branch main Untracked files: (use "git add <file>..." to include in what will be committed) notes.txt nothing added to commit but untracked files present (use "git add" to track)
Nothing is staged. Run git commit now and Git would refuse, because there would be nothing to commit.
On branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) modified: app.js no changes added to commit (use "git add" and/or "git commit -a")
You have done work, but you have staged none of it. That final line is the warning: a bare git commit would achieve nothing.
git status --short: the short form and its table of codes
git status --short: the short form and its table of codesThe long output is excellent for learning, but it wears you down when you consult it forty times a day. The short form fits on one screen:
The key is that there are two columns before the file name, and they mean different things:
- Column 1 (left): the state in the STAGING AREA.
- Column 2 (right): the state in the WORKING TREE.
Grasp that and a cryptic output turns into precise information. The possible codes:
| Code | Name | Meaning |
|---|---|---|
(space) |
unchanged | Nothing to report in that area |
M |
modified | The content differs from the previous version |
A |
added | A new file, already staged |
D |
deleted | The file has been removed |
R |
renamed | The file has been moved or renamed |
C |
copied | The file was created as a copy of another |
U |
updated but unmerged | An unresolved conflict (you will meet it in module 3) |
? |
untracked | Git has never recorded it (shows up as ??) |
! |
ignored | Excluded by a rule (only with --ignored) |
And now the combinations that actually turn up day to day:
| Output | Column 1 | Column 2 | Interpretation |
|---|---|---|---|
M |
M |
|
Modified and staged. It goes in as it stands |
M |
|
M |
Modified but not staged. It will not go in |
MM |
M |
M |
Staged and modified again afterwards. The staged version goes in |
A |
A |
|
A new file, already staged |
AM |
A |
M |
Added, then modified after being added |
D |
D |
|
Deleted, and the deletion is staged |
D |
|
D |
Deleted from disk, but the deletion is not staged |
R |
R |
|
Renamed, staged |
?? |
— | — | Untracked |
UU |
U |
U |
Conflict: both sides modified the file |
Applied to the earlier example:
M styles.css→ modified and staged. It will go in to the commit.A .gitignore→ new and staged. It will go in.M app.js→ modified but not staged. It will not go in. Note the leading space: it carries meaning.D old.js→ deleted from disk, not staged. The commit would still include the file.?? notes.txt→ untracked. It will not go in.
A reading trick. Put a finger over the first column: what is left visible is "what is on disk". Lift the finger: what you see there is "what is going into the commit".
A very useful variant adds the branch information:
The first line sums up the current branch, its tracking branch and how far they have diverged. With -sb you get in five lines everything the long output takes twenty-five to say.
- Files you do not want to version:
.gitignore in two minutes
.gitignore in two minutesThere is a practical problem with Untracked files: most project folders accumulate files that must never be versioned. Installed dependencies, build output, editor temporary files, credentials, operating-system files… If they all show up in every git status, the signal drowns in noise and you end up ignoring the command's output, which is precisely what you do not want.
The solution is a file called .gitignore at the root of the project, with one pattern per line:
# Dependencies node_modules/ # Credentials .env # Logs and temporary files *.log notes.txt # Operating-system files .DS_Store Thumbs.db
Files matching those patterns vanish from git status and cannot be added by accident with git add .. The .gitignore itself is versioned: it is part of the project and must be the same for the whole team, which incidentally settles a long-running clash between the three operating systems of Ana, Bruno and Carla (.DS_Store on macOS, Thumbs.db on Windows).
Two nuances that head off the most common frustration:
.gitignoreonly affects untracked files. If a file is already tracked, adding it to.gitignoredoes not take it out of the project: it has to be removed from the index explicitly, something we will see in Staging and Committing Changes.- Write it early, ideally before the first
git add, as we saw in Creating a Repository.
That gives you everything you need to work. Advanced patterns, negations with !, per-directory .gitignore files, the global one and .git/info/exclude are covered in depth in Ignoring Files with .gitignore.
- A complete working session of Ana's, narrated
Let us follow Ana through a whole afternoon. It is Tuesday and she wants to add a pending-task counter to task-manager. Starting point:
All clean: the three areas match the last commit. That is the right place to start something new from.
17:05 — She edits three files
She adds the counter element in index.html:
She styles it in styles.css:
And she adds the logic in app.js:
function updateCounter() {
const pending = tasks.filter(function (t) { return !t.done; }).length;
document.querySelector('#counter').textContent = pending + ' tasks pending';
}Along the way she starts the project to try it out, which produces a log file, and she opens a notepad with loose ideas in it.
17:40 — First look at the 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: app.js modified: index.html modified: styles.css Untracked files: (use "git add <file>..." to include in what will be committed) debug.log notes.txt no changes added to commit (use "git add" and/or "git commit -a")
Five entries: three modified project files and two new files she does not want to version. That last line reminds her that, as things stand, git commit would do nothing.
17:42 — She strips out the noise
Before staging anything, she writes the .gitignore:
debug.log and notes.txt are gone. The output now carries nothing but signal. Look at the three lines beginning with a space: an empty column 1 means none of that would go into a commit yet.
17:45 — She stages
The Ms and the A have shifted to the first column. All four files are staged. In the long form:
On branch main Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: .gitignore modified: app.js modified: index.html modified: styles.css
The "not staged" section has disappeared: nothing is left outstanding in the working tree.
17:52 — She spots a slip
Trying the page out, Ana sees the counter say "1 tasks pending". She fixes app.js so that the singular works:
function updateCounter() {
const pending = tasks.filter(function (t) { return !t.done; }).length;
const text = pending === 1 ? '1 task pending' : pending + ' tasks pending';
document.querySelector('#counter').textContent = text;
}And she checks again:
There it is, the interesting case: MM app.js. The file shows an M in both columns. It means exactly this:
- Column 1 (
M): there is a staged version ofapp.js— the 17:45 one, without the singular fix. - Column 2 (
M): the file on disk differs from that staged version — it has the fix.
The long form makes it plainer still, because the same file appears twice:
On branch main Changes to be committed: new file: .gitignore modified: app.js modified: index.html modified: styles.css Changes not staged for commit: modified: app.js
If Ana committed now, the version with the slip in it would go in. This is Git's classic trap and the reason it pays to glance at git status right before committing.
17:53 — She stages again
The second column of app.js is blank again. Now the staged version is the good one.
17:55 — She commits
[main 2f6a3c8] Add pending task counter 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .gitignore
Back to the starting point. The three areas agree once more, now over a new commit. And let us confirm that the ignored files are still there, merely invisible to Git:
They are on disk, flagged as ignored. Git sees them and deliberately decides not to bother you with them.
The whole journey in one diagram
sequenceDiagram
participant D as Working<br/>tree
participant I as Staging<br/>area
participant R as Repository
Note over D,R: 17:05 — Ana edits 3 files
Note over D: app.js, styles.css, index.html modified
Note over D,R: 17:42 — She writes .gitignore
Note over D: debug.log and notes.txt drop out of status
D->>I: 17:45 git add (4 files)
Note over D,R: 17:52 — She fixes a slip in app.js
Note over D,I: app.js shows up as MM
D->>I: 17:53 git add app.js
I->>R: 17:55 git commit -m "Add pending task counter"
Note over D,R: working tree clean
- The cycle in daily practice
Once the flow is second nature, day-to-day work boils down to a handful of habits:
The minimal loop, which you will run thousands of times:
git status # where am I?
# ... edit ...
git add <files> # what am I grouping together?
git status # is that exactly what is going in?
git commit -m "..." # record itFour habits that make the difference:
git statusbefore you start. Knowing whether you are setting off from a clean tree or picking up something you left half-done yesterday stops two tasks ending up in one commit.git statusbefore you commit. It is the only moment at which you can catch anMMor a forgotten file. It costs one second.- Commit early and often. A commit is not a delivery or a release: it is a save point. A history of twenty small commits is infinitely more useful than one of two enormous ones.
- End the day with a clean tree whenever you can. If that is not possible, at least leave yourself a note about what you were in the middle of.
What to do when the loop breaks. These are the most frequent situations and the way out; all of them are covered in the next lesson:
| Situation | Command |
|---|---|
| I staged a file I did not mean to | git restore --staged <file> |
| I want to discard my changes to a file | git restore <file> |
| I got the last commit message wrong | git commit --amend |
| I want to see exactly what I changed | git diff (lesson 02-05) |
| I want to see what has been done so far | git log (lesson 02-06) |
Common Mistakes and Tips
- Committing without looking at
git status. TheMMcase — a file staged and then modified again — puts into the history a version that is not the one in front of you. A glance at the status before committing avoids it. - Thinking
git add"saves" the file.git addsaves nothing permanently: it copies the current content into the staging area. Keep editing and that copy goes stale. - Forgetting that
git commit -adoes not include untracked files. The-aoption automatically stages tracked files that have been modified or deleted. A new file has to be added withgit addat least once. - Ignoring the
Untracked filessection. That is where the new file you forgot to add hides — the one that stops the project building on your colleague's machine. - Misreading the two columns of
--short.M(space then M) andM(M then space) mean opposite things. When in doubt, use the long form. - Working with a
git statusfull of noise. If you have forty untracked files you are never going to version, write the.gitignore. A readablestatusis a tool; an unreadable one is an obstacle. - Adding
.gitignoretoo late. If the file is already tracked, ignoring it has no effect. It has to come out of the index first. - Tip: define a short alias for the status, since you will use it constantly:
git config --global alias.s "status -sb". After that,git sis enough. Aliases are covered in Git Log and Aliases. - Tip: if
git statusis slow on a large project, it is usually a symptom of too many untracked files (dependencies, build artefacts). A good.gitignorefixes it.
Exercises
Exercise 1: Read the status without running Git
A repository shows this output:
## main...origin/main [ahead 1] A config.json MM app.js M styles.css D old.css D index.html ?? draft.txt R README.md -> GUIDE.md
Answer, with your reasoning:
- Which files will go into the next
git commit(without using-a), and with what kind of change? - What is going on with
app.js? If it is committed now, which version ends up recorded? index.htmlhas been deleted from disk. Will it disappear from the project when you commit?- How many commits is this branch ahead of the remote?
- Write the sequence of commands that would leave the state ready for everything deleted, modified and renamed to go into the commit, except
draft.txt, which must never be versioned.
Exercise 2: Reproduce the MM case
Create a test repository and deliberately provoke the situation in which an old version of a file gets committed:
- Create a repository with an
app.jsfile containing the lineconst version = 1;and commit it. - Change the line to
const version = 2;and stage the change. - Without committing, change the line to
const version = 3;. - Check the status in both the short and the long form.
- Commit without staging again.
- Work out with commands which version ended up recorded in the history and which one is still on your disk.
- Fix the situation so that the history reflects version 3.
Exercise 3: Clear the noise out of an inherited project
You are handed a project in which git status shows this:
On branch main Untracked files: .env .vscode/settings.json build/index.js build/styles.css dist/package.zip report.log node_modules/ (thousands of files) src/newModule.js temp~
Only src/newModule.js should be versioned. Write:
- The
.gitignorethat leavesgit statusshowing that file alone. - The commands that verify it works, including an explicit check that
.envis being ignored and by which rule. - The sequence that commits the
.gitignoreand the new module in two separate commits, plus a justification for keeping them apart.
Solutions
Solution to Exercise 1
1. What will go into the commit. Everything with something other than a space in the first column:
| File | Code | Goes in as |
|---|---|---|
config.json |
A |
A new file |
app.js |
MM |
Modified (the staged version) |
old.css |
D |
Deleted |
README.md -> GUIDE.md |
R |
Renamed |
What does not go in is styles.css ( M), index.html ( D) and draft.txt (??), because their first column is empty or the file is unknown to Git.
2. app.js is the MM case: there is a staged version and, on top of it, the file on disk differs from that version. Committing will record the staged version, not the one on disk. The difference between the two will remain as an outstanding change after the commit.
3. index.html will not disappear. The code D says the deletion is on disk but not staged. The commit will still contain the file, so anyone cloning the repository will receive it. This is a common mistake: deleting through the file manager and forgetting to record the deletion.
4. One commit ahead, according to [ahead 1]: there is a local commit the remote does not have yet.
5. The sequence:
# 1. Exclude the draft so that it can never sneak in
echo "draft.txt" >> .gitignore
# 2. Stage everything outstanding on tracked files,
# deletions included
git add -u
# 3. Stage the .gitignore, which is a new file
git add .gitignore
# 4. Verify before committing
git status -sThe second column is blank on every line and draft.txt has vanished: exactly what was asked for. The -u option of git add is the key here — it stages modifications and deletions of already tracked files without touching new ones — and we will study it in detail in the next lesson.
Solution to Exercise 2
Steps 1 to 3:
mkdir -p ~/practice/states && cd ~/practice/states
git init
echo "const version = 1;" > app.js
git add app.js
git commit -m "Add the initial version"
echo "const version = 2;" > app.js
git add app.js
echo "const version = 3;" > app.jsStep 4 — the status:
On branch main Changes to be committed: modified: app.js Changes not staged for commit: modified: app.js
The same file appears twice. The long form makes it obvious; the short one condenses it into MM.
Step 5 — commit:
Step 6 — what ended up where:
# What is in the history
git show HEAD:app.js
# → const version = 2;
# What is on your disk
cat app.js
# → const version = 3;
# And there is still work outstanding
git status -s
# → M app.jsConfirmed: version 2 went in, the one that was staged. Version 3 is still in the working tree, unrecorded. This is exactly what ignoring an MM produces.
Step 7 — fixing it:
That is the safest solution: a new commit on top. (There is another option, git commit --amend, which corrects the previous commit instead of adding a new one; it is studied in the next lesson and should only be used on commits that have not been shared yet, because it rewrites the history.)
Solution to Exercise 3
1. The .gitignore:
# Dependencies node_modules/ # Build artefacts build/ dist/ # Local editor configuration .vscode/ # Credentials .env # Logs and temporary files *.log *~
2. Verification:
Exactly two entries: the .gitignore itself and the file we do want to version.
To find out why a particular file is ignored there is a dedicated command:
The output gives the rules file, the line number, the pattern that matched and the file being evaluated. It is the right tool for debugging a .gitignore that "does not work": if the command returns nothing, no rule applies.
And one extra check that puts your mind at rest:
3. Two separate commits:
git add .gitignore
git commit -m "Add .gitignore for deps, artefacts and secrets"
git add src/newModule.js
git commit -m "Add the report generation module"Why keep them apart. They are two entirely unrelated changes: one configures what the project versions, the other adds functionality. Separating them brings concrete benefits:
- Each commit can be described in a single honest sentence.
- If someone has to check tomorrow why
dist/is ignored, they find a commit that talks about only that. - If the new module turns out to be a mistake and has to be undone, it is undone without dragging the
.gitignorealong with it.
This is the idea of the atomic commit, which we develop in the very next lesson.
Conclusion
You now have the complete cycle. To recap:
- The basic flow is three steps — edit, stage, commit — and two commands,
git addandgit commit. The middle step exists to separate the work (what have I changed) from the judgement (what forms one meaningful unit). - The three areas are moved by specific commands:
git addgoes from the working tree to the staging area,git commitfrom the staging area to the repository, andgit restoreundoes in both directions. - A file passes through four states: untracked, unmodified, modified and staged. Only the last three correspond to tracked files, and many commands act on those alone.
- One file can be in two states at once (the
MMcase), and if you fail to spot it you will commit a version that is not the one in front of you. git statusis the compass. In its long form it teaches; in its short form (-s,-sb) it informs at a glance, with two columns separating the staging area from the working tree..gitignorekeeps the signal clean by excluding what should never be versioned, but it acts only on untracked files.
With this you can already work. What is missing is precision: so far we have staged whole files with git add <file>, and that is not always what you want. What if one file holds two changes that belong to different commits? How do you take something back out of the staging area? How do you delete or rename a file inside Git? And what if you get the message wrong on the commit you have just made?
All of that is the subject of the next lesson, Staging and Committing Changes: git add in depth with its -A, -u and . options — a classic source of confusion — and the interactive, hunk-by-hunk mode, how to unstage with git restore, git mv and git rm, and every variant of git commit, including --amend for correcting the last commit.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
