The previous module ended with a list of disasters waiting to happen: the reset --hard over three days of work, the commit on the wrong branch, the pull onto a diverged branch, the deleted branch that did matter, the corrupted .git/index. This module is the manual for when they happen.
And they always happen at the worst possible moment. It is seven o'clock on a Friday evening, Carla has to deliver GT-241 before close of play, she runs a command she does not quite understand and the terminal replies with something she has never seen before. What most people do at that point — trying random commands lifted from a forum answer — is exactly what turns a small problem into a big one.
This lesson is two things. First, a calm protocol: what to do before touching anything. And then the emergency catalogue: a large table of symptom → probable cause → where it is solved, which works as an index to the rest of the module. The trivial cases are solved right here; the ones with substance are referred to their own lesson.
Let us start with the idea that underpins everything else, because it is the one that makes it possible to work calmly.
Contents
- The idea that underpins everything: in Git almost nothing is lost
- The calm protocol: four commands before touching anything
- The ten-second backup
- The emergency catalogue
- The trivial cases, solved here
- How to read a Git error message
- What NEVER to do in a state of panic
- The idea that underpins everything: in Git almost nothing is lost
Recall the data model from lesson 01-04. A commit is an immutable object in .git/objects, identified by the hash of its content. A branch is a 41-byte file containing a hash (lesson 03-01). And the relationship between the two is the key:
Commits do not belong to branches. Branches simply point at commits.
From that follows the fact that makes this module habitable: when you delete a branch, you delete no commits. When you run reset --hard, you delete no commits. When a rebase goes wrong, the original commits are still there. The only thing that has changed is that no reference points at them: they have become unreachable.
flowchart LR
subgraph before["Before reset --hard HEAD~2"]
A1["A"] --> B1["B"] --> C1["C"] --> D1["D"]
R1(["main"]) -.-> D1
end
subgraph after["After"]
A2["A"] --> B2["B"] --> C2["C (unreachable)"] --> D2["D (unreachable)"]
R2(["main"]) -.-> B2
end
before ==> after
C and D still exist, byte for byte, in .git/objects. Nobody has touched them. It is simply that main no longer reaches them, and that is why git log does not show them. Recovering them is a matter of pointing something at them again, and that is what the reflog is for, which is the subject of lesson 09-04.
Unreachable objects do eventually disappear, but not immediately: git gc (lesson 08-06) removes them once they have gone more than two weeks without references, and reflog entries expire after 90 days (30 for unreachable ones). In practice, you have weeks to recover any commit.
The exception, and it is the important one
All of the above rests on one condition: that the work made it as far as being a commit. Whatever was never committed is not in the object database and so there is nothing to recover.
| State of the work | Is it in .git/objects? |
Recoverable? |
|---|---|---|
| Committed (even if unpublished) | Yes | Yes, always, as long as gc has not purged it |
Staged with git add (not committed) |
Yes, as a loose blob | Yes, with git fsck (09-04) |
In git stash |
Yes, as hidden commits | Yes, even after stash drop (09-04) |
| Modified in the working directory | No | No. Only your editor can save you |
| Untracked file | No | No |
That table is the mental map of the whole module. Note the detail in the second row: git add already stores the content in the database. A file you staged and then destroyed with git checkout is recoverable, even though you never committed it.
From this comes the most profitable piece of advice in the module, and it is not a command:
Commit early and often. An ugly commit with the message "wip" protects you from everything that is coming in the next five lessons. You can always tidy it up with
rebase -i(lesson 05-02) before publishing.
- The calm protocol: four commands before touching anything
When something goes wrong, the temptation is to act. Resist it. The following four commands modify nothing, take three seconds and, in most cases, the diagnosis is already done by the time you finish reading them.
Step 1: git status
It is the most underrated command in Git. It does not only say which files have changed: it says what state the repository is in and, very often, it literally suggests the way out.
interactive rebase in progress; onto 7d3a8f4 Last command done (2 commands done): pick e91d4a8 Extract task element creation squash b52c9d1 Fix the counter Next commands to do (3 remaining commands): pick 4f8a2e6 Add the pending filter pick 9c4e7b2 Document the filter in the README (use "git rebase --edit-todo" to view and edit) You are currently rebasing branch 'GT-241' onto '7d3a8f4'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch)
It is all there: which operation is half done, how far along it is, and the three ways out. Many people in a panic do not run git status precisely when they need it most.
The states it can announce:
What git status says |
What it means | Immediate way out |
|---|---|---|
HEAD detached at 4f8a2e6 |
You are outside any branch (03-02) | git switch - or git switch -c new-branch |
You have unmerged paths |
Merge conflict half done (03-05) | Resolve it, or git merge --abort |
interactive rebase in progress |
Interactive rebase half done (05-02) | git rebase --continue / --abort |
You are currently cherry-picking |
Cherry-pick half done (05-03) | git cherry-pick --continue / --abort |
You are currently reverting |
Revert half done (05-06) | git revert --continue / --abort |
You are currently bisecting |
Bisect under way (06-02) | git bisect reset |
Your branch and 'origin/x' have diverged |
Divergence with the remote | Lesson 09-03 |
nothing to commit, working tree clean |
There is nothing unsaved | Whatever is lost, if anything, is in commits |
Step 2: git log --oneline --graph -20
The shape of the history visible from where you are now. It serves two purposes: confirming that you are where you think you are, and seeing whether the commit you are after is still reachable.
* 9c4e7b2 (HEAD -> GT-241) Add the pending filter * b52c9d1 Extract task element creation * 4f8a2e6 (origin/main, main) Merge GT-238 |\ | * 7d3a8f4 Fix the task counter |/ * e91d4a8 Document installation in the README
And a variant worth keeping to hand when you suspect something is missing:
# EVERYTHING reachable from any reference, remote ones included
git log --oneline --graph --decorate --all -30If the commit you are looking for shows up in --all but not in the normal log, it is not lost: it is on another branch. That is the most frequent case of all, and it requires no recovery whatsoever.
Step 3: git reflog -20
The record of where HEAD has been. It is the repository's black box and it answers the question "what on earth did I just do?".
9c4e7b2 HEAD@{0}: reset: moving to HEAD~2
b52c9d1 HEAD@{1}: commit: Add the pending filter
4f8a2e6 HEAD@{2}: commit: Extract task element creation
7d3a8f4 HEAD@{3}: checkout: moving from main to GT-241
e91d4a8 HEAD@{4}: pull: Fast-forwardIt reads from the bottom up in chronological order. The HEAD@{0} entry is always the last thing that happened, and here it says clearly that there was a reset that left two commits behind. The hashes of those commits are still written there, and that is exactly what makes it possible to recover them.
The reflog is the entire subject of lesson 09-04. Here it is enough to run it and read it: in 80 % of scares, the answer is in those twenty lines.
Step 4: git fsck --no-progress, only if the above does not add up
It checks the integrity of the object database. Do not run it routinely: it is slow in large repositories and its output is more alarming than it should be (dangling objects are normal and harmless). Save this step for when Git refuses to do basic things or gives errors about objects. Its full interpretation is in lesson 09-05.
The protocol, as an alias
It is worth having it as an alias (lesson 06-04):
git config --global alias.panic '!f() {
echo "=== STATUS ==="; git status;
echo; echo "=== LOG ==="; git log --oneline --graph --decorate -20;
echo; echo "=== REFLOG ==="; git reflog -20;
}; f'Three seconds, zero modifications, and almost always the complete diagnosis.
- The ten-second backup
If after the calm protocol the situation is still unclear, make a copy before attempting anything. It is the difference between "I have a problem" and "I have a problem and I have also made things worse".
On Windows (PowerShell):
Three important details:
cp -a(or-Rpreserving links) copies the entire directory,.gitincluded. That is what makes it a real copy rather than a partial clone.- Do not use
git clonefor this. A clone does not take the reflog, or the stashes, or the unpublished branches, or the uncommitted changes. Precisely what you need to preserve. - Do it before running any command that starts with
git reset,git clean,git rebaseorgit push --force.
When the problem is sorted, delete the copy. When it is not, you will be glad you have it.
For proper backups, in a real format rather than with cp, there is git bundle, which is covered in lesson 09-05.
- The emergency catalogue
This is the central table of the lesson. Find yourself in the left-hand column.
| Symptom | Probable cause | Where it is solved |
|---|---|---|
| I have made changes on the wrong branch (not committed yet) | git switch carries uncommitted changes across, but you forgot to switch branch before starting |
Here, section 5.1 |
| I have committed on the wrong branch | The same, but you have already run commit |
Here, section 5.2 |
| The last commit has the wrong author | user.email misconfigured in this repository (01-05) |
Here, section 5.3 |
| Several commits have the wrong author | Configuration set wrongly some time ago | 09-02 (rebase -i) or filter-repo (08-05) |
| I have forgotten a file in the last commit | An incomplete git add |
Here, section 5.4 |
fatal: Unable to create '.git/index.lock': File exists |
A Git process died without cleaning up, or there is another Git running | Here, section 5.5 |
detected dubious ownership in repository |
The directory belongs to another user (typical in WSL, Docker, external drives) | Here, section 5.6 |
! [rejected] main -> main (non-fast-forward) |
The remote has commits you do not have | 09-03 |
Your branch and 'origin/main' have diverged |
Both sides have moved on since the common ancestor | 09-03 |
hint: You have divergent branches and need to specify how to reconcile them |
pull without a configured policy |
09-03 |
fatal: refusing to merge unrelated histories |
Two histories with no common ancestor | 09-03 |
I have run push --force and deleted a colleague's work |
Rewriting published history (the golden rule, 05-01) | 09-03 and 09-04 |
You are in 'detached HEAD' state |
HEAD points at a commit and not at a branch (03-02) |
Here, section 5.7 |
I have run git reset --hard and lost commits |
The branch moved; the commits are still there | 09-02 (the concept) and 09-04 (the rescue) |
I have run git reset --hard and lost uncommitted changes |
They never reached the database | 09-02. Bad news: it is almost never recoverable |
I have deleted a branch with -D and I needed it |
The reference was deleted; the commits were not | 09-04 |
| A rebase has gone wrong and I do not know how things were before | Local rewriting | 09-04 (ORIG_HEAD and the reflog) |
I have run stash drop on something I needed |
The stash reference was deleted | 09-04 |
A file I deleted reappears on every pull |
It is still in the remote's history: somebody keeps bringing it back | 09-02 and 09-03 |
A file is in .gitignore and Git still sees it |
It was already tracked: .gitignore only affects untracked files (08-03) |
09-06 (check-ignore -v) |
Permission denied (publickey) |
SSH key missing, not loaded or not registered (04-03) | Here, section 5.8, and 09-06 to trace it |
| I am halfway through a conflict and I do not know how to get out | Interrupted merge, rebase or cherry-pick | 03-05; quick exit in section 5.9 |
| The whole file shows up as modified and I only changed one line | CRLF/LF line endings (08-04) | 09-06 (ls-files --eol, check-attr) |
error: object file ... is empty or fatal: bad object |
Genuine corruption of the object database | 09-05 |
Git refuses to do anything and git status fails |
Corrupted .git/index, or broken HEAD |
09-05 |
git status takes several seconds |
It is not a fault: it is performance (08-06) | 08-06 |
| I do not understand why Git does what it does | Unexpected configuration, attributes, hooks | 09-06 |
A tip on using it: before searching the internet, search this table. Forum answers tend to be stripped of context and many of them start with git reset --hard, which is exactly what you should not run without understanding it.
- The trivial cases, solved here
These do not need a lesson of their own. They are one-minute problems, provided you know which command to use.
5.1. Uncommitted changes on the wrong branch
Ana has spent half an hour working on app.js and discovers that she is on main, not on GT-241.
You have lost nothing. Uncommitted changes belong to no branch: they live in the working directory and in the index. Switching branch takes them with you.
git status # confirm there are only modifications, no new commits
git switch -c GT-241 # create the branch here and take the changes along
git statusThe changes are intact, now on the right branch, and main has not moved.
If the branch already exists:
Git takes the changes across without further ado, unless one of the modified files differs between the two branches. In that case it warns you:
error: Your local changes to the following files would be overwritten by checkout: app.js Please commit your changes or stash them before you switch branches.
The way out is the stash from lesson 05-04:
5.2. You have already committed on the wrong branch
Bruno has made two GT-247 commits directly on main, and main has not yet been published with them.
You have lost nothing. The commits exist; the only thing set wrongly is what each branch points at. The recipe is: mark the spot here, and put the branch back where it belongs.
b52c9d1 (HEAD -> main) GT-247 show the number of pending tasks 4f8a2e6 GT-247 add the counter to the DOM e91d4a8 (origin/main) Document installation in the README
# 2. Create the correct branch HERE (without switching: it just plants a marker)
git branch GT-247
# 3. Put main back where the remote was
git reset --hard origin/main
# 4. Move over to the good branch
git switch GT-247
git log --oneline -3b52c9d1 (HEAD -> GT-247) GT-247 show the number of pending tasks 4f8a2e6 GT-247 add the counter to the DOM e91d4a8 (origin/main, main) Document installation in the README
The order matters: create the branch first, and only then move main. That way the commits never stop being reachable and the reset --hard is completely safe. (If you get the order wrong, it is not the end of the world either: that is what the reflog in 09-04 is for. But do it properly.)
If the commits had already been published on main, the answer is not this one but git revert (lesson 05-06), because you would be rewriting public history.
Variant: if it is some scattered commits rather than the last few, the tool is git cherry-pick (lesson 05-03).
5.3. The commit has the wrong author
Carla has set up her new laptop and has committed as carla@old-laptop.local.
For the last commit, unpublished:
# First, fix the cause, or it will happen again
git config user.name "Carla Vidal"
git config user.email "carla.vidal@example.com"
# And now the commit
git commit --amend --author="Carla Vidal <carla.vidal@example.com>" --no-editA nuance that confuses a lot of people: Git stores two identities per commit.
| Field | Who it is | When it changes |
|---|---|---|
Author (%an/%ae) |
Whoever wrote the change | Preserved through rebase and cherry-pick |
Committer (%cn/%ce) |
Whoever created this commit object | Updated on every rewrite |
--author only touches the first. For the second, Git uses user.email at the moment of creating the commit, so it is enough to have corrected the configuration before the --amend. If you need to force it:
GIT_COMMITTER_NAME="Carla Vidal" GIT_COMMITTER_EMAIL="carla.vidal@example.com" \
git commit --amend --no-editAnd the prevention, which is worth more than the cure (lesson 01-05):
# Make Git refuse to commit if there is no explicit identity in the repository
git config --global user.useConfigOnly trueWith that, a repository without its own user.email gives an error instead of inventing an identity from the machine name. It is the setting that avoids this problem for good.
If it is several commits, rewriting is needed: git rebase -i with exec (lesson 05-02) for a handful, or git filter-repo --mailmap (lesson 08-05) for an entire history. And always with the golden rule in front of you: only if it has not been published.
5.4. I have forgotten a file in the last commit
--no-edit keeps the message as it was. The previous commit is replaced by a new one with the same message and the file included.
The hash changes. If you had already published it, this rewrites history: do not do it, and add a new commit in its place instead. That is exactly the situation in lesson 08-02, where the elegant solution is git commit --fixup + rebase --autosquash before publishing.
A useful check beforehand, so as not to include anything extra:
git status --short # what is staged right now?
git diff --cached --stat # what exactly would go into the amend?5.5. .git/index.lock: File exists
fatal: Unable to create '/home/ana/task-manager/.git/index.lock': File exists. Another git process seems to be running in this repository, e.g. an editor opened by 'git commit'. Please make sure all processes are terminated then try again.
What it is. Git creates .git/index.lock before modifying the index and deletes it when it finishes. It is a lock so that two processes do not write at the same time. If the process died — you closed it with Ctrl+C, the editor hung, the IDE killed it — the file is left orphaned.
The correct procedure, in this order:
# 1. Is there really another Git running? (very common: the IDE)
ps aux | grep -i "[g]it"
# 2. If there is none, look at the lock: if it is from a while ago, it is orphaned
ls -l .git/index.lock
# 3. Delete it
rm -f .git/index.lock
# 4. Check that everything is fine
git statusBy far the most common cause is an IDE (VS Code, IntelliJ, Eclipse) running git status in the background just as you fire off a command. If it happens to you often, it is not a Git problem: it is a race between two clients.
Do not delete
.git/index.lockwithout carrying out step 1. If there really is another process writing, taking its lock away can corrupt the index — which is precisely what lesson 09-05 deals with. With the check done, deleting it is completely harmless.
There are other locks with the same logic and the same treatment: .git/HEAD.lock, .git/refs/heads/main.lock, .git/shallow.lock.
5.6. detected dubious ownership in repository
fatal: detected dubious ownership in repository at '/home/ana/task-manager' To add an exception for this directory, call: git config --global --add safe.directory /home/ana/task-manager
What it is. Since Git 2.35, Git refuses to operate in a repository whose owner in the filesystem is not you. It is a security measure, not a fault: someone else's repository can contain hooks (lesson 06-01) or configuration that run when you launch commands.
The typical scenarios: WSL accessing a Windows drive, a Docker container with a mounted volume, an external drive formatted on another system, or a repository cloned with sudo.
# See who it really belongs to
ls -ld .
# The exception, for one particular repository
git config --global --add safe.directory /home/ana/task-manager
# For a whole tree (Git 2.36+)
git config --global --add safe.directory '/mnt/projects/*'And what you should not do, even though you will find it on the internet:
Before adding the exception, ask yourself whether the ownership is correct. If the repository is yours and it shows up as belonging to root, you probably cloned with sudo and the real solution is:
5.7. "You are in 'detached HEAD' state"
Note: switching to '4f8a2e6'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch.
It is not an error. It is Git telling you that HEAD points directly at a commit and not at a branch (lesson 03-02). You get there with git checkout <sha>, with git switch --detach, when inspecting a tag, during a bisect or in the middle of a rebase.
Two cases:
A. You have made no commits while there. Go back and that is that:
B. You have made commits while there. This is the case you need to know, because those commits are on no branch and will disappear from view as soon as you move:
git log --oneline -3 # note the hash of your last commit
git switch -c rescue-demo # create a branch RIGHT HEREAnd if you had already moved and lost sight of them: no matter, the reflog has them (lesson 09-04).
5.8. Permission denied (publickey)
Diagnosis in three commands, from least to most (lesson 04-03):
Hi ana-ferrer! You've successfully authenticated, but git.example.com does not provide shell access.
If that works and the push does not, the problem is the remote's URL, not the key:
If it does not work:
And if it still fails, the detailed trace, which is the territory of lesson 09-06:
Frequent causes in order of probability: the public key is not registered on the server; the agent is not running (typical after a restart); wrong permissions on ~/.ssh (it must be 700, and private keys 600); or there are several keys and SSH offers the wrong one (fixed with a Host block in ~/.ssh/config).
5.9. I am halfway through a conflict and I want out
The mechanics of resolution are covered in full in lesson 03-05. All that is needed here is the emergency exit, because nobody remembers it in a panic:
git status # says which operation is under way
git merge --abort # if it is a merge
git rebase --abort # if it is a rebase
git cherry-pick --abort # if it is a cherry-pick
git revert --abort # if it is a revert--abort returns the repository exactly to the state prior to the operation. It is safe and loses nothing of what was there before you started (it does lose the resolution work you had done during the conflict).
If --abort fails because you had uncommitted changes mixed in, the reflog and ORIG_HEAD are the way out, and that is lesson 09-04.
- How to read a Git error message
Git has a reputation for cryptic messages and in part it is deserved, but they have improved enormously. It is worth knowing how to read them, because they almost always contain the solution.
The typical anatomy:
error: Your local changes to the following files would be overwritten by merge: app.js Please commit your changes or stash them before you merge. Aborting
| Part | What it tells you |
|---|---|
fatal: |
Git has done nothing. The state is the same as before |
error: |
Something has failed; there may be partial changes. Read the rest |
warning: |
It worked, but there is something you ought to know |
hint: |
Git's suggestion. Read it: it is usually literally the solution |
Aborting |
Explicit confirmation that nothing has been touched |
Three practical rules:
fatal:is reassuring. It means Git has refused to act. Nothing has been broken.- The
hint:lines are the documentation right where you need it. They can be silenced withadvice.*, and a lot of people do so without realising when they copy someone else's configuration. Check withgit config --get-regexp '^advice\.'. - The message names the files involved. It almost always pins the problem down instantly.
And a setting that helps with reading, if your Git has been forced into another language by default:
git config --global advice.detachedHead true # make it warn again if you silenced it
LANG=en_US.UTF-8 git status # force the language in order to search for the errorSearching for the message in English gives far more useful results than searching for a translated version.
- What NEVER to do in a state of panic
The blacklist closes the lesson. Everything that follows has turned small problems into real disasters.
1. Copying and pasting commands from the internet without understanding them. Especially if they start with git reset --hard, git push --force, git clean -fdx or rm -rf .git. The answer that works for you depends on a context the forum knows nothing about.
2. git push --force. Never in a panic, and hardly ever outside one. If it has to happen, --force-with-lease (lessons 04-05 and 09-03), and only on a branch that is yours.
3. git clean -fdx "to leave it clean". It deletes for real, and with no safety net: untracked files are not in the database. Always run git clean -n first (lesson 09-02).
4. Deleting .git. That is the entire repository: all the history, all the branches, the reflog. It is not "the configuration".
5. Re-cloning on top of the problem directory. Sometimes re-cloning is the right answer (lesson 09-05), but you copy and rescue the local material first: unpublished branches, stashes, work in progress.
6. Chaining commands together without looking at the result of each one. After each step: git status and git log --oneline -5.
7. Running git gc --prune=now or git reflog expire --expire=now. It is the only thing on this list that destroys the safety net of the whole module (lesson 08-06). If there is something to recover, those commands make it impossible.
8. Keeping quiet. If you have forced something onto a shared branch, or deleted something from the server, tell the team straight away. Five minutes later it is a heads-up; five hours later it is a puzzle for everybody.
Common Mistakes and Tips
Mistake 1: acting before diagnosing. The calm protocol — status, log --graph, reflog — costs three seconds and solves most cases without touching anything.
Mistake 2: not reading what git status says. It is the command that gives the most information and the first one forgotten in a panic. It often contains literally the command you need.
Mistake 3: believing that a reset --hard or a branch -D has destroyed commits. They have not. The commits are still in .git/objects and the reflog knows where they are (09-04).
Mistake 4: trusting that everything is recoverable, uncommitted work included. It is not. Whatever was never a commit (nor an add, nor a stash) is nowhere at all.
Mistake 5: using git clone as an emergency backup. It does not take the reflog, or the stashes, or the local branches, or the uncommitted work. Use cp -a on the complete directory.
Mistake 6: deleting .git/index.lock without checking whether there is another process. With the check it is harmless; without it, it can corrupt the index.
Mistake 7: silencing Git's hint: lines. They are the best contextual documentation there is, written for exactly the moment you need it.
Mistake 8: safe.directory '*'. It switches off a genuine security protection everywhere. Add the specific exception, or fix the directory's ownership.
Tip 1: commit early and often. A "wip" commit every half hour means you will hardly ever need the next five lessons. The history gets tidied up afterwards with rebase -i.
Tip 2: define the git panic alias. On the day you need it you will not remember the three commands.
Tip 3: cp -a before anything serious. Ten seconds that buy you the peace of mind to experiment.
Tip 4: user.useConfigOnly true in the global configuration. It eliminates the wrong-author commit for good.
Tip 5: search for error messages in English. Ten times more useful results.
Tip 6: when something odd happens with a shared branch, say so immediately. The social cost of speaking up late is far higher than that of speaking up.
Exercises
Exercise 1: the commit on the wrong branch
- Create a repository with
app.jsand three commits onmain. Simulate the remote with a local--bareclone and publishmain. - Without switching branch, make two more commits that should have gone to
GT-247. - Apply the procedure from section 5.2 and leave
mainas it is on the remote andGT-247with the two commits. - Demonstrate with
git log --oneline --all --graphthat no commit has been lost. - Check in the reflog which movements have been recorded.
Exercise 2: identity and forgotten files
- In a new repository, deliberately configure
user.emailwrongly (nobody@localhost) and make a commit that includes onlyindex.html, forgettingstyles.css. - Check the author with
git log -1 --format='%an <%ae>'. - Fix the configuration, add the forgotten file and fix the author in a single
--amend. - Check that the commit's hash has changed and explain why that would make it unacceptable if it were already published.
- Enable
user.useConfigOnly trueand check what happens when you try to commit in a repository with no configured identity.
Exercise 3: the calm protocol on a simulated disaster
- Create a repository with six commits and a branch
GT-241with two commits of its own. - While on
GT-241, rungit reset --hard HEAD~2. - Without recovering anything yet, run the four steps of the calm protocol and write down what each one tells you.
- Identify in the reflog the exact hash of the commit that was the tip of
GT-241before thereset. - Create a branch
rescueon that hash and check that the work is intact. - Repeat the experiment, but this time with a file modified and uncommitted at the moment of the
reset --hard. Explain the difference.
Solutions
Solution 1:
mkdir -p /tmp/p9-01 && cd /tmp/p9-01
git init -b main task-manager
cd task-manager
git config user.name "Ana Ferrer"; git config user.email "ana.ferrer@example.com"
echo "console.log('task-manager');" > app.js
git add . && git commit -q -m "chore: initial commit"
echo "// list" >> app.js && git commit -q -am "feat: task list"
echo "// counter" >> app.js && git commit -q -am "feat: counter"
# The "server"
git init -q --bare /tmp/p9-01/remote.git
git remote add origin /tmp/p9-01/remote.git
git push -q -u origin main# 2. Two commits that were meant for GT-247, but on main
echo "// filter 1" >> app.js && git commit -q -am "GT-247 filter groundwork"
echo "// filter 2" >> app.js && git commit -q -am "GT-247 pending filter"
git log --oneline -37d3a8f4 (HEAD -> main) GT-247 pending filter b52c9d1 GT-247 filter groundwork 4f8a2e6 (origin/main) feat: counter
# 3. The procedure: mark the spot here, put main back
git branch GT-247
git reset --hard origin/main
git switch GT-247* 7d3a8f4 (HEAD -> GT-247) GT-247 pending filter * b52c9d1 GT-247 filter groundwork * 4f8a2e6 (origin/main, main) feat: counter * e91d4a8 feat: task list * 9c4e7b2 chore: initial commit
main is where the remote is, GT-247 has its two commits, and all five commits still exist. The reset --hard was safe because git branch GT-247 was already keeping them reachable.
7d3a8f4 HEAD@{0}: checkout: moving from main to GT-247
4f8a2e6 HEAD@{1}: reset: moving to origin/main
7d3a8f4 HEAD@{2}: commit: GT-247 pending filter
b52c9d1 HEAD@{3}: commit: GT-247 filter groundwork
4f8a2e6 HEAD@{4}: commit: feat: counterNote HEAD@{1}: the reflog records the reset and the hash it moved away from. Even if you had forgotten step 2, 7d3a8f4 is written right there.
Solution 2:
mkdir /tmp/p9-01-b && cd /tmp/p9-01-b && git init -q -b main
git config user.name "Carla Vidal"
git config user.email "nobody@localhost" # wrong on purpose
echo "<h1>Task manager</h1>" > index.html
echo "body { margin: 0; }" > styles.css
git add index.html # we forget styles.css
git commit -q -m "feat: initial page structure"# 3. Everything in a single amend
git config user.email "carla.vidal@example.com"
git add styles.css
git commit --amend --author="Carla Vidal <carla.vidal@example.com>" --no-edit
git log -1 --format='%h author: %an <%ae> | committer: %cn <%ce>'
git show --stat --oneline HEAD3f8a1d6 author: Carla Vidal <carla.vidal@example.com> | committer: Carla Vidal <carla.vidal@example.com>
3f8a1d6 feat: initial page structure styles.css | 1 + index.html | 1 + 2 files changed, 2 insertions(+)
The committer has been corrected too because the --amend created the object after fixing user.email.
3f8a1d6 HEAD@{0}: commit (amend): feat: initial page structure
8f4c2a9 HEAD@{1}: commit (initial): feat: initial page structure8f4c2a9 → 3f8a1d6. A commit is immutable (lesson 01-04): --amend does not modify it, it creates a new one and moves the branch. If the original had been published, anyone who had fetched it would still have it, the branch would have diverged and the push would be rejected: exactly the scenario in lesson 09-03.
# 5. useConfigOnly
git config --global user.useConfigOnly true
mkdir /tmp/p9-01-c && cd /tmp/p9-01-c && git init -q -b main
echo x > f.txt && git add . && git commit -m "test"Git refuses instead of inventing user@machine-name. The problem in section 5.3 stops being possible.
Solution 3:
mkdir /tmp/p9-01-d && cd /tmp/p9-01-d && git init -q -b main
git config user.name "Ana Ferrer"; git config user.email "ana.ferrer@example.com"
for i in 1 2 3 4 5 6; do echo "line $i" >> app.js; git add . ; git commit -q -m "commit $i"; done
git switch -q -c GT-241
echo "// filter" >> app.js && git commit -q -am "GT-241 filter groundwork"
echo "// filter ui" >> styles.css && git add . && git commit -q -m "GT-241 filter styles"
git log --oneline -3b52c9d1 (HEAD -> GT-241) GT-241 filter styles 4f8a2e6 GT-241 filter groundwork 7d3a8f4 (main) commit 6
Reading: there is no operation half done and there is nothing unsaved. That is good news: everything that was there had been committed, and is therefore in the database.
Reading: from here, the two
GT-241commits are no longer reachable.git logdoes not see them. This is what causes the panic, and it is only a matter of references.
7d3a8f4 HEAD@{0}: reset: moving to HEAD~2
b52c9d1 HEAD@{1}: commit: GT-241 filter styles
4f8a2e6 HEAD@{2}: commit: GT-241 filter groundwork
7d3a8f4 HEAD@{3}: checkout: moving from main to GT-241Reading: it is all there.
HEAD@{1}is the tip that existed before the reset.
Reading: Git confirms that the commit exists and that nobody points at it. Dangling here is exactly what we expect (lesson 09-05).
# 4 and 5. The rescue
git branch rescue b52c9d1
git log --oneline rescue -3
git diff GT-241 rescue --statb52c9d1 (rescue) GT-241 filter styles 4f8a2e6 GT-241 filter groundwork 7d3a8f4 (HEAD -> GT-241, main) commit 6
The work is intact. Creating a branch is safer than doing a reset, because it moves nothing that is already fine: it just adds a pointer. It is the pattern generalised in lesson 09-04.
# 6. The difference: UNCOMMITTED changes
git switch -q GT-241
echo "// three hours of uncommitted work" >> app.js
git status --shortThat line is nowhere at all. There is no reflog entry recording it, no dangling object containing it, git fsck finds nothing. It never reached the object database.
And the variant that changes the outcome completely:
echo "// three hours of work" >> app.js
git add app.js # <-- the only difference
git reset --hard HEAD
git fsck --lost-foundWith a simple git add, the content is recoverable. That is the exact boundary between what Git can save and what it cannot, and that is why the advice to commit (or at least stage) often is not a stylistic fussiness: it is the safety net.
Conclusion
This lesson is the module's index and, above all, the change of attitude that makes the rest work.
- In Git almost nothing is truly lost. Commits are immutable objects that survive even when no reference points at them; deleting a branch or moving a pointer with
resetdestroys nothing. What is genuinely fragile is whatever never reached the database: uncommitted changes and untracked files. git addalready counts. Staged content is stored as a blob and can be rescued. Committing is better, but staging is already a net.- The calm protocol —
git status,git log --oneline --graph -20,git reflog -20and, only if needed,git fsck— modifies nothing, takes three seconds and solves most scares before you touch a single destructive command. cp -aon the complete directory is the emergency backup. Aclonewill not do: it does not take the reflog, or the stashes, or the local material.- Git's messages are better than their reputation suggests.
fatal:means nothing has been done; thehint:lines usually contain literally the solution. - The trivial cases are solved in one command:
git switch -cfor changes on the wrong branch,git branch+reset --hard origin/xfor badly placed commits,commit --amend --authorfor identity,--amend --no-editfor the forgotten file, deletingindex.lockafter checking the processes, andsafe.directoryfor dubious ownership. - And the blacklist: no
--force, noclean -fdx, norm -rf .gitand nogc --prune=nowin a state of panic.
The rest of the module develops the cases that do not fit in a table row. We start with the most frequent one, and the one that keeps a promise outstanding since lesson 05-06: the mechanics of git reset, its three modes, exactly what each one touches, and how to choose the right tool depending on whether what you want to undo is uncommitted, committed but unpublished, or already published.
Continue in lesson 09-02: Undoing Changes.
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
