This course will be delivered in blended learning mode (i.e., a mix of online and F2F activities) this semester.

Tools → Git and GitHub →

T4L5. Rewriting History to Start Over


Git can also reset the revision history to a specific point so that you can start over from that point.

This lesson covers that part.

Suppose you realise your last few commits have gone in the wrong direction, and you want to go back to an earlier commit and continue from there — as if the “bad” commits never happened. Git’s reset feature can help you do that.

Git reset moves the tip of the current branch to a specific commit, optionally adjusting your staged and unstaged changes to match. This effectively rewrites the branch's history by discarding any commits that came after that point.

Resetting is different from the checkout feature:

  • Reset: Lets you start over from a past state. It rewrites history by moving the branch ref to a new location.
  • Checkout: Lets you explore a past state without rewriting history. It just moves the HEAD ref.
C3 masterHEAD (original tip of the branch)
|
C2
|
C1


[reset to C2...]

C3commit no longer in the master branch!
|
C2 masterHEAD (the new tip)
|
C1

There are three types of resets: soft, mixed, hard. All three move the branch pointer to a new commit, but they vary based on what happens to the staging area and the working directory.

  • soft reset: Moves the cumulative changes from the discarded commits into the staging area, waiting to be committed again. Any staged and unstaged changes that existed before the reset will remain untouched.
  • mixed reset: Cumulative changes from the discarded commits, and any existing staged changes, are moved into the working directory.
  • hard reset: All staged and unstaged changes are discarded. Both the working directory and the staging area are aligned with the target commit (as if no changes were done after that commit).
HANDS-ON: Resetting to past commits

Preparation First, set the stage as follows (e.g., in the things repo):
i) Add four commits that are supposedly 'bad' commits.
ii) Do a 'bad' change to one file and stage it.
iii) Do a 'bad' change to another file, but don't stage it.

B4 masterHEADAdd incorrect.txt
|
B3Incorrectly update fruits.txt
|
B2Incorrectly update shapes.txt
|
B1Incorrectly update colours.txt
|
C4Update fruits list
|

The following commands can be used to add commits B1-B4:

echo "bad colour" >> colours.txt
git commit -am "Incorrectly update colours.txt"

echo "bad shape" >> shapes.txt
git commit -am "Incorrectly update shapes.txt"

echo "bad fruit" >> fruits.txt
git commit -am "Incorrectly update fruits.txt"

echo "bad line" >> incorrect.txt
git add incorrect.txt
git commit -m "Add incorrect.txt"

echo "another bad colour" >> colours.txt
git add colours.txt

echo "another bad shape" >> shapes.txt

Now we have some 'bad' commits and some 'bad' changes in both the staging area and the working directory. Let's use the reset feature to get rid of all of them, but do it in three steps so that you can learn all three types of resets.

1 Do a soft reset to B2 (i.e., discard last two commits). Verify,

  • the master branch is now pointing at B2, and,
  • the changes that were in the discarded commits (i.e., B3 and B4) are now in the staging area.

Use the git reset --soft <commit> command to do a soft reset.

git reset --soft HEAD~2

You can run the following commands to verify the current status of the repo is as expected.

git status                    # check overall status
git log --oneline --decorate  # check the branch tip
git diff                      # check unstaged changes
git diff --staged             # check staged changes

Right-click on the commit that you want to reset to, and choose Reset <branch-name> to this commit option.

In the next dialog, choose Soft - keep all local changes.


2 Do a mixed reset to commit B1. Verify,

  • the master branch is now pointing at B1.
  • the staging area is empty.
  • the accumulated changes from all three discarded commits (including those from the previous soft reset) are now appearing as unstaged changes in the working directory.
    Note how incorrect.txt appears as an 'untracked' file -- this is because unstaging a change of type 'add file' results in an untracked file.

Use the git reset --mixed <commit> command to do a mixed reset. The --mixed flag is the default, and can be omitted.

git reset HEAD~1

Verify the repo status, as before.


Similar to the previous reset, but choose the Mixed - keep working copy but reset index option in the reset dialog.


3 Do a hard reset to commit C4. Verify,

  • the master branch is now pointing at C4 i.e., all 'bad' commits are gone.
  • the staging area is empty.
  • there are no unstaged changes (except for the untracked files incorrect.txt -- Git leaves untracked files alone, as untracked files are not meant to be under Git's control).

Use the git reset --hard <commit> command.

git reset --hard HEAD~1

Verify the repo status, as before.


Similar to the previous reset, but choose the Hard - discard all working copy changes option.


done!

Rewriting history can cause your local repo to diverge from its remote counterpart. For example, if you discard earlier commits and create new ones in their place, and you’ve already pushed the original commits to a remote repository, your local branch history will no longer match the corresponding remote branch. Git refers to this as a diverged history.

To protect the integrity of the remote, Git will reject attempts to push a diverged branch using a normal push. If you want to overwrite the remote history with your local version, you must perform a force push.

HANDS-ON: Force-push commits

Preparation Choose a local-remote repo pair under your control e.g., the things repo from Tour 2: Backing up a Repo on the Cloud.

1 Rewrite the last commit: Reset the current branch back by one commit, and add a new commit.
For example, you can use the following commands.

git reset --hard HEAD~1
echo "water" >> drinks.txt
git add .
git commit -m "Add drinks.txt"

2 Observe how the local branch is diverged.

git log --oneline --graph --all
* fc1d04e (HEAD -> master) Add drinks.txt
| * e60deae (upstream/master, origin/master) Update fruits list
|/
* f761ea6 (tag: v1.0) Add colours.txt, shapes.txt
* 2bedace (tag: v0.9) Add figs to fruits.txt
* d5f91de Add fruits.txt

3 Attempt to push to the remote. Observe Git rejects the push.

git push origin master
To https://github.com/.../things.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/.../things.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: ...

4 Do a force-push.

You can use the --force (or -f) flag to force push.

git push -f origin master

A safer alternative to --force is --force-with-lease which overwrites the remote branch only if it hasn’t changed since you last fetched it (i.e., only if remote doesn't have recent changes that you are unaware of):

git push --force-with-lease origin master

done!

DETOUR: Resetting Uncommitted Changes

At times, you might need to get rid of uncommitted changes so that you have a fresh start to the next commit.

To get rid of uncommitted changes, you can reset the repo to the last commit (i.e., HEAD):

The command git reset (without specifying a commit) defaults to git reset HEAD.

  • git reset: moves any staged changes to working directory (i.e., unstage).

  • git reset --hard: get rid of any staged and unstaged changes.




Related DETOUR: Updating the Last Commit

Git allows you to amend the most recent commit. This is useful when you realise there’s something you’d like to change — e.g., fix a typo in the commit message, or to exclude some unintended change from the commit.

That aspect is covered in a detour in the lesson T5L3. Reorganising Commits.


DETOUR: Undoing/Deleting Recent Commits

How do you undo or delete the last few commits if you realise they were incorrect, unnecessary, or done too soon?

Undoing or deleting recent n commits is easily accomplished with Git's reset feature.

  • To delete recent n commits and discard the those changes entirely, do a hard reset the commit HEAD~n e.g.,
    git reset --hard HEAD~3
    
  • To undo recent n commits, but keep changes staged, do a soft reset the commit HEAD~n e.g.,
    git reset --soft HEAD~3
    
  • To undo recent n commits, and move changes to the working directory, do a mixed reset the commit HEAD~n e.g.,
    git reset --mixed HEAD~3
    

To do the above for the most recent commit only, use HEAD~1 (or just HEAD~).


DETOUR: Resetting a Remote-Tracking Branch Ref

Suppose you moved back the current branch ref by two commits, as follows:

git reset --hard HEAD~2
C4 masterHEAD origin/master
|
C3
|
C2
|
C1


C4 origin/master
|
C3
|
C2 masterHEAD
|
C1

If you now wish to move back the remote-tracking branch ref by two commits, so that the local repo 'forgets' that it previously pushed two more commits to the remote, you can do:

git update-ref refs/remotes/origin/master HEAD
C4 origin/master
|
C3
|
C2 masterHEAD
|
C1


|
|
C2 masterHEAD origin/master
|
C1

The git update-ref refs/remotes/origin/master HEAD commands resets the remote-tracking branch ref origin/master to follow the current HEAD.

update-ref is an example of what are known as Git plumbing commands -- lower-level commands used by Git internally. In contrast, day-to-day Git commands (such as commit, log, push etc.) are known as porcelain commands (as in, in bathrooms we see the porcelain parts but not the plumbing parts that operates below the surface to make everything work).