In the previous lesson we walked through the full cycle — edit, stage, commit — and saw how git status guides you at every step. But we used git add and git commit in their most elementary form: staging whole files and committing with a short message.
In practice that runs out of road very quickly. A real afternoon's work does not produce tidy changes: it produces one file with two fixes that have nothing to do with each other, a configuration file that should never have been versioned, another one that needs renaming, and a commit message with a typo in it. Git has a precise answer for every one of those situations, and mastering them is the difference between a history you can read and one you can only endure.
This is the densest lesson in the module. We are going to look at git add in depth — including the difference between -A, -u and ., which trips up almost everybody — the interactive hunk-by-hunk mode, how to unstage, how to move and delete files inside Git, and every useful variant of git commit, --amend included. We will close with the idea that gives all of it a purpose: the atomic commit.
Contents
git add: ways of saying what to stage-Aversus-uversus.: the table that settles it- Staging hunk by hunk with
git add -p - Taking things out of the staging area:
git restore --staged - Discarding working tree changes:
git restore - Moving and renaming with
git mv - Deleting with
git rm(and the special case--cached) git commit: every way of committing--amend: fixing the last commit- The atomic commit
git add: ways of saying what to stage
git add: ways of saying what to stagegit add copies the current content of one or more files into the staging area. It accepts many ways of telling it what to act on:
# A single specific file
git add app.js
# Several files
git add app.js styles.css index.html
# A whole directory, recursively
git add src/
# Everything below the current directory
git add .
# The whole repository, wherever you happen to be inside it
git add -A
# A pattern (quotes are mandatory, see below)
git add "*.css"An important detail about patterns
When you type git add *.css without quotes, it is your shell that expands the asterisk, not Git. The upshot is that only files in the current directory are expanded, and if there are none the command fails.
With quotes, the pattern reaches Git untouched, and Git applies it recursively across the whole repository:
git add "*.css" # every .css in the repository, at any depth
git add *.css # only the .css files in the current folder (the shell expands them)It is a subtle difference with real consequences. The recommendation: use quotes when you want Git's behaviour, and be aware of which one you are asking for.
git add also stages deletions and new files
A common misunderstanding is to think that git add is only for "adding". Its actual job is to synchronise the staging area with the working tree for the paths you name. That covers:
- New files → they start being tracked.
- Modified files → their staged version is updated.
- Files deleted from disk → the deletion is staged.
That last case takes people by surprise:
Yes: git add on a file that no longer exists records its disappearance. It makes sense once you read add as "take note of the current state of this path".
Checking without executing
Two very handy options before a broad add:
In a large or unfamiliar repository, git add -n . costs a second and saves you grief.
-A versus -u versus .: the table that settles it
-A versus -u versus .: the table that settles itThis is, by a distance, the most widespread confusion around git add. The three forms look alike and do different things. The difference comes down to two axes: which kinds of change they include, and which part of the repository they act on.
| Form | New files | Modified files | Deleted files | Scope |
|---|---|---|---|---|
git add . |
Yes | Yes | Yes | Only from the current directory downwards |
git add -A (--all) |
Yes | Yes | Yes | The whole repository, wherever you are |
git add -u (--update) |
No | Yes | Yes | The whole repository |
git add <path> |
Yes | Yes | Yes | That path only |
Two conclusions read straight off the table:
-uis the only one that leaves new files out. It acts exclusively on files Git already tracks. It is the right choice when you want to record all your work on known files without risking adding a new file by accident..and-Ado the same thing apart from the scope. The difference only shows up when you are not at the root of the repository.
A practical demonstration
Let us set the scene in Ana's repository:
cd ~/projects/task-manager
mkdir -p src
echo "// utils" > src/utils.js # new, inside src/
echo "/* new */" >> styles.css # modified, at the root
rm README.md # deleted, at the root
echo "# temporary" > DRAFT.md # new, at the root
git status -sCase A — git add . from the root:
Everything goes in: new, modified and deleted.
Case B — git add . from a subdirectory:
Here is the trap. Only src/utils.js has been staged, because . means "the current directory". The changes at the root are still unstaged. Somebody committing now, convinced they had added everything, would leave three changes out.
Case C — git add -A from that same subdirectory:
-A ignores where you are and acts on the whole repository. It is the behaviour most people believe they are asking for when they type git add ..
Case D — git add -u:
Only changes to already tracked files have been staged: the modification to styles.css and the deletion of README.md. The two new files remain untracked. This is the behaviour you want when you are working in the middle of a folder full of generated files and you do not trust a sweeping add.
A historical note. Before Git 2.0,
git add .did not stage deletions, and that asymmetry caused no end of trouble. From Git 2.0 onwards the behaviour is the one described in the table. If you read old documentation saying otherwise, it is out of date.
Which one to use
git add <path>when you know exactly what you want. It is the default choice of anyone who looks after their history.git add -uto record all your work on known files, with no surprises.git add -Awhen you genuinely want everything and you have a.gitignoreyou trust.git add .only if you are at the root and you know you are at the root.
And in every case: git status before committing.
- Staging hunk by hunk with
git add -p
git add -pThe following Tuesday comes round. Ana wants to implement marking tasks as done, but while she is at it she spots an ugly colour in styles.css and fixes it in passing. By the time she stops, app.js contains two unrelated changes: the new feature, and a small fix to a console message that she also patched on the fly.
Staging app.js whole would mix the two things into the same commit. The answer is hunk mode:
Git walks through the file showing the changes in chunks (hunks) and asking what to do with each one:
diff --git a/app.js b/app.js
index 7b2e8f1..3c9d4a2 100644
--- a/app.js
+++ b/app.js
@@ -12,4 +12,5 @@ function addTask(text) {
tasks.push({ id: Date.now(), text: text, done: false });
renderList();
+ updateCounter();
}
(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?There are a great many possible answers, but in practice five get used:
| Key | Action |
|---|---|
y |
Yes, stage this hunk |
n |
No, leave it unstaged |
s |
Split the hunk into smaller ones |
e |
Edit the hunk by hand, line by line |
q |
Quit interactive mode |
a |
Stage this hunk and all the remaining ones in the file |
d |
Stage neither this one nor any of the remaining ones in the file |
? |
Show the help with every option |
Let us watch Ana's session in full. First hunk: the call to the counter, which is part of the new feature.
Second hunk: the marking function.
@@ -25,4 +26,12 @@ function renderList() {
const li = document.createElement('li');
li.textContent = task.text;
+ li.addEventListener('click', function () {
+ task.done = !task.done;
+ renderList();
+ updateCounter();
+ });
+ if (task.done) {
+ li.classList.add('done');
+ }
list.appendChild(li);
(2/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? yThird hunk: the console message fix, which does not belong in this commit.
@@ -48,4 +57,4 @@ document.querySelector('#new-task').addEventListener('submit', function (event) {
if (field.value.trim() !== '') {
addTask(field.value.trim());
- console.log('task addded');
+ console.log('Task added successfully');
field.value = '';
(3/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? nThe result:
app.js comes out as MM: one part staged (the feature) and another outstanding (the message). Ana can now commit just the first:
And afterwards, in a separate commit, the rest:
Two clean commits where there was a jumble.
s: splitting a hunk
If two independent changes fall inside the same hunk because they sit close together, s breaks it up:
Git can only split when there is at least one unchanged context line between the two blocks. If the changes are literally adjacent, s will not be offered and you will have to use e.
e: editing the hunk by hand
It is the most powerful option and the most intimidating. Git opens the hunk in your editor (the one you set up in Initial Configuration) with instructions at the bottom:
The two rules you need:
- To not stage an added line (
+), delete it from the hunk. - To not stage a removed line (
-), change its-to a space.
Never delete a line starting with -, and never add brand new lines: the patch would stop applying and Git will complain (though it will give you the chance to try again).
Related commands.
-pworks with other commands too:git restore -p,git stash -p,git checkout -p. Andgit add -iopens a full interactive menu, of which-pis only one option. In practice,-pcovers 95% of cases.
- Taking things out of the staging area:
git restore --staged
git restore --stagedYou staged something you did not mean to. The inverse operation is:
An example. Ana stages everything out of habit and realises that DRAFT.md should not have gone in:
DRAFT.md has gone back to being an untracked file. Its content on disk has not been touched: --staged acts on the staging area alone.
The old form: git reset HEAD
You will see a great deal of documentation — and plenty of colleagues — using this:
git reset HEAD <file>
git reset <file> # equivalent, HEAD is the default
git reset # takes EVERYTHING out of the staging areaIt does exactly the same thing. The difference is one of design: git reset is a command with three modes and several meanings, and git checkout was overloaded too. To sort that out, Git 2.23 (2019) introduced two commands with one clear purpose each:
| Task | Modern form | Old form |
|---|---|---|
| Take out of the staging area | git restore --staged <f> |
git reset HEAD <f> |
| Discard changes on disk | git restore <f> |
git checkout -- <f> |
| Switch branch | git switch <branch> |
git checkout <branch> |
The old forms still work and you will see them everywhere. Use the modern ones in your own work and recognise the old ones when you read them. Using git reset to move HEAD and rewrite history is another matter altogether, covered in Undoing Changes.
- Discarding working tree changes:
git restore
git restoreWithout --staged, git restore does something a good deal more serious: it overwrites the file on disk with the reference version, throwing your changes away.
Ana experiments with a design in styles.css, hates the result and wants to go back:
The file is back as it was. The changes are gone for good: they were in no commit and in no staging area, so Git holds no copy of them. This is one of the few Git commands that destroys work with no safety net.
A serious warning. Before a
git restore, ask yourself whether you really want to throw that work away. If you are unsure, there are alternatives that keep it:git stash(lesson 05-04) or simply committing it and deciding later.
The combination matters
When a file is in the MM state — staged and then modified — you need to be clear about what each variant discards:
| Command | Staging area | Working tree |
|---|---|---|
git restore --staged f |
Restored from HEAD |
Untouched |
git restore f |
Untouched | Restored from the staging area |
git restore --staged --worktree f |
Restored from HEAD |
Restored from HEAD |
git restore --source=HEAD~2 f |
— | The version from 2 commits ago is put in place |
Look closely at the second row: a plain git restore f restores from the staging area, not from the last commit. If you had staged an intermediate version, that is the one you go back to.
Useful options:
# Discard changes to EVERY file in the repository (dangerous)
git restore :/
# Discard hunk by hunk, reviewing each one
git restore -p styles.css
# Bring an old version of a file back to disk
git restore --source=HEAD~3 app.jsThat last one is not an "undo": it changes your working tree to hold old content, which you can then stage and commit as normal. It is a simple way of recovering the state of one particular file without disturbing the rest of the project.
- Moving and renaming with
git mv
git mvAna has created a file called release-notes.md and wants it to be called CHANGES.md. She could do it from the file browser, but then Git would see one deleted file and one new one. The right way is:
The R code marks a rename, and the change is already staged: git mv stages the result automatically.
git mv is a shortcut
The command is not magic. It is exactly equivalent to three instructions:
In fact, had you used a plain mv, Git would have detected the rename anyway once both changes were staged:
This connects with something fundamental about the data model we saw in 01-04: Git does not store renames. A blob holds content, and the name lives in the tree. What Git does is detect renames after the fact, by comparing the content of the deleted and added files. If the content is identical, detection is infallible; if you changed the file a great deal while moving it, Git may miss it and show a deletion plus a new file.
Common uses
# Rename
git mv styles.css main-styles.css
# Move into a subdirectory (which must exist)
mkdir css
git mv styles.css css/styles.css
# Move several files into a directory
git mv app.js utils.js src/
# Force the destination to be overwritten
git mv -f draft.md CHANGES.mdAnd the awkward case: changing only upper and lower case on macOS or Windows, where the filesystem does not distinguish between guide.md and GUIDE.md. Since Bruno works on macOS, he will run into it:
The answer is an intermediate step:
Or use -f, which in recent versions of Git handles this particular case.
- Deleting with
git rm (and the special case --cached)
git rm (and the special case --cached)To remove a file from the project:
It does two things at once: it deletes the file from disk and stages the deletion. It is the equivalent of rm old.js && git add old.js.
If Git spots that the file has uncommitted changes, it refuses:
error: the following file has local modifications:
app.js
(use --cached to keep the file, or -f to force removal)The refusal is deliberate: it warns you that you are about to lose work. If you are sure, -f.
git rm --cached: the case that really matters
This variant removes the file from version control but leaves it on disk. It is the answer to a very frequent problem: a file that should never have been versioned and that is already tracked.
Remember section 6 of the previous lesson: .gitignore only acts on untracked files. If config.local.json is already in the history, adding it to .gitignore achieves nothing. The correct sequence is:
# 2. Make sure it does not come back
echo "config.local.json" >> .gitignore
# 3. Commit both things
git add .gitignore
git commit -m "Stop versioning the local environment config"ls config.local.json
# → config.local.json ← still there, untouched
git status -s
# → (empty: it is ignored now)For a whole directory you need -r (recursive):
A very handy trick once you have fixed the .gitignore and want it applied to everything already tracked:
git rm -r --cached . # stop tracking everything (deleting nothing from disk)
git add . # add it all back, now honouring the .gitignore
git status -s # check what got left out
git commit -m "Apply .gitignore to already versioned files"An important warning.
git rm --cachedstops tracking a file from now on, but does not remove it from the history. Every earlier commit still contains it, and anyone with access to the repository can recover it. If the file held credentials, this does not protect them: you have to rotate them and, if there is no way round it, rewrite the history. We cover that in Security Best Practices.
Deletion at a glance
| Command | File on disk | Tracked by Git |
|---|---|---|
rm f (the system one) |
Deleted | Still tracked; deletion unstaged |
git rm f |
Deleted | Deleted, staged |
git rm --cached f |
Kept | Stops being tracked |
git rm -f f |
Deleted even with changes | Deleted, staged |
git rm -r --cached dir/ |
Kept | The whole directory stops being tracked |
git commit: every way of committing
git commit: every way of committingWith the staging area ready, it is time to record. The form you already know:
And these are the variants worth knowing.
Without -m: the editor
Git opens your editor with a template:
# Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # On branch main # Changes to be committed: # modified: app.js # modified: styles.css #
You type at the top, save and close. If you save without typing anything, the commit is cancelled. Lines starting with # are dropped.
This form has two advantages over -m: it shows you what you are about to commit while you write the message (one last chance to catch a mistake), and it makes multi-line messages easy to write.
Multi-line messages
A commit message has two parts: a first summary line and, optionally, a body separated by a blank line.
From the editor it comes naturally. From the command line there are two ways:
# With several -m: each one is a paragraph, separated by a blank line
git commit -m "Mark tasks as done on click" \
-m "The state is kept in the in-memory array; reloading the page loses it. Persistence will be tackled once local storage exists."# With a real line break (Bash and Zsh)
git commit -m "Mark tasks as done on click
The state is kept in the in-memory array; reloading the
page loses it."The result is identical. Many commands — git log --oneline, the commit lists on web platforms — show only the first line, so that line has to work on its own.
-a: staging and committing in one go
git commit -a -m "Adjust the secondary text colour"
git commit -am "Adjust the secondary text colour" # short formIt automatically stages every tracked file that is modified or deleted, and commits.
Its risk is that it skips the staging area — which is to say, it skips the decision about what goes in. With -a you commit everything you have touched, including:
- The debugging
console.logyou left inapp.js. - The three-line tweak in a file that has nothing to do with what you are committing.
- The change you were going to look at more carefully before recording it.
On top of that, it does not include untracked files, which produces the opposite mistake: you think you have committed everything, the new file stays out, the project does not build on your colleague's machine and nobody can work out why.
git add + git commit |
git commit -a |
|
|---|---|---|
| New files | Included if you add them | Never included |
| Modified files | The ones you pick | All of them |
| Deleted files | The ones you pick | All of them |
| Control over the content | Total | None |
| Risk of a jumbled commit | Low | High |
Use it when you are certain that everything you modified belongs to the same commit, and even then look at git status first. As a default habit, I do not recommend it.
Other useful options
# See the diff of what you are committing while you write the message
git commit -v
# Commit even with nothing staged (useful in scripts and for marking milestones)
git commit --allow-empty -m "Trigger the deployment"
# Set a specific authoring date
git commit --date="2026-07-15 10:00:00" -m "..."
# Commit on someone else's behalf (pair work)
git commit --author="Bruno Salas <bruno.salas@example.com>" -m "..."
# Sign the commit cryptographically
git commit -S -m "..."git commit -v deserves a comment of its own: it appends the full diff of what is staged to the foot of the template. It is the best way of reviewing your own work just before recording it, and many developers turn it on permanently:
--amend: fixing the last commit
--amend: fixing the last commitAna commits and, a second later, spots the typo in the message:
The fix:
git commit --amend -m "Mark tasks as done on click"
git log --oneline -1
# → 5c2e8b4 Mark tasks as done on clickSorted. Notice that the hash has changed: it was 9d1e4b7 and it is now 5c2e8b4.
--amend is good for three things:
# 1. Change the message only
git commit --amend -m "Corrected message"
# 2. Add a forgotten file to the last commit
git add file-i-forgot.js
git commit --amend --no-edit # --no-edit keeps the current message
# 3. Fix the authorship
git commit --amend --author="Ana Ferrer <ana.ferrer@example.com>" --no-editThe second case is the most useful day to day: you have just committed and discover a file was missing. Instead of creating a "Add the missing file" commit, you fold it into the previous one and the history stays clean.
--amend rewrites history
This is the part to understand properly, and it ties directly back to The Git Data Model.
--amend does not modify the existing commit. It cannot: Git's objects are immutable, and their identifier is the hash of their content. What it does is:
- Create a new commit with the same parent as the old one and the corrected content and message.
- Move the current branch to that new commit.
- Leave the old commit orphaned, with nothing pointing at it.
graph LR
subgraph "Before"
A1["c5d9b1e"] --> B1["2f6a3c8"] --> C1["9d1e4b7<br/>(message with a typo)"]
M1["main"] -.-> C1
end
subgraph "After --amend"
A2["c5d9b1e"] --> B2["2f6a3c8"] --> C2["9d1e4b7<br/>(orphaned)"]
B2 --> D2["5c2e8b4<br/>(corrected message)"]
M2["main"] -.-> D2
end
Hence the golden rule:
Never
--amenda commit you have already shared.
If commit 9d1e4b7 was already on the server and Bruno had fetched it, rewriting it makes your history and his diverge: he has a commit you no longer have, and your new commit is unknown to him. Fixing that takes work and coordination. As long as the commit lives only on your machine, --amend is safe and highly recommended.
The general rule — rewrite local work freely, never shared work — applies just as much to rebase (module 5) and to every history-cleaning technique (08-02).
- The atomic commit
Everything above exists for one purpose: to let you create atomic commits.
An atomic commit is one that contains one complete change and only one. Two conditions at once:
- Complete: it includes everything needed for that change to work. If you add a function and its styling, both go together; a commit that leaves the project broken is not atomic.
- Single: it includes nothing that does not belong to that change. Not the spelling fix you made in passing, and not the debugging
console.log.
The minimum rule
Here is the only rule you need for now, and it doubles as an atomicity test:
If describing your commit requires the word "and", it should probably be two commits.
Applied to Ana's case: "Mark tasks as done and fix the console message and adjust the colour". Three implicit "and"s, three commits. That is why she reached for git add -p.
Why it is worth the effort
| With atomic commits | With jumbled commits |
|---|---|
git log tells the real story of the project |
The history is a list of "various changes" |
| Undoing one particular change is trivial | Undoing drags in things you did not want |
git bisect finds the exact guilty commit |
The guilty commit contains ten changes |
| Code reviews are quick | Nobody wants to review 40 mixed-up files |
git blame genuinely explains each line |
git blame points at "Friday's changes" |
The benefits are collected months later, when something breaks and you have to work out why. Tools such as Git Bisect and Git Blame depend entirely on this discipline.
About the messages
An atomic commit needs a message that describes it. For now, three rules will do:
- A short first line (around 50 characters), descriptive on its own.
- In the imperative, describing what the commit does: "Add the task counter", not "Added the counter" and certainly not "changes".
- The what on the first line, the why in the body if it needs explaining.
The full conventions — Conventional Commits, lengths, body, trailers, issue references — are covered in Writing Good Commit Messages. With these three rules you will already write better messages than average.
Common Mistakes and Tips
- Using
git add .from a subdirectory thinking it adds everything. It only adds from there downwards. If you want the whole repository, it isgit add -A. - Expecting
-uto include new files. It does not, by design. That is its advantage, not its flaw. - Trusting
git commit -a. It commits everything modified with no filter and leaves the new files out. It is the recipe for jumbled commits and forgotten files. - Reaching for
git restore <file>lightly. It destroys your changes with no backup. It is one of the very few Git commands with no safety net. If you are unsure,git stash. - Confusing
git restore --stagedwithgit restore. The first takes things out of the staging area and does not touch the disk; the second overwrites the disk. The difference is between an undone change and a lost one. - Believing
git rm --cachedremoves the file from the history. It does not: it remains in every earlier commit. For secrets, this is not enough. - Running
--amendon a commit you have already shared. It rewrites history and causes divergence with your colleagues. Only on work still sitting on your machine. - Committing three days' work in a single commit. Nobody can review it, undo it or understand it. Commit early and often.
- Tip: turn on
git config --global commit.verbose true. Seeing the diff while you write the message catches a huge number of mistakes before they reach the history. - Tip: if
git add -pfeels slow at first, stick with it for a week. It is the habit that improves the quality of a history most, and it ends up being automatic. - Tip: when you are unsure what you are about to commit,
git diff --stagedshows you exactly. That is the subject of the next lesson.
Exercises
Exercise 1: Mastering -A, -u and .
Set up this scenario:
mkdir -p ~/practice/add-test/components && cd ~/practice/add-test
git init
echo "initial" > root.txt
echo "initial" > components/button.js
git add -A && git commit -m "Initial state"
echo "change" >> root.txt
echo "change" >> components/button.js
echo "new" > root-new.txt
echo "new" > components/menu.js
rm root.txtPut yourself inside components/ and, running git reset between attempts to get back to the starting point, work out exactly what each of these commands stages. Write down the output of git status -s in each case and explain why:
git add .git add -Agit add -ugit add ..
Exercise 2: Separating two changes in the same file
In the task-manager repository (or in a test one with a similar app.js), make these two changes at the same time in app.js:
- Change A (feature): add a
deleteTask(id)function that removes a task from the array and redraws the list. - Change B (maintenance): fix a misspelt comment in the file's header.
Then:
- Use
git add -pto stage only change A. - Show with
git status -sthat the file is in theMMstate. - Commit change A with the message "Add task deletion to the list".
- Commit change B as a second commit.
- Check with
git log --onelinethat there are two commits, and withgit show --statthat each one touches what it should.
Exercise 3: Rescuing a badly staged repository
A colleague hands you a repository in this state, just after committing:
.env | 3 + app.js | 24 ++++++++--- styles.css | 6 ++-- node_modules/lib/a.js | 99 +++++++++++++++++ README.md | 2 +- 5 files changed, 128 insertions(+), 6 deletions(-)
The commit has not been shared with anybody yet. Sort it out:
- What are the three problems with this commit?
- Write the sequence of commands that stops versioning
.envandnode_modules/, adds them to the.gitignoreand keeps both files on disk. - Replace the commit message with a descriptive one, without creating an extra commit.
- What would have changed in your answer if the commit had already been shared? And what extra step would be needed because
.envwas versioned at all?
Solutions
Solution to Exercise 1
The starting state (seen from the root):
1. git add . from components/:
It stages everything (new, modified and deleted) but only from the current directory downwards. The changes at the root stay out.
2. git add -A:
It stages the whole repository, completely ignoring where you are. All four entries have their first column filled in.
3. git add -u:
It acts on the whole repository, but only on tracked files: the modification to button.js and the deletion of root.txt. The two new files remain untracked.
4. git add ..:
The same result as -A, but for a different reason: .. is the root of the repository, so "from there downwards" is everything. It confirms that the difference between . and -A is purely the starting point of the walk.
In summary:
Command (from components/) |
button.js |
menu.js |
root.txt |
root-new.txt |
|---|---|---|---|---|
git add . |
Yes | Yes | No | No |
git add -A |
Yes | Yes | Yes | Yes |
git add -u |
Yes | No | Yes | No |
git add .. |
Yes | Yes | Yes | Yes |
Solution to Exercise 2
The two changes in app.js. The header (change B):
And the new function (change A):
function deleteTask(id) {
const index = tasks.findIndex(function (t) { return t.id === id; });
if (index !== -1) {
tasks.splice(index, 1);
renderList();
updateCounter();
}
}1. Staging change A only:
The first hunk Git shows is the header one (it sits higher up the file):
@@ -1,3 +1,3 @@
-// task-manager — mian logic
+// task-manager — main logic
const tasks = [];
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? nWe answer n: it does not belong in this commit. The second one is the function:
@@ -30,6 +30,15 @@ function renderList() {
+function deleteTask(id) {
+ const index = tasks.findIndex(function (t) { return t.id === id; });
...
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? y2. Checking the MM state:
First column M: there is a staged version (with the new function, without the comment fix). Second column M: the disk differs from it.
You can verify it more precisely:
Nine staged lines (the function) and one outstanding line (the comment). Exactly the separation we were after.
3 and 4. The two commits:
git commit -m "Add task deletion to the list"
# → [main 4e7f2a9] Add task deletion to the list
# → 1 file changed, 9 insertions(+)
git add app.js
git commit -m "Fix a typo in the header comment"
# → [main 8a1d5c3] Fix a typo in the header comment
# → 1 file changed, 1 insertion(+), 1 deletion(-)5. Verification:
git show --stat 4e7f2a9 | tail -2
# → app.js | 9 +++++++++
# → 1 file changed, 9 insertions(+)
git show --stat 8a1d5c3 | tail -2
# → app.js | 2 +-
# → 1 file changed, 1 insertion(+), 1 deletion(-)Two atomic commits on the same file. Without git add -p the only way to get there would be to undo one change by hand, commit, and type it back in.
Solution to Exercise 3
1. The three problems:
.envis versioned. It holds credentials; they are now in the history and visible to anyone with access to the repository.node_modules/is versioned. Those are reinstallable dependencies: they bloat the repository, clutter the diffs and add nothing.- The message "various fixes" says nothing. And underneath it, the commit is not atomic: it mixes logic (
app.js), styling (styles.css) and documentation (README.md) with files that should not be there at all.
2. Stop versioning .env and node_modules/:
--cached is the key: the files stay on disk, they merely stop being tracked.
Checking that the files are still there:
3. Fixing the message without creating a new commit.
Since the changes from step 2 are staged and the commit has not been shared, they can be folded into that same commit with --amend, fixing the message along the way:
[main 3b9e7d1] Add task deletion and adjust the list styles 4 files changed, 29 insertions(+), 6 deletions(-)
.gitignore | 2 ++ app.js | 24 ++++++++--- styles.css | 6 ++-- README.md | 2 +- 4 files changed, 29 insertions(+), 6 deletions(-)
.env and node_modules/ have vanished from the commit, and the message now describes something. The hash has changed (7f3c9a2 → 3b9e7d1) because, as we know, --amend creates a new commit.
A critical note: this
--amendworks cleanly because7f3c9a2was the only commit containing.env. Had the file been in twenty commits,--amendwould only pull it out of the last one and it would still sit in the other nineteen.
About the message: it still has an "and" in it, so the truly correct move would have been to split the commit in two (feature and styling). A git reset and a couple of git add -p runs would do it, but that is already the territory of Keeping a Clean History.
4. If the commit had already been shared.
Two things would change:
- No
--amend. Rewriting a commit others already hold causes histories to diverge. The correct answer would be a new commit on top:
git rm --cached .env
git rm -r --cached node_modules/
git add .gitignore
git commit -m "Stop versioning credentials and dependencies"The bad message on the earlier commit stays where it is. That is the price of having published: shared history is immutable in practice.
- And, above all, the credentials have to be rotated. This is the point you cannot skip past:
.envwas in the history and, if that history was shared, the keys count as compromised. Untracking the file does not protect them; anyone who cloned the repository has them on disk, andgit show 7f3c9a2:.envprints them out. The only valid response is to invalidate those credentials and generate new ones. Cleaning the history (withgit filter-repoor similar) is a complementary step, never a substitute. We will look at it in Security Best Practices.
Conclusion
This lesson has given you fine control over what enters the history and how. To recap:
git addsynchronises paths with the staging area, and that covers new, modified and deleted files.-A,-uand.differ along two axes:-uis the only one that excludes new files, and.is the only one limited to the current directory.-Ais "everything, everywhere".git add -pstages hunk by hunk and is the tool that lets you separate two changes tangled together in the same file. It is the habit that improves a history most.git restore --stagedtakes things out of the staging area without touching the disk;git restoreoverwrites the disk and destroys your changes with no safety net. Their old equivalents aregit reset HEADandgit checkout --.git mvandgit rmperform the filesystem operation and stage it in one step.git rm --cachedstops tracking while keeping the file, and it is the missing piece whenever a.gitignore"does not work".git committakes a message inline or in the editor, supports multi-line messages, and offers-a, which is convenient and risky: it commits everything modified and leaves everything new out.--amendfixes the last commit by creating a new one and discarding the old. It is safe and very useful as long as that commit has not been shared.- The atomic commit — one complete change and only one — is the goal of everything above. The minimum rule: if you need "and" to describe it, split it.
Ana can now choose precisely what she commits. But there is a piece we have used without explaining it: in -p mode, Git showed her the changes in a format with + and - lines, @@ headers and hunks. That format turns up everywhere in Git, and being able to read it is essential.
In the next lesson, Inspecting Changes with git diff, we will learn to see exactly what has changed before committing: the difference between git diff, git diff --staged and git diff HEAD in terms of which areas they compare, how to read the unified diff format line by line, how to compare two specific commits or a single file, and the options that make the output readable in the difficult cases.
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
