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

  1. The idea that underpins everything: in Git almost nothing is lost
  2. The calm protocol: four commands before touching anything
  3. The ten-second backup
  4. The emergency catalogue
  5. The trivial cases, solved here
  6. How to read a Git error message
  7. What NEVER to do in a state of panic

  1. 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.

  1. 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

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

git log --oneline --graph --decorate -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 -30

If 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

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-forward

It 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

git fsck --no-progress

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'
git panic

Three seconds, zero modifications, and almost always the complete diagnosis.

  1. 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".

cp -a ../task-manager ../task-manager-BACKUP-$(date +%Y%m%d-%H%M)

On Windows (PowerShell):

Copy-Item -Recurse ..\task-manager ..\task-manager-BACKUP

Three important details:

  • cp -a (or -R preserving links) copies the entire directory, .git included. That is what makes it a real copy rather than a partial clone.
  • Do not use git clone for 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 rebase or git 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.

  1. 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.

  1. 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 status
Switched to a new branch 'GT-241'
On branch GT-241
Changes not staged for commit:
	modified:   app.js

The changes are intact, now on the right branch, and main has not moved.

If the branch already exists:

git switch GT-241

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:

git stash push -m "GT-241 half done"
git switch GT-241
git stash pop

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.

# 1. Check what is there
git log --oneline -3
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 -3
b52c9d1 (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-edit
git log -1 --format='%an <%ae>  |  %cn <%ce>'
Carla Vidal <carla.vidal@example.com>  |  Carla Vidal <carla.vidal@example.com>

A 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-edit

And 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 true

With 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

git add styles.css
git commit --amend --no-edit

--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 status

By 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.lock without 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:

# NO: disables the check EVERYWHERE
git config --global --add safe.directory '*'

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:

sudo chown -R "$USER:$USER" /home/ana/task-manager

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:

git switch -          # to where you were
git switch main       # or wherever you like

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 HERE
Switched to a new branch 'rescue-demo'

And if you had already moved and lost sight of them: no matter, the reflog has them (lesson 09-04).

5.8. Permission denied (publickey)

git push
git@git.example.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Diagnosis in three commands, from least to most (lesson 04-03):

# 1. Does the server recognise me?
ssh -T git@git.example.com
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:

git remote -v      # is it an SSH or an HTTPS URL?

If it does not work:

# 2. Are there any keys loaded in the agent?
ssh-add -l
The agent has no identities.
# 3. Load it
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

And if it still fails, the detailed trace, which is the territory of lesson 09-06:

GIT_SSH_COMMAND="ssh -v" git push 2>&1 | head -40

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.

  1. 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:

  1. fatal: is reassuring. It means Git has refused to act. Nothing has been broken.
  2. The hint: lines are the documentation right where you need it. They can be silenced with advice.*, and a lot of people do so without realising when they copy someone else's configuration. Check with git config --get-regexp '^advice\.'.
  3. 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 error

Searching for the message in English gives far more useful results than searching for a translated version.

  1. 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

  1. Create a repository with app.js and three commits on main. Simulate the remote with a local --bare clone and publish main.
  2. Without switching branch, make two more commits that should have gone to GT-247.
  3. Apply the procedure from section 5.2 and leave main as it is on the remote and GT-247 with the two commits.
  4. Demonstrate with git log --oneline --all --graph that no commit has been lost.
  5. Check in the reflog which movements have been recorded.

Exercise 2: identity and forgotten files

  1. In a new repository, deliberately configure user.email wrongly (nobody@localhost) and make a commit that includes only index.html, forgetting styles.css.
  2. Check the author with git log -1 --format='%an <%ae>'.
  3. Fix the configuration, add the forgotten file and fix the author in a single --amend.
  4. Check that the commit's hash has changed and explain why that would make it unacceptable if it were already published.
  5. Enable user.useConfigOnly true and check what happens when you try to commit in a repository with no configured identity.

Exercise 3: the calm protocol on a simulated disaster

  1. Create a repository with six commits and a branch GT-241 with two commits of its own.
  2. While on GT-241, run git reset --hard HEAD~2.
  3. Without recovering anything yet, run the four steps of the calm protocol and write down what each one tells you.
  4. Identify in the reflog the exact hash of the commit that was the tip of GT-241 before the reset.
  5. Create a branch rescue on that hash and check that the work is intact.
  6. 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 -3
7d3a8f4 (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
# 4. Nothing lost
git log --oneline --all --graph --decorate
* 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.

# 5. The reflog
git reflog -6
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: counter

Note 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"
# 2. The author
git log -1 --format='%an <%ae>'
Carla Vidal <nobody@localhost>
# 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 HEAD
3f8a1d6  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.

# 4. The hash has changed
git reflog -2
3f8a1d6 HEAD@{0}: commit (amend): feat: initial page structure
8f4c2a9 HEAD@{1}: commit (initial): feat: initial page structure

8f4c2a93f8a1d6. 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"
fatal: no email was given and auto-detection is disabled

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 -3
b52c9d1 (HEAD -> GT-241) GT-241 filter styles
4f8a2e6 GT-241 filter groundwork
7d3a8f4 (main) commit 6
# 2. The disaster
git reset --hard HEAD~2
git log --oneline -1
7d3a8f4 (HEAD -> GT-241, main) commit 6
# 3. The protocol
git status
On branch GT-241
nothing to commit, working tree clean

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.

git log --oneline --graph --decorate -20
* 7d3a8f4 (HEAD -> GT-241, main) commit 6
* e91d4a8 commit 5
...

Reading: from here, the two GT-241 commits are no longer reachable. git log does not see them. This is what causes the panic, and it is only a matter of references.

git reflog -6
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-241

Reading: it is all there. HEAD@{1} is the tip that existed before the reset.

git fsck --no-progress
dangling commit b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b

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 --stat
b52c9d1 (rescue) GT-241 filter styles
4f8a2e6 GT-241 filter groundwork
7d3a8f4 (HEAD -> GT-241, main) commit 6
 app.js     | 1 +
 styles.css | 1 +
 2 files changed, 2 insertions(+)

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 --short
 M app.js
git reset --hard HEAD
git status --short          # (no output)
grep -c "three hours" app.js
0
git reflog -2
git fsck --lost-found | grep -c blob
7d3a8f4 HEAD@{0}: reset: moving to HEAD
0

That 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-found
dangling blob 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b
git cat-file -p 6f2b9d4 | tail -1
// three hours of work

With 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 reset destroys nothing. What is genuinely fragile is whatever never reached the database: uncommitted changes and untracked files.
  • git add already counts. Staged content is stored as a blob and can be rescued. Committing is better, but staging is already a net.
  • The calm protocolgit status, git log --oneline --graph -20, git reflog -20 and, only if needed, git fsck — modifies nothing, takes three seconds and solves most scares before you touch a single destructive command.
  • cp -a on the complete directory is the emergency backup. A clone will 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; the hint: lines usually contain literally the solution.
  • The trivial cases are solved in one command: git switch -c for changes on the wrong branch, git branch + reset --hard origin/x for badly placed commits, commit --amend --author for identity, --amend --no-edit for the forgotten file, deleting index.lock after checking the processes, and safe.directory for dubious ownership.
  • And the blacklist: no --force, no clean -fdx, no rm -rf .git and no gc --prune=now in 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

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved