Ran Wei/Git Series
中文
Git and Version Control — Ran Wei

History and undoing

Read your project's history, see exactly what changed, and undo mistakes at every stage: before staging, after staging, in the last commit and in older commits.

3 hours3 study sessions6 command sessionsGit 2.50.1 (Apple Git-155)

By the end you can

  • List and filter history with git log.
  • Read a diff, and use git diff, git diff --staged and git show.
  • Throw away uncommitted changes and unstage files with git restore.
  • Fix the last commit with git commit --amend, and undo older commits with git revert.
  • Look at an old version safely, and explain what a detached HEAD is.

Before you start

Module 2: creating a repository, staging with git add and committing with clear messages.

Contents

Study plan

3 hours

Times are estimates and include typing the commands. Progress is saved in this browser and shared by both language editions.

Session 160 min
Looking back
Session 260 min
Undoing commits
Session 360 min
Practice
1

Reading the history

Every commit you make is kept, with its author, its date and the message you wrote. git log is how you read that history. Without options, it lists the commits newest first, and shows each one’s full hash, author, date and message. For a long history that is a lot of text, so a few options help:

  • --oneline prints one line per commit: the short hash and the message.
  • -n 2 shows only the newest two commits. Any number works; -n 1 shows just the newest.
  • --stat adds which files each commit changed, and by how much.
  • A file name at the end, as in git log pancakes.md, lists only the commits that changed that file.

Options combine freely: git log --oneline -n 5 shows the newest five commits, one line each.

Every session on this page starts from the recipes repository as Module 2’s main example left it, with three commits. Your own repository may hold a few more, if you also committed a .gitignore or did the exercises, and your hashes are different, because Git calculates them from the time of each commit as well as its content. Always use the hashes your own git log prints. Your repository also grows as you work through this module, so your output may differ from the pages in small ways; the commands work the same.

Tip

When the history is longer than your window, git log shows it in a pager, a viewer that shows one screen at a time. Scroll with the arrow keys or Space, and press q to leave it and return to the prompt. The sessions on these pages print everything at once.

Try itRead the history
git log --oneline
Output
1ad5842 Say what the notes are for
8bf3c2d Add a pancake recipe
907a979 Add a README
git log -n 1
Output
commit 1ad584283c1471035cb8e8538e2ea2cc2ce54824
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:09:00 2026 +0000

    Say what the notes are for
git log --oneline --stat
Output
1ad5842 Say what the notes are for
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
8bf3c2d Add a pancake recipe
 pancakes.md | 5 +++++
 1 file changed, 5 insertions(+)
907a979 Add a README
 README.md | 3 +++
 1 file changed, 3 insertions(+)
git log --oneline pancakes.md
Output
8bf3c2d Add a pancake recipe
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git log -n 1 shows the newest commit in full: its 40-character hash, the author, the date and the message. With --stat, each commit gets a short summary of its changes. README.md | 2 +- says that the commit changed two lines of README.md: one added (+) and one removed (-), which is how Git records a line that was reworded. pancakes.md | 5 +++++ says that five lines were added.

The last command, git log --oneline pancakes.md, lists only 8bf3c2d Add a pancake recipe, because no other commit touched pancakes.md. When you want to know when, and why, a file changed, this is the place to start.

2

Seeing what changed

git log tells you which commits exist. Three related commands show what actually changed:

  • git diff compares the working tree with the staging area. It shows the changes you have made but not yet staged.
  • git diff --staged compares the staging area with the last commit. It shows what your next commit will contain. (--cached is another name for the same option.)
  • git show shows one commit: its hash, author, date and message, followed by its changes. On its own it shows the newest commit; give it a hash to see another one.

git status tells you which files changed; these commands show you how, so you can check every change before it goes into the history. In the session, you change the pancake recipe to use less milk and add a pinch of salt, then follow the change from the working tree into a commit.

Try itSee what changed
Create or edit pancakes.md so that it contains:
# Pancakes

- 200 g flour
- 2 eggs
- 250 ml milk
- a pinch of salt
git diff
Output
diff --git a/pancakes.md b/pancakes.md
index 871d830..fcc716f 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,4 +2,5 @@

 - 200 g flour
 - 2 eggs
-- 300 ml milk
+- 250 ml milk
+- a pinch of salt
git add pancakes.md
git diff
git diff --staged
Output
diff --git a/pancakes.md b/pancakes.md
index 871d830..fcc716f 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,4 +2,5 @@

 - 200 g flour
 - 2 eggs
-- 300 ml milk
+- 250 ml milk
+- a pinch of salt
git commit -m "Use less milk and add salt"
Output
[main b7b26af] Use less milk and add salt
 1 file changed, 2 insertions(+), 1 deletion(-)
git show
Output
commit b7b26af362cd6db7ab58150f347b1c07ccfa83e5
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:14:00 2026 +0000

    Use less milk and add salt

diff --git a/pancakes.md b/pancakes.md
index 871d830..fcc716f 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,4 +2,5 @@

 - 200 g flour
 - 2 eggs
-- 300 ml milk
+- 250 ml milk
+- a pinch of salt
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

The output of git diff is called a diff, and every diff has the same parts:

  • diff --git a/pancakes.md b/pancakes.md names the file: a/ is the old version and b/ the new one. The index, --- and +++ lines that follow say the same in more detail, and you can skip them.
  • @@ -2,4 +2,5 @@ starts a hunk, one block of changes. -2,4 means the block covers four lines of the old version, starting at line 2. +2,5 means it covers five lines of the new version, also starting at line 2: the new version is one line longer.
  • Below it, each line starts with a marker. A line starting with - was removed, a line starting with + was added, and a line starting with a space is unchanged context, shown to help you find the place. Here, - 300 ml milk was replaced by - 250 ml milk, and - a pinch of salt was added. The first character of each line is the diff’s marker; the - after it belongs to the recipe’s list.

After git add pancakes.md, the second git diff prints nothing, because the working tree and the staging area now match. The change has not vanished: git diff --staged shows exactly the same diff, because the change now sits between the staging area and the last commit. Once you commit, git show displays it once more, with the commit’s hash, author, date and message above it.

git diff can also compare two commits: git diff <older> <newer>, with hashes from git log --oneline. The diff then shows how the newer commit differs from the older one: lines marked - are only in the older commit, and lines marked + only in the newer one. Exercise 2 practises this.

3

Undoing changes you have not committed

Mistakes are easiest to undo before they reach a commit. Two forms of git restore cover the two cases:

  • git restore <file> throws away your edits to a file. It replaces the file in the working tree with the version in the staging area, which is the version from the last commit unless you have staged something since.
  • git restore --staged <file> unstages a file. It takes the change out of the staging area, so that it will not go into the next commit, but leaves your edit in the working tree.

You do not need to memorise either: git status suggests both in its hints, as the session shows. First, the README’s description has been deleted by accident. Then a change to the pancake recipe, three eggs instead of two, is staged before it is ready.

Try itThrow away changes you have not committed
Create or edit README.md so that it contains:
# Family recipes

Oops: the description is gone.
git status
Output
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
git restore README.md
git status
Output
On branch main
nothing to commit, working tree clean
cat README.md
Output
# Family recipes

Recipes we cook again and again, with notes on what worked.
Create or edit pancakes.md so that it contains:
# Pancakes

- 200 g flour
- 3 eggs
- 300 ml milk
git add pancakes.md
git status
Output
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   pancakes.md
git restore --staged pancakes.md
git status
Output
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   pancakes.md

no changes added to commit (use "git add" and/or "git commit -a")
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

After git restore README.md, git status reports a clean working tree, and cat shows the description again. After git restore --staged pancakes.md, the change is back under “Changes not staged for commit”: the three eggs are still in the file, but they are no longer staged. If you are following along, run git restore pancakes.md now to throw that edit away as well, so that your working tree is clean for the next section.

Pitfall

git restore <file> without --staged throws away edits that were never committed. Git never had a copy of them, so it cannot bring them back. If you are unsure, look at git diff first.

Older guides and many answers online use git checkout -- <file> to throw away edits and git reset HEAD <file> to unstage. Both still work, but git restore, added in Git 2.23, says what it does, and it is what git status suggests.

4

Fixing the last commit

Sometimes you notice a mistake just after committing: a typo in the message, or a file you forgot to stage. As long as you have not shared the commit, git commit --amend fixes it by replacing the last commit with a new one:

  • git commit --amend -m "new message" gives the commit a new message.
  • git commit --amend --no-edit keeps the message, and adds whatever you have staged since.

With neither option, Git opens your editor with the old message, ready to change. In the session, you commit a soda bread recipe with a typo in the message, correct the message, and then notice that the recipe has no raising agent.

Try itFix the last commit
Create or edit bread.md so that it contains:
# Soda bread

- 450 g flour
- 400 ml buttermilk
git add bread.md
git commit -m "Add a sdoa bread recipe"
Output
[main 087bb2c] Add a sdoa bread recipe
 1 file changed, 4 insertions(+)
 create mode 100644 bread.md
git commit --amend -m "Add a soda bread recipe"
Output
[main 3fc0d24] Add a soda bread recipe
 Date: Mon Jan 5 09:11:00 2026 +0000
 1 file changed, 4 insertions(+)
 create mode 100644 bread.md
Create or edit bread.md so that it contains:
# Soda bread

- 450 g flour
- 400 ml buttermilk
- 1 teaspoon bicarbonate of soda
git add bread.md
git commit --amend --no-edit
Output
[main 31783ba] Add a soda bread recipe
 Date: Mon Jan 5 09:11:00 2026 +0000
 1 file changed, 5 insertions(+)
 create mode 100644 bread.md
git log --oneline -n 2
Output
31783ba Add a soda bread recipe
1ad5842 Say what the notes are for
git show --stat
Output
commit 31783bacd01cd94e817a9f878b5a49d36be8dbf3
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:11:00 2026 +0000

    Add a soda bread recipe

 bread.md | 5 +++++
 1 file changed, 5 insertions(+)
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

Look at the hashes. The first commit was 087bb2c; after the first amend the newest commit is 3fc0d24, and after the second it is 31783ba. Amending does not edit a commit in place, because a commit never changes once it is made. Git makes a new commit, with the same parent as the old one, and moves the branch to it. The old commit is no longer part of the branch, which is why git log --oneline -n 2 shows 31783ba directly above 1ad5842. git show --stat confirms that the new commit holds all five lines of bread.md.

The Date: line in each amend’s output is the time of the original commit: an amended commit keeps its original author and date.

Pitfall

Amend only commits you have not shared. If someone already has the old commit, amending leaves you with a history that differs from theirs. Module 8 covers rewriting history, and why to avoid it on shared branches.

5

Undoing a commit

--amend reaches only the newest commit, and it rewrites history. For a commit that is older, or that others already have, use git revert <commit>. It removes nothing: it makes a new commit that undoes the old commit’s changes. Lines that the old commit added are removed, and lines it removed are put back.

You name the commit to undo by its hash, or as HEAD, Git’s name for the commit you are on, normally the newest on your branch. Revert needs a clean working tree, so commit or restore your edits first. In the session, a mistake slips in: the pancake recipe asks for 2000 g of flour instead of 200 g.

Try itUndo a commit with a new commit
Create or edit pancakes.md so that it contains:
# Pancakes

- 2000 g flour
- 2 eggs
- 300 ml milk
git add pancakes.md
git commit -m "Use more flour"
Output
[main 654de4f] Use more flour
 1 file changed, 1 insertion(+), 1 deletion(-)
git revert HEAD
Output
[main 4407f3a] Revert "Use more flour"
 Date: Mon Jan 5 09:12:00 2026 +0000
 1 file changed, 1 insertion(+), 1 deletion(-)
git log --oneline -n 3
Output
4407f3a Revert "Use more flour"
654de4f Use more flour
1ad5842 Say what the notes are for
cat pancakes.md
Output
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git revert HEAD made a new commit, 4407f3a, with the message Revert "Use more flour". On your computer, Git first opens your editor with this ready-made message, followed by a line saying which commit it reverts. Save and close the editor to accept it; the sessions here accept it automatically. The Date: line is simply the time of the new commit.

The log now shows both commits: the mistake, 654de4f, and the commit that undoes it, 4407f3a. That may look untidy, but it is honest, and it is safe on a branch that others share: nobody’s history is rewritten, they simply receive one more commit. cat confirms that the recipe asks for 200 g of flour again.

You can revert any commit, not only the newest, by giving its hash. If later commits changed the same lines, Git may stop with a conflict for you to resolve, which Module 5 explains.

6

Looking at an old version

Sometimes you want to see how the project looked earlier: to find out when something still worked, or to copy an old paragraph back. Git offers two safe ways to look.

To see one file, use git show <commit>:<file>. It prints the file as it was in that commit, and changes nothing. You can name a commit by its hash, or by counting back from HEAD: HEAD~1 is the commit before the one you are on, and HEAD~2 the one before that.

To see the whole project, use git switch --detach <commit>. It puts every file in the working tree back to that commit, so that you can look around, open files or run the project, and it leaves your branch untouched. HEAD, Git’s marker for where you are, then points straight at a commit instead of at a branch. Git calls this a detached HEAD.

You have made more commits by now, so in your repository HEAD~2 is a different commit. To follow along, use the hash of your first commit, Add a README, from git log --oneline.

Try itLook at an old version safely
git show HEAD~2:README.md
Output
# Family recipes

Recipes we cook again and again.
git switch --detach HEAD~2
Output
HEAD is now at 907a979 Add a README
git status
Output
HEAD detached at 907a979
nothing to commit, working tree clean
cat README.md
Output
# Family recipes

Recipes we cook again and again.
* 1ad5842 (main) Say what the notes are for
* 8bf3c2d Add a pancake recipe
* 907a979 (HEAD) Add a README
git switch main
Output
Previous HEAD position was 907a979 Add a README
Switched to branch 'main'
git status
Output
On branch main
nothing to commit, working tree clean
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git show HEAD~2:README.md printed the README’s first wording, and git switch --detach HEAD~2 reported the move: HEAD is now at 907a979 Add a README. git status no longer says On branch main but HEAD detached at 907a979, and cat README.md shows the old wording in the working tree itself.

The graph shows where everything is. main still points at the newest commit, 1ad5842, while HEAD sits alone on 907a979. Nothing has been lost or changed: the newer commits are all still there, and git switch main takes you back. Git tells you where you came from (Previous HEAD position was 907a979), and git status says On branch main again.

Tip

A detached HEAD is fine for looking around. If you make commits there and want to keep them, create a branch for them with git switch -c <name> before you switch away. Exercise 6 shows why.

Older guides use git checkout <commit> for the same thing. It detaches HEAD as well, and prints a longer explanation when it does.

7

Exercises

Exercise 1★★★conceptual6 min

Which command shows it? Name the command that shows each of these.

  1. The changes you have made but not yet staged.
  2. The changes you have staged for the next commit.
  3. What the last commit changed.
  4. Every commit that changed pancakes.md.
Show solution
  1. git diff
  2. git diff --staged
  3. git show
  4. git log --oneline pancakes.md (or git log pancakes.md for the full entries)
Exercise 2★★★coding8 min

Compare two commits. In the recipes repository, show everything that changed between the first commit and the newest one.

Show solution

List the commits to see how far back the first one is, then compare it with HEAD:

SolutionCompare two commits
git log --oneline
Output
1ad5842 Say what the notes are for
8bf3c2d Add a pancake recipe
907a979 Add a README
git diff HEAD~2 HEAD
Output
diff --git a/README.md b/README.md
index 2ecc1f2..130cb17 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
 # Family recipes

-Recipes we cook again and again.
+Recipes we cook again and again, with notes on what worked.
diff --git a/pancakes.md b/pancakes.md
new file mode 100644
index 0000000..871d830
--- /dev/null
+++ b/pancakes.md
@@ -0,0 +1,5 @@
+# Pancakes
+
+- 200 g flour
+- 2 eggs
+- 300 ml milk
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

HEAD~2 is the first commit here. The diff shows the reworded line in README.md, and the whole of pancakes.md as added lines, because the file did not exist in the first commit.

Exercise 3★★★conceptual7 min

Choose the undo. Which command fits each situation?

  1. You edited README.md and want the committed version back.
  2. You staged notes.md by mistake, but want to keep your edits.
  3. The last commit, which you have not shared, has a typo in its message.
  4. A commit from last week, already shared with your team, broke the bread recipe.
Show solution
  1. git restore README.md
  2. git restore --staged notes.md
  3. git commit --amend -m "…" with the corrected message
  4. git revert <hash>, which undoes the commit with a new commit instead of rewriting shared history
Exercise 4★★★coding10 min

Stop tracking a file. Suppose that passwords.txt was committed by mistake, as in Module 2’s last exercise. Keep the file on your disk, but stop tracking it from now on. To try it, first create passwords.txt with any text, and commit it.

Show solution

List the file in .gitignore, remove it from the staging area only, and commit. The session’s repository has no .gitignore yet; if yours has one, add the line passwords.txt to it and keep the lines already there.

SolutionStop tracking a file
Create or edit .gitignore so that it contains:
passwords.txt
git rm --cached passwords.txt
Output
rm 'passwords.txt'
git status
Output
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    passwords.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore
git add .gitignore
git commit -m "Stop tracking the passwords file"
Output
[main 908ddf5] Stop tracking the passwords file
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 .gitignore
 delete mode 100644 passwords.txt
git status
Output
On branch main
nothing to commit, working tree clean
ls
Output
README.md  pancakes.md  passwords.txt
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git rm --cached removes the file from the next commit but leaves it on disk, as ls shows. The passwords are still in the older commits, so change them, as Module 2 advised.

Exercise 5★★★coding7 min

Forgot a file. You committed bread.md, then noticed that butter.md, part of the same change, was never added. Fix this without making a second commit. When should you not do this?

Show solution

Stage the file and amend the commit, keeping its message:

git add butter.md
git commit --amend --no-edit

Do not amend a commit you have already shared: amending replaces it with a new commit, and anyone who has the old one ends up with a different history (Module 8).

Exercise 6★★★conceptual7 min

Commits on a detached HEAD. You ran git switch --detach HEAD~2, edited a file and made a commit. What happens when you run git switch main? How can you keep the commit?

Show solution

Git switches, but warns that you are leaving a commit behind that is not connected to any of your branches, and suggests a git branch command with its hash. The commit still exists, but no branch points at it.

To keep it, create a branch before you switch away: git switch -c rescue. If you have already switched, the hash in Git’s warning (or the reflog, in Module 8) lets you create the branch afterwards.

8

Self-check quiz

1
Which command shows the changes you have staged but not yet committed?
2
In a diff, what does a line starting with + mean?
3
You want to throw away your uncommitted edits to README.md. Which command does that?
4
What does git commit --amend do?
5
A commit you shared last week introduced a bug. What is the safe way to undo it?
6
What does “HEAD detached at 907a979” mean?
7
What does HEAD~2 refer to?
9

Further reading