What had to happen has happened. Yesterday Bruno published a commit on main that changes how tasks are stored in localStorage, and this morning Ana has discovered that users who already had tasks saved find their list emptied when they open the application. The commit has been on git.example.com for fourteen hours, Ana and Carla have it downloaded, and on top of that there are three more commits above it.

Everything we have learned in this module rewrites history, and the golden rule expressly forbids that in this situation: that commit is already public. It cannot be rebased, it cannot be removed with a rebase -i, it cannot be made to disappear.

Git has an answer for this, and it is elegant precisely because it does not fight immutability but accepts it: the commit is not deleted, another one is added that applies the opposite change. The history grows instead of shrinking, nobody has to force anything, and there is a public record that it was undone and why.

That is git revert, and it is the only safe way of undoing something that is already published. With it we close the module.

Contents

  1. What git revert does
  2. Reverting Bruno's commit
  3. revert versus reset versus --amend
  4. Reverting several commits and ranges
  5. -n: grouping several reverts into one commit
  6. Conflicts while reverting
  7. Reverting a merge commit: -m 1 and -m 2
  8. The problem of the reverted branch that will not go back in
  9. Reverting a revert
  10. When to revert and when not to

  1. What git revert does

git revert <sha>

Git works out the diff that <sha> introduced, inverts it (what it added, it removes; what it removed, it adds) and creates a new commit with that inverse change on the tip of your branch.

gitGraph
   commit id: "8b6d3c2"
   commit id: "c5d9b1e"
   commit id: "4f8a2e6" type: HIGHLIGHT
   commit id: "7d3a8f4"
   commit id: "e91d4a8"
   commit id: "b52c9d1"
   commit id: "3e7f1a8"

Commit 4f8a2e6 (highlighted) is the one that broke things. 3e7f1a8 is its revert: it undoes its effect without touching anything in between. All six commits are still there, in the same order, with the same hashes.

The three properties that set it apart from everything else in the module:

Property git revert
Does it rewrite history? No: it only adds
Do existing hashes change? No, none of them
Does the push have to be forced? No
Can it be used on published work? Yes: that is exactly what it is for
Is there a record of it? Yes, and that is an advantage

That last point is often misread. Somebody says "I want it to disappear, not to be left in the history". But leaving a record is the right thing: six months from now, when somebody looks at why that functionality is not there, the history will tell them. A deleted commit explains nothing; a reverted commit with a good message explains everything.

  1. Reverting Bruno's commit

The starting situation:

git switch main
git pull
git log --oneline -6
b52c9d1 (HEAD -> main, origin/main) Add the pending filter
e91d4a8 Extract the task element creation into its own function
7d3a8f4 Return focus to the text field after deleting a task
4f8a2e6 Change the localStorage storage format
c5d9b1e Document installation in the README
8b6d3c2 Add base styles for the list

The culprit is 4f8a2e6. First, look at it:

git show 4f8a2e6
commit 4f8a2e6...
Author: Bruno Salas <[email protected]>
Date:   Thu Jul 30 16:41:09 2026 +0200

    Change the localStorage storage format

diff --git a/app.js b/app.js
--- a/app.js
+++ b/app.js
@@ -8,11 +8,11 @@
 function loadTasks() {
-  const saved = localStorage.getItem('tasks');
-  return saved ? JSON.parse(saved) : [];
+  const saved = localStorage.getItem('taskmanager.tasks.v2');
+  return saved ? JSON.parse(saved) : [];
 }

 function saveTasks() {
-  localStorage.setItem('tasks', JSON.stringify(tasks));
+  localStorage.setItem('taskmanager.tasks.v2', JSON.stringify(tasks));
 }

There is the bug: the storage key was changed without migrating the existing data. Now, the revert:

git revert 4f8a2e6

Git opens the editor with a proposed message:

Revert "Change the localStorage storage format"

This reverts commit 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a.

Ana fills it out, because the default message says what but not why:

Revert "Change the localStorage storage format"

This reverts commit 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a.

Changing the localStorage key leaves anyone who already had data
saved under the old key with no tasks at all. Reverted to restore
the service; the change will come back once it includes the
migration of the existing data.
[main 3e7f1a8] Revert "Change the localStorage storage format"
 1 file changed, 2 insertions(+), 2 deletions(-)
git log --oneline -3
git show 3e7f1a8 --stat
3e7f1a8 (HEAD -> main) Revert "Change the localStorage storage format"
b52c9d1 (origin/main) Add the pending filter
e91d4a8 Extract the task element creation into its own function

The new commit's diff is exactly the inverse of the original:

@@ -8,11 +8,11 @@
 function loadTasks() {
-  const saved = localStorage.getItem('taskmanager.tasks.v2');
+  const saved = localStorage.getItem('tasks');
   return saved ? JSON.parse(saved) : [];
 }

And now the best part of all:

git push
To git.example.com:team/task-manager.git
   b52c9d1..3e7f1a8  main -> main

An ordinary push, with no --force, no --force-with-lease, and nobody having to be told to rewrite anything. Carla and Bruno will run git pull, receive one more commit and everything will work. That is the value of revert: undoing at no social cost.

git revert options:

Option What it does
-n / --no-commit Applies the inverse change to the index without committing
-e / --edit Opens the editor for the message (it does so by default anyway)
--no-edit Uses the automatic message without opening the editor
-m <n> Chooses the mainline parent when reverting a merge (section 7)
-s / --signoff Adds the Signed-off-by: line
--continue / --skip / --abort / --quit Controlling the process when there are conflicts

  1. revert versus reset versus --amend

All three "undo" things and they are constantly confused. The key difference is whether they touch the existing history, and that determines which you can use on published work.

git revert <sha> git reset <sha> git commit --amend
What it does Creates a new commit with the inverse change Moves the branch to another commit Replaces the last commit
Does it rewrite history? No Yes Yes
Do existing hashes change? No The later ones are left orphaned The last one changes hash
Does the history grow or shrink? Grows Shrinks (apparently) Stays the same
On published work Yes, it is the right tool No No
On local work You can, but there is usually something better Yes, perfect Yes, perfect
Does it leave a record? Yes No No
Scope Any commit in the history It only moves the tip Only the last commit
Does it need push --force? No Yes Yes
Typical case A published feature breaks something Undoing today's local commits Fixing the message or adding a forgotten item to the last commit

The way to decide, in one question: is what I want to undo already on the server where others may have it?

  • Yesgit revert. There is no safe alternative.
  • Noreset or --amend are cleaner, because the result does not drag along an undo commit nobody cares about.

An example that clarifies the difference in outcome. History A → B → C, and we want to remove B:

With revert:   A → B → C → B'        (B' undoes B; everything is still there)
With reset:    A                      (B and C stop being referenced)

Note the important detail about reset: it takes C with it too, because it moves the branch to A and everything after that is left orphaned. To remove only B while keeping C you have to rebase, not reset.

git reset is far more than fits here. It has three modes — --soft, --mixed and --hard — which behave very differently depending on which of the three areas from lesson 01-03 they affect, and it is the central tool for undoing local work. All of that, with the full tables and procedures, is the subject of lesson 09-02: Undoing Changes. Here we only needed the conceptual contrast: reset rewrites, revert does not.

  1. Reverting several commits and ranges

git revert accepts several commits and ranges, with the same notation as lesson 02-06:

# Several individual ones
git revert 4f8a2e6 7d3a8f4

# A range: reverts those after A up to B (A NOT included)
git revert A..B

# With A included
git revert A^..B

# The last three commits
git revert HEAD~3..HEAD

One fundamental detail: Git reverts them in reverse order, from the most recent to the oldest. That is the correct thing and what avoids unnecessary conflicts: if C builds on B, you have to undo C before B. That is why git revert HEAD~3..HEAD produces three revert commits in this order:

Revert "Commit C"
Revert "Commit B"
Revert "Commit A"
git log --oneline -7
9c4e7b2 (HEAD -> main) Revert "Add the pending filter"
5f1d8a3 Revert "Extract the task element creation into its own function"
2a8c6f9 Revert "Return focus to the text field after deleting a task"
b52c9d1 Add the pending filter
e91d4a8 Extract the task element creation into its own function
7d3a8f4 Return focus to the text field after deleting a task
4f8a2e6 Change the localStorage storage format

Three commits undone, three revert commits, and a history that tells exactly what happened. If you would rather it were just one, on to the next section.

  1. -n: grouping several reverts into one commit

Three revert commits in a row are usually noise: what happened conceptually was one decision ("we are taking the filter feature out"), not three. -n (--no-commit) applies the inverse changes to the index without committing, and you commit at the end:

git revert -n HEAD~3..HEAD
git status --short
M  app.js
M  styles.css
M  index.html
git commit -m "Revert the pending filter functionality

Reverts commits b52c9d1, e91d4a8 and 7d3a8f4 because the filter
leaves the counter out of step when there are hidden tasks. It will
come back once the counter is computed over the full list."
[main 7e2c9f4] Revert the pending filter functionality
 3 files changed, 18 insertions(+), 42 deletions(-)

One commit, one message, one decision. It is almost always the best way of reverting a set of related commits.

While you are in the middle of a revert -n, the repository is in a special state (there is a .git/REVERT_HEAD file). If you have second thoughts:

git revert --abort     # undoes everything accumulated
git revert --quit      # leaves the state but KEEPS what has been applied to the index

  1. Conflicts while reverting

A revert is, underneath, a merge: Git tries to apply an inverse patch onto a context that may have changed since. If the code you want to undo has been modified afterwards, it conflicts.

git revert 4f8a2e6
Auto-merging app.js
CONFLICT (content): Merge conflict in app.js
error: could not revert 4f8a2e6... Change the localStorage storage format
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run "git revert --continue".
hint: You can instead skip this commit with "git revert --skip".
hint: To abort and get back to the state before "git revert",
hint: run "git revert --abort".

The mechanics of resolution are the usual ones (lesson 03-05). What is specific:

Command What it does
git revert --continue Commits the revert with your resolution and carries on with the next one
git revert --skip Discards that revert and carries on with the rest of the range
git revert --abort Cancels the whole operation and returns to the initial state
git revert --quit Leaves the revert state, keeping what has been applied

As with cherry-pick (lesson 05-03), here ours and theirs are NOT inverted: ours is your branch, theirs is the inverse change being applied. The inversion was a peculiarity of rebase.

And a warning worth taking to heart: when a revert conflicts, stop and think. The conflict is telling you that somebody built on top of what you want to undo. Reverting by brute force can leave code calling a function that has just disappeared. Check who depends on it before carrying on:

git log --oneline 4f8a2e6..HEAD -- app.js          # what has been touched since in that file
git log -S "taskmanager.tasks.v2" --oneline        # who else uses what I am removing

-S is the pickaxe search from lesson 02-06, and here it is exactly the right tool.

  1. Reverting a merge commit: -m 1 and -m 2

This is the case that trips everybody up the first time.

git revert c2a8f1e
error: commit c2a8f1ea4b7d9c3e5f2a8b6d1c9e4f7a3b5d2c8e is a merge but no -m option was given.
fatal: revert failed

Why it fails. Reverting means applying the inverse diff. But a merge commit has two parents, and therefore two possible diffs: one against the first parent and one against the second. Git cannot guess which you want, so it refuses and forces you to say.

gitGraph
   commit id: "8b6d3c2"
   commit id: "c5d9b1e"
   branch feature/pending-filter
   commit id: "a1e5c93"
   commit id: "6f2b9d4"
   checkout main
   commit id: "7d3a8f4"
   merge feature/pending-filter id: "c2a8f1e"
   commit id: "e91d4a8"

The merge commit's parents, in order:

git show --format='%h %p %s' -s c2a8f1e
c2a8f1e 7d3a8f4 6f2b9d4 Merge feature/pending-filter
Number Parent What it is
1 7d3a8f4 The branch you were on when you merged: main
2 6f2b9d4 The branch you merged in: feature/pending-filter

The first parent is always the destination branch, because the merge commit was created while you were standing on it. The second (and subsequent ones, in an octopus merge) are the branches brought in.

-m 1 means: "keep parent 1's line", that is, undo everything the merged branch contributed and keep main as it was. That is what you want 99% of the time.

git revert -m 1 c2a8f1e
[main 4d7c1a9] Revert "Merge feature/pending-filter"
 3 files changed, 4 insertions(+), 47 deletions(-)

-m 2 would do the opposite: undo what main contributed and keep the branch. It is an extremely rare operation and almost always means you have picked the wrong command.

The mnemonic: -m 1 = "keep the mainline, throw away what I brought in".

A practical tip: before reverting a merge, look at its parents. git log --format='%h %p %s' -1 <sha> gives them to you in a second, and git show <sha>^1 / git show <sha>^2 shows you each one. Two commands and you avoid the mistake.

  1. The problem of the reverted branch that will not go back in

Here comes the subtle consequence, the one that bites weeks later, and the reason reverting a merge deserves a section of its own.

Bruno reverted the merge of feature/pending-filter. A fortnight later he fixes the counter problem and wants to integrate the branch again:

git switch main
git merge feature/pending-filter
Already up to date.

"Already up to date", and there is no sign of the filter on main. Bewildering.

Why it happens. Remember the three-way merge of lesson 03-03: Git finds the common ancestor and compares. But the common ancestor of main and feature/pending-filter is now the reverted merge itself (c2a8f1e), because that merge is still in the history and makes all of the branch's commits reachable from main.

From the graph's point of view, those commits are already integrated. That their content was undone afterwards by 4d7c1a9 is irrelevant to the calculation: Git reasons about reachability, not about content.

gitGraph
   commit id: "c5d9b1e"
   branch feature/pending-filter
   commit id: "a1e5c93"
   commit id: "6f2b9d4"
   checkout main
   commit id: "7d3a8f4"
   merge feature/pending-filter id: "c2a8f1e"
   commit id: "4d7c1a9" type: HIGHLIGHT
   commit id: "e91d4a8"

The highlighted commit is the revert. The branch is connected to main's graph; the content is not.

And if Bruno adds new commits to the branch and merges again, the result is worse still: only the new commits go in, on top of a main that is missing the base they were expecting. Code calling functions that do not exist, and no conflict to warn you.

The three solutions, in order of preference:

A. Revert the revert. The simplest and the one used almost always:

git revert 4d7c1a9
[main 9e2c6b1] Revert "Revert \"Merge feature/pending-filter\""
 3 files changed, 47 insertions(+), 4 deletions(-)

That brings the branch's content back to main. From then on, the new commits Bruno makes on the branch will merge normally. The history looks a little comical — a "Revert of the Revert" — but it is correct, honest and safe. If you like, edit the message to explain it:

Reintegrate the pending filter

Reverts the revert 4d7c1a9. The counter problem is fixed in
8f3d2c7, so the functionality goes back in.

B. Redo the branch on top of the current main. If the branch was short, creating a new branch from main and bringing the commits over with git cherry-pick (lesson 05-03) gives a cleaner history:

git switch -c feature/pending-filter-v2 main
git cherry-pick a1e5c93 6f2b9d4
# ... fix the counter problem ...

The new commits have new hashes, they are not reachable from main, and the later merge works normally.

C. Force the merge, ignoring the ancestor. It exists, but it is hardly ever a good idea; we mention it so that you recognise it if you see it:

git merge --no-commit --no-ff feature/pending-filter

The underlying lesson, and it deserves underlining:

Reverting a merge undoes the content, not the topology. The graph still says that branch was integrated.

That is why, when a whole branch has to come out and it is known that it will come back, many teams prefer to revert the individual commits rather than the merge, or simply not to merge until they are sure. And that is why it is worth knowing this before reverting a merge, not afterwards.

  1. Reverting a revert

We have already used it in the previous section, but it is worth stating: a revert is a perfectly ordinary commit, so it can be reverted.

git revert <sha-of-the-revert>

The result is that the original change comes back. It is the canonical way of saying "I was too hasty in undoing this".

And since it is symmetrical, it works as many times as you like: reverting the revert of the revert undoes it again. Each step is a new commit, nobody forces anything and the history tells the complete sequence of decisions. Inelegant, but rigorously safe.

  1. When to revert and when not to

Situation Tool
A published commit breaks something git revert
A published merge has to be undone git revert -m 1
I want to undo local commits I have not pushed git reset (lesson 09-02)
I got the message of the last commit wrong (unpublished) git commit --amend (lesson 02-04)
I want to remove a commit from the middle of an unpublished branch git rebase -i with drop (lesson 05-02)
I want to discard uncommitted changes git restore (lesson 02-04)
I want to try an old version again without changing anything git switch --detach <sha> (lesson 03-02)

And two matters of judgement that are not technical:

Revert early. If something is broken on main, reverting first and diagnosing afterwards is almost always the right call. A broken main blocks the whole team; the reverted commit is not lost and can be reintroduced once fixed. It is a decision about service, not about pride.

Write down why. The automatic message (This reverts commit ...) says what was undone but not why. Adding three lines explaining the reason and what would be needed to try again turns a revert into useful information six months from now. Message conventions are the subject of lesson 08-01, but this particular case is worth mentioning here: an unexplained revert is one of the most frustrating entries you can find in a history.

Common Mistakes and Tips

Mistake 1: using reset instead of revert on published work. It is the fast lane to the scenario in section 9 of lesson 05-01: push --force, colleagues with divergent histories and an afternoon of coordination. If it is on the server, revert.

Mistake 2: trying to revert a merge without -m. Git fails with a clear message. Remember: -m 1 in 99% of cases.

Mistake 3: getting the order of the parents wrong. -m 1 is the branch you were on (usually main); -m 2 is the one you brought in. Check with git show --format='%h %p %s' -s <sha> before deciding.

Mistake 4: assuming that after reverting a merge the branch will go back in by itself. It will not: Already up to date. You have to revert the revert or redo the branch.

Mistake 5: accepting the default message and leaving it at that. It says what, not why. Thirty seconds of explanation save a future investigation.

Mistake 6: reverting a commit others built on without checking the dependencies. You can leave code calling something that no longer exists. Use git log -S and git log <sha>..HEAD -- <file> first.

Mistake 7: reverting on the wrong branch. git revert acts on the current branch. Check with git status before launching it, especially if you have just been looking at another branch.

Tip 1: always look at the commit before reverting it. git show <sha> takes three seconds and tells you whether the change is self-contained or drags half the project with it.

Tip 2: for several related commits, use -n and commit once. One decision, one commit.

Tip 3: if the revert conflicts, read it as a signal. Somebody built on top. It may be that the right way out is not to revert but to fix forwards.

Tip 4: reverting is reversible. If you were too hasty, a git revert on the revert brings everything back. Nothing you do with this command is unrecoverable.

Tip 5: on main, revert first and think afterwards. Restoring the service is the priority; the diagnosis can be done calmly on a branch.

Exercises

Exercise 1: reverting a published commit

In a repository with five commits, of which the third introduces a bug:

  1. Revert the third commit with a message explaining the reason.
  2. Demonstrate that none of the earlier hashes has changed.
  3. Demonstrate that the file's content is what it was before that commit, apart from what the fourth and fifth contributed.
  4. Compare conceptually with what would have happened using git reset --hard on the second commit (there is no need to run it on the main branch: do it on a copy).

Exercise 2: reverting a merge and integrating it again

  1. Create a branch with two commits and merge it into main with --no-ff.
  2. Add one more commit to main.
  3. Revert the merge with -m 1 and check that the branch's content has disappeared.
  4. Try to merge the branch again and observe the message.
  5. Resolve it by reverting the revert and check that the content comes back.
  6. Draw (or describe) the resulting graph.

Exercise 3: grouping reverts

With a branch that has four commits on top of main, revert the last three in a single commit using -n. Verify with git show --stat that the resulting commit contains the revert of all three, and with git diff that the state of the project matches the one after the first.

Solutions

Solution 1:

mkdir /tmp/practice-revert && cd /tmp/practice-revert
git init -b main
printf 'line 1\n' > f.txt && git add . && git commit -m "One"
printf 'line 1\nline 2\n' > f.txt && git commit -am "Two"
printf 'line 1\nline 2\nERROR\n' > f.txt && git commit -am "Three (the bug)"
printf 'line 1\nline 2\nERROR\nline 4\n' > f.txt && git commit -am "Four"
printf 'line 1\nline 2\nERROR\nline 4\nline 5\n' > f.txt && git commit -am "Five"

git log --oneline
7c2e9b4 Five
3f8a1d6 Four
9b5c7e2 Three (the bug)
1d4f8a3 Two
6e2b9c7 One
# 1. Revert the third one
git revert 9b5c7e2 --no-edit
git commit --amend -m "Revert \"Three (the bug)\"

This reverts commit 9b5c7e2.

The ERROR line slipped in through an accidental paste and breaks
the parsing of the file. Reverted to restore the behaviour."
# 2. The earlier hashes have not changed
git log --oneline
8a1f6c3 Revert "Three (the bug)"
7c2e9b4 Five
3f8a1d6 Four
9b5c7e2 Three (the bug)
1d4f8a3 Two
6e2b9c7 One

The five original commits keep their hashes; only one has been added on top.

# 3. The content
cat f.txt
line 1
line 2
line 4
line 5

ERROR has gone and the lines from Four and Five are preserved.

# 4. Comparison with reset, on a copy
git switch -c reset-copy 7c2e9b4
git reset --hard 1d4f8a3
git log --oneline
cat f.txt
1d4f8a3 Two
6e2b9c7 One
line 1
line 2

The reset has swept away Three, Four and Five: it moves the branch, it does not undo one specific change. And if that branch were published, the push would be rejected and would have to be forced.

Solution 2:

mkdir /tmp/practice-revert-merge && cd /tmp/practice-revert-merge
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"

git switch -c feature/filter
echo "filter part 1" > filter.js && git add . && git commit -m "Filter part 1"
echo "filter part 2" >> filter.js && git commit -am "Filter part 2"

git switch main
git merge --no-ff feature/filter -m "Merge feature/filter"
echo "something else" > other.txt && git add . && git commit -m "Other work on main"

git log --oneline --graph
* 5c1e8f2 (HEAD -> main) Other work on main
*   9d3b7a4 Merge feature/filter
|\
| * 2f8c6e1 (feature/filter) Filter part 2
| * 7a4d9b3 Filter part 1
|/
* 1e6f2c8 Base
# 3. Revert the merge
git show --format='%h %p %s' -s 9d3b7a4
git revert -m 1 9d3b7a4 --no-edit
ls
9d3b7a4 1e6f2c8 2f8c6e1 Merge feature/filter
f.txt  other.txt

filter.js has gone: the branch's content is undone.

# 4. Try to merge again
git merge feature/filter
Already up to date.

There is the problem: the graph says it is already integrated.

# 5. Revert the revert
git log --oneline -1
git revert HEAD --no-edit
ls
cat filter.js
4b8e2c9 Revert "Merge feature/filter"
f.txt  filter.js  other.txt
filter part 1
filter part 2

The content is back.

# 6. The graph
git log --oneline --graph
* 8f2d6a1 (HEAD -> main) Revert "Revert "Merge feature/filter""
* 4b8e2c9 Revert "Merge feature/filter"
* 5c1e8f2 Other work on main
*   9d3b7a4 Merge feature/filter
|\
| * 2f8c6e1 (feature/filter) Filter part 2
| * 7a4d9b3 Filter part 1
|/
* 1e6f2c8 Base

Eight commits telling the whole story: it went in, it came out, it went back in. Nothing has been rewritten and nobody has forced anything.

Solution 3:

mkdir /tmp/practice-revert-n && cd /tmp/practice-revert-n
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"

echo "a" > a.txt && git add . && git commit -m "Add a"
echo "b" > b.txt && git add . && git commit -m "Add b"
echo "c" > c.txt && git add . && git commit -m "Add c"
echo "d" > d.txt && git add . && git commit -m "Add d"

git log --oneline
9c2f7e4 Add d
5a8d1b6 Add c
3e7b9c2 Add b
8f4c2a9 Add a
1d6e8f3 Base
# Revert the last three in a single commit
git revert -n HEAD~3..HEAD
git status --short
D  b.txt
D  c.txt
D  d.txt
git commit -m "Revert files b, c and d

Reverts 3e7b9c2, 5a8d1b6 and 9c2f7e4: the one-file-per-letter
approach was not what was intended. It will come back with the
correct structure."

git show --stat HEAD
commit 2b9e6c1...
    Revert files b, c and d

 b.txt | 1 -
 c.txt | 1 -
 d.txt | 1 -
 3 files changed, 3 deletions(-)
# The state matches the one after "Add a"
git diff 8f4c2a9 HEAD
ls
(no output: they are identical)
a.txt  f.txt

One single revert commit and a state identical to that of the first commit in the series, without having touched a single hash.

Conclusion

git revert is the missing piece: the way of undoing that respects the golden rule. The essentials:

  • Reverting does not delete: it adds. Git creates a new commit with the inverse change. No existing hash changes, the push does not have to be forced and there is a public record of the decision.
  • It is the only safe way of undoing something published. reset and --amend rewrite history and are only good for work that is still yours; lesson 09-02 covers reset thoroughly.
  • It accepts several commits and ranges, which it reverts from the most recent to the oldest; -n lets you accumulate several reverts and commit them as a single decision.
  • Conflicts are resolved as always, with --continue, --skip and --abort, and here ours/theirs are not inverted. A conflict while reverting usually means somebody built on top: stop and look.
  • Reverting a merge requires -m to choose the mainline parent: -m 1 (the branch you were on, nearly always main) or -m 2 (the one you brought in, extremely rare).
  • Reverting a merge undoes the content, not the topology. The branch is still reachable, so merging it again says Already up to date. It is resolved by reverting the revert, or by redoing the branch with cherry-pick.
  • Everything is reversible: a revert is an ordinary commit and can itself be reverted.
  • And the judgement call: on main, revert first and diagnose afterwards, and always explain why.

Closing module 5

With this lesson you close the block of advanced operations. Looking back at what has changed in the way you work:

  • git rebase to reapply commits onto a different base and obtain a linear history, knowing that it creates new commits.
  • git rebase -i to reorder, join, split, rename and remove commits before publishing them, with --fixup/--autosquash as the daily flow and --exec as the validation net.
  • git cherry-pick to transplant specific commits between branches that are not going to be merged, with -x to leave a trail and git cherry to detect duplicates.
  • git stash to set half-finished work aside for minutes or hours, with -u for untracked files and git stash branch when it no longer fits.
  • Annotated tags to mark versions permanently, with SemVer, --follow-tags and git describe.
  • git revert to undo what is published without rewriting anything.

And above all of them, the golden rule: do not rewrite history you have already published and that others may hold. Rebase and interactive rebase, before publishing. Cherry-pick and revert, always safe. It is the line that separates expert use from reckless use.

What comes next

The team now knows how to build the history and how to manipulate it with judgement. What it lacks now is learning how to get value out of it.

Because task-manager's history is an enormous database of information that we have barely queried so far. Written in it is who wrote each line and why, in which exact commit something that used to work stopped working, and what checks each commit ought to pass before it exists. And there are new problems: the project is beginning to depend on an internal library that lives in its own repository, and Carla needs to work on two branches at once without spending the day running stash.

In module 6: Git Tools and Techniques we shall see hooks for automating checks on every commit and every push, git bisect for finding by binary search the exact commit that introduced a bug, git blame for reconstructing the history of each line, aliases and the advanced git log formats for querying all of that comfortably, submodules for composing projects out of several repositories, and git worktree for having several working copies of the same repository at once.

We begin with automation, in lesson 06-01: Using Git Hooks.

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