Skip to content

Recover work

  • Lesson 12
  • 40 min
  • Module 4: Resolve and recover
Lesson 12 of 15 · Git and Team Development EssentialsRecover work

What you will be able to do

Get your work back after you moved a branch to the wrong commit or deleted a branch. After this lesson, you can find a lost commit, recover it, and keep your teammate’s work at the same time.

Before you startWhat to know and to have readyAbout 40 minutes

What you need to know

What you need on your computer

  • The lab ~/projects/c14-lab from this module, with ticket-notes, sam-notes and team-remote.git. In ticket-notes, git status shows Your branch is up to date with 'origin/main' and nothing to commit, working tree clean.
  • The reverts that you pushed in Undo safely: Guided practice: revert, then reset in a clone and Undo safely: Your turn: revert one commit, keep the next. In this lesson, the newest commit on the remote is the revert from that “Your turn” task. If you did not do the task, your newest commit is different. The steps work the same.
  • No scratch folder in c14-lab. You deleted it in the last step of Undo safely: Guided practice: revert, then reset in a clone. If it is there, go to ~/projects/c14-lab, check with pwd, and run rm -rf scratch.
  • If the lab is missing or broken, run sh nextia-learning-labs/C14/setup.sh m04 --reset in ~/projects. Then do the worked examples in Conflicts step by step and Undo safely again.
  • You do every reset in this lesson in a disposable clone called scratch. You make it with git clone, and delete it before you make it again. The commits that you make have different hashes from this lesson.

When you reset a branch, the commits that leave it are not deleted at once. Git keeps them for some time. The problem is to find their hashes, because no branch and no git log shows them now.

The reflog (reference log) is Git’s record of where HEAD and each branch pointed, newest first. Git adds an entry each time HEAD moves: when you commit, switch, reset, merge, pull or clone. git reflog shows the entries for HEAD:

git reflog (example)
2bf80ca HEAD@{0}: reset: moving to HEAD~2
41faf11 HEAD@{1}: commit: Add ticket 107
121ec42 HEAD@{2}: commit: Add ticket 106
2bf80ca HEAD@{3}: clone: from /Users/amira/projects/c14-lab/team-remote.git

Read each line from left to right:

Part Example Meaning
Hash 41faf11 The commit where HEAD was after this action.
Name HEAD@{1} “Where HEAD was one move ago”. HEAD@{0} is now.
Action commit: Add ticket 107 What moved HEAD, and the commit message or target.

Three limits are important:

  • The reflog is local. It is only in your clone. It is not pushed, and a new clone starts with an empty reflog. Your teammate cannot use your reflog.
  • Entries expire. By default, Git keeps an entry for 90 days. For a commit that no branch contains, it keeps the entry for 30 days. After that, Git can delete the commit.
  • It records only commits. If you never committed a change, or never stashed it, the reflog has nothing to bring back. git restore and git reset --hard discard such changes for good.

Amira makes a disposable clone, then two local commits in it:

Terminal (Amira)
cd ~/projects/c14-lab
git clone team-remote.git scratch
cd scratch

She adds a line for ticket 106 to tickets.txt and commits it with git commit -am "Add ticket 106". Then she does the same for ticket 107:

git log --oneline -3
41faf11 (HEAD -> main) Add ticket 107
121ec42 Add ticket 106
2bf80ca (origin/main, origin/HEAD) Revert "Update the accounts team extension"

She wants to remove only the last commit, but she types HEAD~2 instead of HEAD~1:

Terminal (Amira, in scratch)
git reset --hard HEAD~2
Output
HEAD is now at 2bf80ca Revert "Update the accounts team extension"

Both commits are gone from main, and from git log. She looks in the reflog. -5 shows the newest five entries:

Terminal (Amira, in scratch)
git reflog -5
Output
2bf80ca HEAD@{0}: reset: moving to HEAD~2
41faf11 HEAD@{1}: commit: Add ticket 107
121ec42 HEAD@{2}: commit: Add ticket 106
2bf80ca HEAD@{3}: clone: from /Users/amira/projects/c14-lab/team-remote.git

The entry before the reset is 41faf11, her ticket 107 commit. Its parent is the ticket 106 commit. So 41faf11 has both of her commits.

She could run git reset --hard 41faf11 to move main back. But if she copies the wrong hash, she makes a second mistake. A safer way is to put a new branch on the commit first. A branch changes no files, and she can inspect it:

Terminal (Amira, in scratch)
git branch rescue 41faf11
git log --oneline -3 rescue
Output
41faf11 (rescue) Add ticket 107
121ec42 Add ticket 106
2bf80ca (HEAD -> main, origin/main, origin/HEAD) Revert "Update the accounts team extension"

These are the correct commits. main is behind rescue, so she moves main forward with a fast-forward merge. --ff-only means “only if this is a fast-forward”. If it is not, Git stops and changes nothing. Then she deletes the rescue branch:

Terminal (Amira, in scratch)
git merge --ff-only rescue
git branch -d rescue
Output
Updating 2bf80ca..41faf11
Fast-forward
tickets.txt | 2 ++
1 file changed, 2 insertions(+)
Deleted branch rescue (was 41faf11).

main points to 41faf11 again, and both commits are back.

Worked example: bring back a deleted branch

Section titled “Worked example: bring back a deleted branch”

Amira tries an idea on a branch, try-priorities, with one commit. She switches back to main and deletes the branch. git branch -d refuses, because the branch has a commit that is on no other branch:

Output
error: the branch 'try-priorities' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D try-priorities'

This warning protects work. She uses -D, which deletes the branch anyway:

Terminal (Amira, in scratch)
git branch -D try-priorities
Output
Deleted branch try-priorities (was edc37a8).

Then she understands that she needs the idea after all. The output above shows the hash: (was edc37a8). If that output is gone from the screen, use the reflog. The hash is in the entry for her commit on the branch:

git reflog -3
41faf11 HEAD@{0}: checkout: moving from try-priorities to main
edc37a8 HEAD@{1}: commit: Add priority levels
41faf11 HEAD@{2}: checkout: moving from main to try-priorities

A branch is only a label on a commit, so she makes the label again:

Terminal (Amira, in scratch)
git branch try-priorities edc37a8
git log --oneline -2 try-priorities
Output
edc37a8 (try-priorities) Add priority levels
41faf11 (HEAD -> main) Add ticket 107

A stash is a place where Git keeps changes that you did not commit, so that you can work on something else. git stash push saves the changes and makes your files match the last commit again. git stash pop puts the changes back.

Amira is halfway through ticket 108 in tickets.txt. She also has a new file, draft.txt, which Git does not track. A teammate asks her to check something on a clean main:

Terminal (Amira, in scratch)
git status --short
git stash push -m "Half-done ticket 108"
git status --short
git stash list
Output
M tickets.txt
?? draft.txt
Saved working directory and index state On main: Half-done ticket 108
?? draft.txt
stash@{0}: On main: Half-done ticket 108

-m gives the stash a message, so she can recognise it in git stash list. The tickets.txt change is in the stash. But draft.txt is still in the folder: by default, stash does not take untracked files. git stash push -u takes them too.

When she is ready, she brings the change back:

Terminal (Amira, in scratch)
git stash pop
Output (last line)
Dropped refs/stash@{0} (e42f7c918e3b194e6e2218fcf006ec4e10a15bed)

The change is back in tickets.txt, and the stash is removed from the list.

  • Untracked files stay behind unless you use -u.
  • A stash is local. git push does not send it. If your computer breaks, the stash is lost.
  • pop can conflict. If the same lines changed after you stashed, pop stops with a conflict. The stash then stays in the list. See the common mistake below.
  • Stashes are easy to forget. After a week, stash@{0}: On main: WIP tells you very little.

For work that must wait more than a few minutes, a commit on its own branch is safer. The reflog records it, you can push it, and its message says what it is:

Terminal window
git switch -c wip-ticket-108
git commit -am "WIP: ticket 108, cause not found yet"
git switch main

Guided practice: lose commits and get them back

Section titled “Guided practice: lose commits and get them back”
  1. If you did the worked examples in your own scratch clone, delete that clone. Otherwise, git clone in the next step stops, because the folder exists. rm -rf deletes a folder with no undo, so check the name before you press Enter:

    Terminal window
    cd ~/projects/c14-lab
    rm -rf scratch

    ls no longer shows scratch.

  2. Make a new disposable clone:

    Terminal window
    cd ~/projects/c14-lab
    git clone team-remote.git scratch
    cd scratch
  3. Add a line for ticket 106 to tickets.txt, save, and commit:

    Terminal window
    git commit -am "Add ticket 106"
  4. Add a line for ticket 107, save, and commit:

    Terminal window
    git commit -am "Add ticket 107"
  5. Run pwd. Then remove both commits by mistake:

    Terminal window
    git reset --hard HEAD~2

    git log --oneline -1 shows the commit from the remote. Your two commits are not in the log.

  6. Find them:

    Terminal window
    git reflog -5

    The entry HEAD@{1} shows commit: Add ticket 107. Write down its hash.

  7. Put a branch on that hash, and check it. Use your hash instead of 41faf11:

    Terminal window
    git branch rescue 41faf11
    git log --oneline -3 rescue

    The output shows both of your commits.

  8. Move main forward, and delete the rescue branch:

    Terminal window
    git merge --ff-only rescue
    git branch -d rescue

    The output includes Fast-forward. git log --oneline -2 shows both commits again.

  9. Make a branch with one commit, and go back to main:

    Terminal window
    git switch -c try-priorities
    echo "Priority levels: low, normal, urgent." > priorities.txt
    git add priorities.txt
    git commit -m "Add priority levels"
    git switch main
  10. Run pwd. Then delete the branch:

    Terminal window
    git branch -D try-priorities

    The output is Deleted branch try-priorities (was …), with a hash.

  11. Make the branch again with that hash. Use your hash instead of edc37a8. Then check it:

    Terminal window
    git branch try-priorities edc37a8
    git log --oneline -1 try-priorities

    The output shows Add priority levels.

  12. Add a line for ticket 108 to tickets.txt, but do not commit. Stash it, and check:

    Terminal window
    git stash push -m "Half-done ticket 108"
    git stash list
    git status --short

    git stash list shows your message. git status --short prints nothing.

  13. Bring the change back with git stash pop. git status --short shows M tickets.txt.

Your turn: recover your commit and keep Sam’s

Section titled “Your turn: recover your commit and keep Sam’s”

This is the completion check for Module 4. You recover your own lost commit, and your teammate’s work must stay.

  1. Delete the old scratch, and make a new one. In it, commit a line for ticket 106.
  2. In sam-notes, run git pull. Add Network printer: ext. 4520 to contacts.txt, commit with the message Add the network printer contact, and push.
  3. In scratch, run git fetch. Then make this mistake on purpose: git reset --hard origin/main. You wanted to “update to the remote”, and your ticket 106 commit left main.
  4. Get your commit back so that main in scratch has both your ticket 106 commit and Sam’s printer contact. Do not push from scratch.

Then show the result with git log --oneline --graph -4, tail -1 contacts.txt, tail -1 tickets.txt and sh check.sh.

Show a model answer

After the mistake, the reflog shows the lost commit at HEAD@{1}:

git reflog -4
6a64a93 HEAD@{0}: reset: moving to origin/main
a89b80c HEAD@{1}: commit: Add ticket 106
2bf80ca HEAD@{2}: clone: from /Users/amira/projects/c14-lab/team-remote.git

Put a branch on it. Then merge it into main. This is not a fast-forward, because main now has Sam’s commit and rescue has yours. So Git makes a merge commit that keeps both:

Terminal window
git branch rescue a89b80c
git merge --no-edit rescue
git log --oneline --graph -4
tail -1 contacts.txt
tail -1 tickets.txt
sh check.sh
Output (part)
Merge made by the 'ort' strategy.
tickets.txt | 1 +
1 file changed, 1 insertion(+)
* 50d5765 (HEAD -> main) Merge branch 'rescue'
|\
| * a89b80c (rescue) Add ticket 106
* | 6a64a93 (origin/main, origin/HEAD) Add the network printer contact
|/
* 2bf80ca Revert "Update the accounts team extension"
Network printer: ext. 4520
Ticket 106: keyboard missing keys. Ordered a new keyboard.
PASS: 6 ticket lines checked

The graph shows both lines of work: your recovered commit and Sam’s commit. You can now delete the label with git branch -d rescue. Sam’s commit was never at risk on the remote. The danger was in the next step: if you had moved main back to a89b80c with reset, your main would not contain Sam’s commit.

Another correct answer: git reset --hard a89b80c, then git pull --no-rebase --no-edit. The files are the same. Only the message of the merge commit is different.

Common mistake

git stash pop stops with a conflict

Symptom: you stashed a change to tickets.txt, and then committed a change to the same lines. git stash pop shows:

Output (first and last lines)
Auto-merging tickets.txt
CONFLICT (content): Merge conflict in tickets.txt
...
The stash entry is kept in case you need it again.

git stash list still shows the stash. tickets.txt has conflict markers. Here, the names of the sides are different from a merge:

tickets.txt (end)
<<<<<<< Updated upstream
Ticket 107: cannot print in colour. Installed the colour driver. Fixed.
||||||| Stash base
Ticket 107: cannot print in colour. Installed the colour driver.
=======
Ticket 107: cannot print in colour. Installed the colour driver.
Ticket 108: VPN drops every hour.
>>>>>>> Stashed changes

Cause: Updated upstream is your current commit. Stashed changes is the stash. Both changed lines next to each other since you stashed, so Git cannot apply the stash alone. It keeps the stash so that the change is not lost.

Fix:

  1. Resolve the conflict as in Conflicts step by step. Here, keep the Fixed. line and add the ticket 108 line after it.

  2. Run git diff --check and sh check.sh.

  3. Run git add tickets.txt to mark the file as resolved.

  4. Remove the stash, because its change is now in your file:

    Terminal window
    git stash drop
    Output
    Dropped refs/stash@{0} (1b54611302688949f349718abca555a35e38fcb9)

Common mistake

The reflog does not have my change

Symptom: you ran git restore tickets.txt or git reset --hard, and lost an edit. git reflog shows no entry for it.

Cause: the edit was never in a commit or a stash. The reflog records only where HEAD and branches pointed, and they point to commits. Git never saved the edit, so Git cannot bring it back.

Fix: Git cannot help here. Look for another copy: your editor’s undo history, or its local history feature. To prevent this, commit or stash before any command that discards changes, and read git status before you run it.

Check your understanding

Predict: Amira runs git reset --hard HEAD~2 in her clone and loses two local commits. Sam clones the remote on his computer. Can Sam find Amira's two commits with git reflog?

Show the answer

No. The reflog is local to each clone, and it is never pushed. Amira never pushed the two commits, so they exist only in her clone. Sam’s new clone has its own reflog, which starts with his clone. Only Amira can recover the commits, from her own reflog, and only before the entries expire.

Check your understanding

Completion check: in your scratch clone, show that you recovered your lost commit and kept Sam's commit. Which output proves each part?

Show the answer
  • git log --oneline --graph -4 shows a merge commit with two parents: your Add ticket 106 and Sam’s Add the network printer contact.
  • tail -1 tickets.txt shows your ticket 106 line. tail -1 contacts.txt shows Sam’s printer line.
  • sh check.sh prints PASS.
  • You did not push from scratch, and you did not rewrite origin/main: git status says that you are ahead of origin/main, not that you diverged or are behind.

Recap

  • The reflog records where HEAD pointed after each move. Use it to find the hash of a commit that left a branch.
  • Recover by putting a branch on the hash first, git branch rescue <hash>. Inspect it, then merge it or reset to it.
  • A deleted branch is a lost label. Make it again with git branch <name> <hash>, using the hash from (was …) or from the reflog.
  • The reflog is local, and its entries expire. It cannot recover changes that you never committed or stashed.
  • A stash keeps uncommitted changes for a short time. It skips untracked files without -u, stays on your computer, and keeps the entry if pop conflicts. For longer work, commit on a branch.

This is the end of Module 4. You can now resolve a conflict, undo a shared commit with revert, and recover lost local work. In Module 5, you use these skills to prepare a release and fix it.

Next: Release branches and tags