Software Engineering for Self-Directed Learners »

Tour 4: Using the Revision History of a Repo

Destination: To be able to make use of the revision history stored by Git.

Motivation: Having put in effort to record the revision history of the working folder, it only makes sense that we use the revision history to our benefit. For example, to be able to answer questions such as "What did I change in this file since last Monday?"

Lesson plan:

It is useful to be able to visualise the commits timeline, aka the revision graph.

   Lesson: Examining the Revision History covers that part.

It is also useful to be able to see what changes were included in a specific commit.

   Lesson: Examining a Commit covers that part.

When working with many commits, it helps to tag specific commits with custom names so they’re easier to refer to later.

   Lesson: Tagging Commits covers that part.

Git can tell you the net effect of changes between two points of history.

   Lesson: Comparing Points of History covers that part.

Another useful feature of revision control is to be able to view the working directory as it was at a specific point in history, by checking out a commit created at that point.

   Lesson: Traversing to a Specific Commit covers that part.

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

   Lesson: Rewriting History to Start Over covers that part.

Git can add a new commit to reverse the changes done in a specific past commit, called reverting a commit.

   Lesson: Reverting a Specific Commit covers that part.

Lesson: Examining the Revision History


It is useful to be able to visualise the commits timeline, aka the revision graph.

This lesson covers that part.

The Git data model consists of two types of entities: objects and refs (short for references). In this lesson, you will encounter examples of both.

A Git revision graph is visualisation of a repo's revision history, contains examples of both objects and refs. First, let us learn to work with simpler revision graphs consisting of one branch, such as the one given below.

C3
|
C2
|
C1

Nodes in the revision graph represent commits.

  • A commit is one of four main types of Git objects (blobs, trees, and annotated tags are the other three, to be covered later).
  • A commit identified by its SHA value. A SHA (Secure Hash Algorithm) value is a unique identifier generated by Git to represent each commit. It is produced by using SHA-1 (i.e., one of the algorithms in the SHA family of cryptographic hash functions) on the entire content of the commit. It's a 40-character hexadecimal string (e.g., f761ea63738a67258628e9e54095b88ea67d95e2) that acts like a fingerprint, ensuring that every commit can be referenced unambiguously.
  • A commit is a full snapshot of the working directory, constructed based on the previous commit, and the changes staged. The previous commit a commit is based on is called the parent commit (some commits can have multiple parent commits -- we’ll cover that later).

Edges in the revision graph represent links between a commit and its parent commit(s) In some revision graph visualisations, you might see arrows (instead of lines) showing how each commit points to its parent commit.

C3
C2
C1

Git uses refs to name and keep track of various points in a repository’s history. These refs are essentially 'named-pointers' that can serve as bookmarks to reach a certain point in the revision graph using the ref name.

C3 masterHEAD
|
C2
|
C1

In the revision graph above, there are two refs master and  HEAD.

  • master is a branch ref. A branch points to the latest commit on a branch (in this visualisation, the commit shown alongside the ref is the one it points to i.e., C3). When you create a new commit, the ref of the branch moves to the new commit.
  • HEAD is a special ref. Normally, it points to the current branch (in this example, it is pointing to the master branch), and moves together with the branch ref.
C3 masterHEAD origin/master
|
C2
|
C1

In the revision graph above you see a third type of ref ( origin/master). This is a remote-tracking branch ref that represents the state of a branch in a remote repository (if you previously set up the branch to track a remote branch). In this example, the master branch in the remote origin is also at the commit C3 (which means you have not created new commits after you pushed to the remote).

If you now create a new commit C4, the state of the revision graph will be as follows:

C4 masterHEAD
|
C3 origin/master
|
C2
|
C1

Explanation: When you create C4, the current branch master move to point to C4, and HEAD moves along with it. However, the master branch in the remote origin remains at C3 (because you have not pushed C4 yet). The origin/master ref will move to C4 after you push your repo to the remote again.

HANDS-ON: View the revision graph

Use Git features to examine the revision graph of a simple repo. For this, use a repo with just a few commits and only one branch for this hands-on practical.

1 First, use a simple git log to view the list of commits.

git log
commit f761ea63738a... (HEAD -> master, origin/master)
Author: ... <...@...>
Date:   Sat ...

    Add colours.txt, shapes.txt

commit 2bedace69990...
Author: ... <...@...>
Date:   Sat ...

    Add figs to fruits.txt

commit d5f91de5f0b5...
Author: ... <...@...>
Date:   Fri ...

    Add fruits.txt

For comparison, given below the visual representation of the same revision graph. As you can see, the log output shows the refs slightly differently, but it is not hard to see what they mean.

C3 masterHEAD origin/masterAdd colours.txt, shapes.txt
|
C2Add figs to fruits.txt
|
C1Add fruits.txt

2 Use the --oneline flag to get a more concise view. Note how the commit SHA has been truncated to first seven characters (first seven characters of a commit SHA is enough for Git to identify a commit).

git log --oneline
f761ea6 (HEAD -> master, origin/master) Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt

3 The --graph flag makes the result closer to a graphical revision graph. Note the * that indicates a node in a revision graph.

git log --oneline --graph
* f761ea6 (HEAD -> master, origin/master) Add colours.txt, shapes.txt
* 2bedace Add figs to fruits.txt
* d5f91de Add fruits.txt

The --graph option is more useful when examining a more complicated revision graph consisting of multiple parallel branches.


Click the History to see the revision graph.

  • In some versions of Sourcetree, the HEAD ref may not be shown -- it is implied that the HEAD ref is pointing to the same commit the currently active branch ref is pointing.
  • If the remote-tracking branch ref (e.g., origin/master) is not showing up, you may need to enable the Show Remote Branches option.


Observe how the revision graph changes as you add a commit, and push that commit to the remote repo.

For example, we can update the fruits.txt in the things repo as follows, and commit it with the message Update fruits list.

fruits.txt
apples
bananas
cherries
dragon fruits
elderberries
figs


[update file as...]

fruits.txt
apples, apricots
bananas
blueberries
cherries
dragon fruits
figs

After creating the new commit, the output of git log --oneline --decorate should be of the form:

e60deae (HEAD -> master) Update fruits list
f761ea6 (origin/master) Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt

After pushing the new commit to the remote, the remote-tracking branch ref should move to the new commit:

e60deae (HEAD -> master, origin/master) Update fruits list
f761ea6 Add colours.txt, shapes.txt
2bedace Add figs to fruits.txt
d5f91de Add fruits.txt

After creating the new commit, the branch ref (master) will move to the new commit, but the remote-tracking branch ref (origin/master) will remain at the previous commit. Sourcetree will also indicate that your local branch is 1 commit ahead (i.e., has one more commit than) the remote-tracking branch.

After pushing the new commit to the remote, the remote-tracking branch ref should move to the new commit:



Lesson: Examining a Commit


It is also useful to be able to see what changes were included in a specific commit.

This lesson covers that part.

When you examine a commit, normally what you see is the 'changes since the previous commit'. This should not be interpreted as Git commits contain only the changes. As you recall, a Git commit contains a full snapshot of the working directory. However, tools used to examine commits show only the changes, as that is the more informative part.

Git shows changes included in a commit by dynamically calculating the difference between the snapshots stored in the target commit and the parent commit. This is because Git commits stores snapshots of the working directory, not changes themselves.

To address a specific commit, you can use its SHA (e.g., e60deaeb2964bf2ebc907b7416efc890c9d4914b). In fact, just the first few characters of the SHA is enough to uniquely address a commit (e.g., e60deae).
Naturally, a commit can be addressed using any ref pointing to it too (e.g., HEAD, master).
Another related technique is to use the <ref>~<n> notation (e.g., HEAD~1) to address the commit that is n commits prior to the commit pointed by <ref> i.e., "start with the commit pointed by <ref> and go back n commits".
A shortcut of this notation is to use HEAD~, HEAD~~, HEAD~~~, ... to mean HEAD~1, HEAD~2, HEAD~3 etc.

C3 master HEADThis commit can be addressed as HEAD or master
|
C2Can be addressed as HEAD~1 or master~1
|
C1Can be addressed as HEAD~2 or master~2

Git uses the diff format to show file changes in a commit. The diff format was originally developed for Unix, later extended with headers and metadata to show changes between file versions and commits. Here is an example diff showing the changes to a file.

diff --git a/fruits.txt b/fruits.txt
index 7d0a594..f84d1c9 100644
--- a/fruits.txt
+++ b/fruits.txt
@@ -1,6 +1,6 @@
-apples
+apples, apricots
 bananas
 cherries
 dragon fruits
-elderberries
 figs
@@ -20,2 +20,3 @@
 oranges
+pears
 raisins
diff --git a/colours.txt b/colours.txt
new file mode 100644
index 0000000..55c8449
--- /dev/null
+++ b/colours.txt
@@ -0,0 +1 @@
+a file for colours

A Git diff can consist of multiple file diffs, one for each changed file. Each file diff can contain one or more hunk i.e., a localised group of changes within the file — including lines added, removed, or left unchanged (included for context).

Here is the explanation of the above diff:

All changes in the commit:

file diff for fruits.txt:

diff --git a/fruits.txt b/fruits.txt
index 7d0a594..f84d1c9 100644
--- a/fruits.txt
+++ b/fruits.txt

Hunk 1:

@@ -1,6 +1,6 @@
-apples
+apples, apricots
 bananas
 cherries
 dragon fruits
-elderberries
 figs

Hunk 2:

@@ -20,2 +20,3 @@
 oranges
+pears
 raisins

file diff for colours.txt:

diff --git a/colours.txt b/colours.txt
new file mode 100644
index 0000000..55c8449
--- /dev/null
+++ b/colours.txt

Hunk 1:

@@ -0,0 +1 @@
+a file for colours

Here is an explanation of the diff:

Part of Diff Explanation
diff --git a/fruits.txt b/fruits.txt The diff header, indicating that it is comparing the file fruits.txt between two versions: the old (a/) and new (b/).
index 7d0a594..f84d1c9 100644 Shows the SHA of the file (not the commit) before and after the change, and the file mode (100 means a regular file, 644 are file permission indicators).
--- a/fruits.txt
+++ b/fruits.txt
Marks the old version of the file (a/fruits.txt) and the new version of the file (b/fruits.txt).
@@ -1,6 +1,6 @@ This hunk header shows that lines 1-6 (i.e., starting at line 1, showing 6 lines) in the old file were compared with lines 1–6 in the new file.
-apples
+apples, apricots
Removed line apples and added line apples, apricots.
bananas
cherries
dragon fruits
Unchanged lines, shown for context.
-elderberries Removed line: elderberries.
figs Unchanged line, shown for context.
@@ -20,2 +20,3 @@ Hunk header showing that lines 20-21 in the old file were compared with lines 20–22 in the new file.
  oranges
+pears
  raisins
Unchanged line.
Added line: pears.
Unchanged line.
diff --git a/colours.txt b/colours.txt The usual diff header, indicates that Git is comparing two versions of the file colours.txt: one before and one after the change.
new file mode 100644 This is a new file being added. 100644 means it’s a normal, non-executable file with standard read/write permissions.
index 0000000..55c8449 The usual SHA hashes for the two versions of the file. 0000000 indicates the file did not exist before.
--- /dev/null
+++ b/colours.txt
Refers to the "old" version of the file (/dev/null means it didn’t exist before), and the new version.
@@ -0,0 +1 @@ Hunk header, saying: “0 lines in the old file were replaced with 1 line in the new file, starting at line 1.”
+a file for colours Added line

Points to note:

  • + indicates a line being added.
    - indicates a line being deleted.
  • Editing a line is seen as deleting the original line and adding the new line.
HANDS-ON: View specific commits

View contents of specific commits in a repo (e.g., the things repo):

1 Locate the commits to view, using the revision graph.

git log --oneline --decorate
 e60deae (HEAD -> master, origin/master) Update fruits list
 f761ea6 Add colours.txt, shapes.txt
 2bedace Add figs to fruits.txt
 d5f91de Add fruits.txt

2 Use the git show command to view specific commits.

git show  # shows the latest commit
commit e60deaeb2964bf2ebc907b7416efc890c9d4914b (HEAD -> master, origin/master)
Author: damithc <...@...>
Date:   Sat Jun ...

    Update fruits list

diff --git a/fruits.txt b/fruits.txt
index 7d0a594..6d502c3 100644
--- a/fruits.txt
+++ b/fruits.txt
@@ -1,6 +1,6 @@
-apples
+apples, apricots
 bananas
+blueberries
 cherries
 dragon fruits
-elderberries
 figs

To view the parent commit of the latest commit, you can use any of these commands:

git show HEAD~1
git show master~1
git show e60deae  # first few characters of the SHA
git show e60deae.....  # run git log to find the full SHA and specify the full SHA

to view the one two commits prior to the latest commit, you can use git show HEAD~2 etc.


Click on the commit. The remaining panels (indicated in the image below) will be populated with the details of the commit.



Lesson: Tagging Commits


When working with many commits, it helps to tag specific commits with custom names so they’re easier to refer to later.

This lesson covers that part.

Git lets you tag commits with names, making them easy to reference later. This is useful when you want to mark specific commits -- such as releases or key milestones (e.g., v1.0 or v2.1). Using tags to refer to commits is much more convenient than using SHA hashes. In the diagram below, v1.0 and interim are tags.

C3 masterHEAD interimUpdate list
|
C2 v1.0Populate list
|
C1Add empty list

A tag stays fixed to the commit. Unlike branch refs or HEAD, tags do not move automatically as new commits are made. As you see below, after adding a new commit, tags stay in the previous commits while masterHEAD have moved to the new commit.

C4 masterHEADTrim the list
|
C3 interimUpdate list
|
C2 v1.0Populate list
|
C1Add empty list

Git supports two kinds of tags:

  1. A lightweight tag is just a ref that points directly to a commit, like a branch that doesn’t move.
  2. An annotated tag is a full Git object that stores a reference to a commit along with metadata such as the tagger’s name, date, and a message.

Annotated tags are generally preferred for versioning and public releases, while lightweight tags are often used for less formal purposes, such as marking a commit for your own reference.

HANDS-ON: Adding tags

0 Preparation: fork and clone the samplerepo-preferences. Use the cloned repo in your computer for the following steps.

1 Add a lightweight tag to the current commit as v1.0:

git tag v1.0

2 Verify the tag was added. To view tags:

git tag
v1.0

To view tags in the context of the revision graph:

git log --oneline --decorate
507bb74 (HEAD -> master, tag: v1.0, origin/master, origin/HEAD) Add donuts
de97f08 Add cake
5e6733a Add bananas
3398df7 Add food.txt

3 Use the tag to refer to the commit e.g., git show v1.0 should show the changes in the tagged commit.

4 Add an annotated tag to an earlier commit. The example below adds a tag v0.9 to the commit HEAD~2 with the message First beta release. The -a switch tells Git this is an annotated tag.

git tag -a v0.9  HEAD~2 -m "First beta release"

5 Check the new annotated tag. While both types of tags appear similarly in the revision graph, the show command on an annotated tag will show the details of the tag and the details of the commit it points to.

git show v0.9
tag v0.9
Tagger: ... <...@...>
Date:   Sun Jun ...

First beta release

commit ....999087124af... (tag: v0.9)
Author: ... <...@...>
Date:   Sat Jun ...

    Add figs to fruits.txt

diff --git a/fruits.txt b/fruits.txt
index a8a0a01..7d0a594 100644
# rest of the diff goes here

Right-click on the commit (in the graphical revision graph) you want to tag and choose Tag….

Specify the tag name e.g. v1.0 and click Add Tag.

Configure tag properties in the next dialog and press Add. For example, you can choose whether to make it a lightweight tag or an annotated tag (default).

Tags will appear as labels in the revision graph, as seen below. To see the details of an annotated tag, you need to use the menu indicated in the screenshot.


If you need to change what a tag points to, you must delete the old one and create a new tag with the same name. This is because tags are designed to be fixed references to a specific commit, and there is no built-in mechanism to 'move' a tag.

HANDS-ON: Deleting/moving tags

Move the v1.0 tag to the commit HEAD~1, by deleting it first and creating it again at the destination commit.

Delete the previous v1.0 tag by using the -d switch. Add it again to the other commit, as before.

git tag -d v1.0
git tag v1.0 HEAD~1

The same dialog used to add a tag can be used to delete and even move a tag. Note that the 'moving' here translates to deleting and re-adding behind the scene.


Tags are different from commit messages, in purpose and in form. A commit message is a description of the commit that is part of the commit itself. A tags is a short name for a commit, which you can use to address a commit.

Pushing commits to a remote does not push tags automatically. You need to push tags specifically.

HANDS-ON: Pushing tags to a remote

Push tags you created earlier to the remote.

You can go to your remote on GitHub link https://github.com/{USER}/{REPO}/tags (e.g., https://github.com/johndoe/samplerepo-prefrences/tags) to verify the tag is present there.

Note how GitHub assumes these tags are meant as releases, and automatically provides zip and tar.gz archives of the repo (as at that tag).

1 Push a specific tag in the local repo to the remote (e.g., v1.0) using the git push <origin> <tag-name> command.

git push origin v1.0

In addition to verifying the tag's presence via GitHub, you can also use the following command to list the tags presently in the remote.

git ls-remote --tags origin

2 Delete a tag in the remote, using the git push --delete <remote> <tag-name> command.

git push --delete origin v1.0

3 Push all tags to the remote repo, using the git push <remote> --tags command.

git push origin --tags

To push a specific tag, use the following menu:

To push all tags, you can tick the Push all tags option when pushing commits:



Lesson: Comparing Points of History


Git can tell you the net effect of changes between two points of history.

This lesson covers that part.

Git's diff feature can show you what changed between two points in the revision history. Given below are some use cases.

Usage 1: Examining changes in the working directory.
Example use case: To verify the next commit will include exactly what you intend it to include.

HANDS-ON: Examining staged and unstaged changes

0 Preparation: For this, you can use the things repo you created earlier. If you don't have it, you can clone a copy of a similar repo given here.

1 Do some changes to the working directory. Stage some (but not all) changes. For example, you can run the following commands.

echo "blue\nred\ngreen" >> clours.txt
git add .  # a shortcut to stage all changes
echo "no shapes added yet" >> shapes.txt

2 Examine the staged and unstaged changes.

The git diff command shows unstaged changes in the working directory (tracked files only). The output of the diff command, is a diff view (introduced in this lesson).

git diff
diff --git a/shapes.txt b/shapes.txt
index 5c2644b..949c676 100644
--- a/shapes.txt
+++ b/shapes.txt
@@ -1 +1,2 @@
a file for shapes
+no shapes added yet!

The git diff --staged (same as git diff --cached) command shows the staged changes.

git diff --staged

Select the two commits: Click on one commit, and Ctrl-Click (or Cmd-Click) on the second commit. The changes between the two selected commits will appear in the other panels, as shown below:


Usage 2: Comparing two commits at different points of the revision graph.
Example use case: Suppose you’re trying to improve the performance of a piece of software by experimenting with different code tweaks. You commit after each change (as you should). After several commits, you now want to review the overall effect of all those changes on the code.

HANDS-ON: Comparing two commits

Compare two commits in a repo (e.g., the things repo).

You can use the git diff <commit1> <commit2> command for this.

  • You may use any valid way to refer to commits (e.g., SHA, tag, HEAD~n etc.).
  • You may also use the .. notation to specify the commit range too e.g., 0023cdd..fcd6199, HEAD~2..HEAD
git diff v0.9 HEAD
diff --git a/colours.txt b/colours.txt
new file mode 100644
index 0000000..55c8449
--- /dev/null
+++ b/colours.txt
@@ -0,0 +1 @@
+a file for colours
# rest of the diff ...

Swap the commit order in the command and see what happens.

git diff HEAD v0.9
diff --git a/colours.txt b/colours.txt
deleted file mode 100644
index 55c8449..0000000
--- a/colours.txt
+++ /dev/null
@@ -1 +0,0 @@
-a file for colours
# rest of the diff ...

As you can see, the diff is directional i.e., dif <commit1> <commit2> shows what changes you need to do to go from the <commit1> to <commit2>. If you swap <commit1> and <commit2>, the output will change accordingly e.g., lines previously shown as 'added' will now be shown as 'deleted'.


Select the two commits: Click on one commit, and Ctrl-Click (or Cmd-Click) on the second commit. The changes between the two selected commits will appear in the other panels, as shown below:

The same method can be used to compare the current state of the working directory (which might have uncommitted changes) to a point in the history.


Usage 3: Examining changes to a specific file.
Example use case: Similar to other use cases but when you are interested in a specific file only.

HANDS-ON: Examining changes to a specific file

Examine the changes done to a file between two different points in the version history (including the working directory).

Add the -- path/to/file to a previous diff command to narrow the output to a specific file. Some examples:

git diff -- fruits.txt               # unstaged changes to fruits.txt
git diff --staged -- src/main.java   # staged changes to src/main.java
git diff HEAD~2..HEAD -- fruits.txt  # changes to fruits.txt between commits

Sourcetree UI shows changes to one file at a time by default; just click on the file to view changes to that file. To view changes to multiple files, Ctrl-Click (or Cmd-Click) on multiple files to select them.



Lesson: Traversing to a Specific Commit


Another useful feature of revision control is to be able to view the working directory as it was at a specific point in history, by checking out a commit created at that point.

This lesson covers that part.

Suppose you added a new feature to a software product, and while testing it, you noticed that another feature added two commits ago doesn’t handle a certain edge case correctly. Now you’re wondering: did the new feature break the old one, or was it already broken? Can you go back to the moment you committed the old feature and test it in isolation, and come back to the present after you found the answer? With Git, you can.

To view the working directory at a specific point in history, you can check out a commit created at that point.

When you check out a commit, Git:

  1. Updates your working directory to match the snapshot in that commit, overwriting current files as needed.
  2. Moves the HEAD ref to that commit, marking it as the current state you’re viewing.
C3 masterHEAD
|
C2
|
C1


[check out commit C2...]

C3 master
|
C2HEAD detached head!
|
C1

Checking out a specific commit puts you in a "detached HEAD" state: i.e., the HEAD no longer points to a branch, but directly to a commit (see the above diagram for an example). This isn't a problem by itself, but any commits you make in this state can be lost, unless certain follow-up actions are taken. It is perfectly fine to be in a detached state if you are only examining the state of the working directory at that commit.

To get out of a "detached HEAD" state, you can simply check out a branch, which "re-attaches" HEAD to the branch you checked out.

HANDS-ON: Checking out some commits

Checkout a few commits in a local repo (e.g., the things repo), which examining the working directory to verify that it matches the state when you created the corresponding commit:

1 Examine the revision tree, to get your bearing first.

git log --oneline --decorate
e60deae (HEAD -> 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

2 Use the checkout <commit-identifier> command to check out a commit other than the one currently pointed by HEAD. You can use any of the following methods:

  • git checkout v1.0: checks out the commit tagged v1.0
  • git checkout 0023cdd: checks out the commit with the hash 0023cdd
  • git checkout HEAD~2: checks out the commit 2 commits behind the most recent commit.
git checkout HEAD~2
Note: switching to 'HEAD~2'.

You are in 'detached HEAD' state.
# rest of the warning about the detached head ...

HEAD is now at 2bedace Add figs to fruits.txt

3 Verify HEAD and the working directory have updated as expected.

  • HEAD should now be pointing at the target commit
  • The working directory should match the state it was in at that commit (e.g., files added after that commit -- such as shapes.txt should not be in the folder).
git log --one-line --decorate
2bedace (HEAD, tag: v0.9) Add figs to fruits.txt
d5f91de Add fruits.txt

HEAD is indeed pointing at the target commit.

But note how the output does not show commits you added after the checked-out commit.

The --all switch tells git log to show commits from all refs, not just those reachable from the current HEAD. This includes commits from other branches, tags, and remotes.

git log --one-line --decorate --all
e60deae (origin/master, master) Update fruits list
f761ea6 (tag: v1.0) Add colours.txt, shapes.txt
2bedace (HEAD, tag: v0.9) Add figs to fruits.txt
d5f91de Add fruits.txt

4 Go back to the latest commit by checking out the master branch again.

git checkout master

In the revision graph, double-click the commit you want to check out, or right-click on that commit and choose Checkout....

Click OK to the warning about ‘detached HEAD’ (similar to below).

The specified commit is now loaded onto the working folder, as indicated by the HEAD label.

To go back to the latest commit on the master branch, double-click the master branch.


If you check out a commit that comes before the commit in which you added a certain file (e.g., temp.txt) to the .gitignore file, and if the .gitignore file is version controlled as well, Git will now show it under ‘unstaged modifications’ because at Git hasn’t been told to ignore that file yet.

If there are uncommitted changes in the working directory, Git proceeds with a checkout only if it can preserve those changes.

  • Example 1: There is a new file in the working directory that is not committed yet.
    → Git will proceed with the checkout and will keep the uncommitted file as well.
  • Example 2: There is an uncommitted change to a file that conflicts with the version of that file in the commit you wish to check out.
    → Git will abort the checkout, and the repo will remain in the current commit.

DETOUR: Dealing with Uncommitted Confilicting Changes at a Checkout

To proceed with a checkout when there are conflicting uncommitted changes in the working directory, there are several options:

  • Commit the changes.
  • Stash the changes.
  • Discard the changes.


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

Reset is different from the checkout feature:

  • Reset: Lets you start over from a past state. Rewrites history by moving the branch ref.
  • Checkout: Lets you explore a past state without rewriting history. Moves the HEAD ref.
C3 masterHEAD (original tip of the branch)
|
C2
|
C1


[reset to C2...]

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

There are three types of resets: soft, mixed, hard. All three moves 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 in to 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

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

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


3 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 --mixed reset <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.


4 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 --hard reset <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.


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.




DETOUR: Undoing/Deleting Recent Commits

How 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 mixe 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 works below the surface to make everything work).



Lesson: Reverting a Specific Commit


Git can add a new commit to reverse the changes done in a specific past commit, called reverting a commit.

This lesson covers that part.

When a past commit introduced a bug or an unwanted change, but you do not want to modify that commit — because rewriting history can cause problems if others have already based work on it — you can instead revert that commit.

Reverting creates a new commit that cancels out the changes of the earlier one i.e., Git computes the opposite of the changes introduced by that commit — essentially a reverse diff — and applies it as a new commit on top of the current branch. This way, the problematic changes are reversed while preserving the full history, including the "bad" commit and the "fix".

C3
|
C2
|
C1


[revert C2]

R2This commit is the reverse of C2
|
C3
|
C2
|
C1

HANDS-ON: Revert a commit

0 Preparation: Run the following commands to create a repo with a few commits:

mkdir pioneers
cd pioneers
git init

echo "hacked the matrix" >> neo.txt
git add .
git commit -m "Add Neo"

echo "father of theoretical computing" >> alan-turing.txt
git add .
git commit -m "Add Turing"

echo "created COBOL, compiler pioneer" >> grace-hopper.txt
git add .
git commit -m "Add Hopper"
C3HEADAdd Hopper
|
C2Add Turing
|
C1Add Neo

1 Revert the commit Add Neo.

You can use the git revert <commit> command to revert a commit. In this case, we want to revert the commit that is two commits behind the HEAD.

git revert HEAD~2

What happens next:

  1. Git prepares a new commit which reverses the target commit
  2. Git opens your default text editor containing a proposed commit message. You can edit it, or accept the proposed text.
  3. Once you close the editor, Git will create the new commit.

In the revision graph, right-click on the commit you want to revert, and choose Reverse commit...


A revert can result in a conflict, if the new changes done to reverse the previous commit conflict with the changes done in other more recent commits. Then, you need to resolve the conflict before the revert operation can proceed. Conflict resolution is covered in a later topic.


At this point: You should now be able to use a repository’s revision history to understand how the working directory evolved over time -- and use that insight to inform your work.
How useful this history is depends greatly on how well it was constructed -- for example, how frequently and meaningfully commits were made -- we’ll explore that in a later tour.

What's next: Tour 5: Fine-Tuning the Revision History