Git Reset Modes
Tags: git, softwaredevelopment, softwareengineering, cicd
Published: 2023-12-06 03:08:05

---
title: Git Reset Modes
published: true
date: 2023-12-06 02:58:56 UTC
tags: git, softwaredevelopment, softwareengineering, cicd
canonical_url: https://blog.nicholasfragiskatos.dev/git-reset-modes
---

Sometimes when working with Git it would be useful to rewind your branch to an earlier commit. Maybe one of your recent commits introduced a bug or you just want to organize your changes differently between commits. Whatever the reason, Git provides this functionality through the `git reset` command.

In this article I will explain and demonstrate how to use the three most common modes of the `git reset` command, _soft_, _mixed_, and _hard_.

## Important Background

Before diving into what `git reset` is and how it works, we need to have a basic understanding of these four important pieces of Git: _working directory_, _repository_, _index_, and _HEAD_.

### Working Directory

The easiest piece to understand is the _working directory_, a.k.a. _workspace_. This is simply the main directory where all the source files live that you see and edit for your project.

### Repository

Typically, when we are working on a project that Git is managing, the working directory houses another directory named `.git`. This directory is the _repository_ and contains all the commit history, branch information, files, etc. for Git to manage the project. The `.git` directory is also all that's needed to rebuild (i.e. clone) the repository.

From Git's perspective the `.git` directory is the repository. However, the term _repository_ is often used more generally to mean the working directory and everything inside it. The important point is that when Git references the _repository_ it means the `.git` directory.

### HEAD

The _HEAD_ is a pointer to the latest commit in the commit history on the current branch reference. Each branch has only one _HEAD_ that is unique to that specific branch. While often used interchangeably for convenience, branch and _HEAD_ aren't the same, because a branch represents a collection of commits, while the _HEAD_ is specifically the latest commit on the branch.

The repository stores all head references in `.git/refs/heads/`. Also, there is a _HEAD_ file in the root of `.git/` that points to a specific head reference in `.git/refs/heads`.

### Index

The _index_ is the storage space for all changes that will be included in the next commit and is stored within the repository. The _index_ might also be referred to as the _staging area_, or _cache_. When we use the `git add` command, the file is then placed in the _index_.

### Putting the Pieces Together

For visual clarity, below is a simple diagram of how these pieces are related. The working directory encompasses everything, then the repository(`.git` directory) contains the index and the HEAD.

![Diagram of how the working directory, index, reposiotry, and head are related to each other.](https://cdn.hashnode.com/res/hashnode/image/upload/v1701829488709/35190bee-1eb6-406e-8641-c001f41cfda3.png)

### The File Lifecycle

The example below demonstrates the three main stages for a file when working with Git. It starts in the working directory where It's either a new file or an existing file that has been modified. <sup>1</sup> Next, when the file is done being modified, it's ready to be added to the index using the `git add` command.<sup>2</sup> Lastly, it is ready to be committed with `git commit`. Once committed it becomes part of the latest commit, and this latest commit becomes the new HEAD.<sup>3</sup>

![Example of the file lifecycle as it transitions from the working directory to the index and finally to the commit history](https://cdn.hashnode.com/res/hashnode/image/upload/v1701829562740/70fb1542-983d-4152-b457-18169054df03.png)

It's important to know that none of the files are actually getting moved around. Instead, copies of files are being made (it's more complicated than a basic copy and paste, but for modeling purposes copy is accurate enough). All the files in the working directory stay in the working directory. When the file is staged, a copy of it is placed in the index. Then, when the file is committed, another copy is created and added to the repository as a part of the commit history.

With that being said, it is entirely possible and not uncommon for three different versions of a file to exist at a time between the working directory, index, and HEAD. Take for example, the figure below. Initially, the file is created/edited, staged, and then committed. Meaning all three areas have the same file.<sup>1</sup> If the file is edited and staged again, but not committed, then the working directory and the index will have a different version of the file than the HEAD.<sup>2</sup> Finally, if the file is edited again but _not_ staged, then the working directory now contains a third version of the same file.

![Example of the same file existing in the working directory, index, and HEAD at the same time](https://cdn.hashnode.com/res/hashnode/image/upload/v1701829618985/47ca74c4-c0f1-4109-b74a-655ab6d5404c.png)

## The Reset Command

A reset command has the following template, where MODE can be _soft_, _mixed_, or _hard_, and the COMMIT\_HASH is the hash ID for a commit that can be found when viewing the log.

```bash
git reset --MODE COMMIT_HASH
```

E.x.

```bash
git reset --soft 78dffe9c5fa346d03458d15862a38325ebb2e896
```

At a minimum, a reset is just moving the head back to a previous commit. However, an important thing to consider is what happens to the commits between the new HEAD position and the old HEAD position. Each of those commits contain changes, and what happens to those changes is determined by the different reset modes.

This is most easily seen through an example. Consider the following branch, _my\_example\_branch_, from the figure below.<sup>1</sup> If for some reason we don't like commits _D_ and _E_ and we want to undo them, we can do a `git reset C` to move the head back to commit _C_.<sup>2</sup> This makes commits _D_ and _E_ orphaned and no longer part of the branch's commit history.

![Example of how a branch changes after a git reset](https://cdn.hashnode.com/res/hashnode/image/upload/v1701829762103/b5951cfb-273d-4d70-ba1f-9df0aaedf1c9.png)

## Soft Reset

For a soft reset, all current changes in the index remain in the index, all changes in the orphaned commits are placed back in the index, and the working directory is unchanged.

The example below demonstrates this. We start with a series of five commits: A, B, C, D, and E, all with various changes.<sup>1</sup> The notation F1<sub>A</sub> specifies version _A_ of a file named _F1_. It's important to note that in this state we also have changes in the working directory with F3<sub>C</sub> that haven't been staged, and changes in the index with F2<sub>E</sub> that haven't been committed.

Performing a `git reset --soft C`<sup>2</sup> moves the HEAD back to commit C. This causes commits D and E to be orphaned. Commit D has changes F1<sub>C</sub> and F3<sub>B</sub> placed back into the index. Notice that F3<sub>C</sub> and F3<sub>B</sub> now exist simultaneously in the working directory and index, respectively. Commit E has change F4<sub>A</sub> placed back into the index. However, change F2<sub>D</sub> is a little different. Since F2<sub>E</sub> already exists in the index, and is more recent than F2<sub>D</sub>, the more recent change is kept.

![Example of how a soft reset affects the HEAD, index, and working directory](https://cdn.hashnode.com/res/hashnode/image/upload/v1701829832388/07e12ebd-c2f7-4033-9735-22c988d3ef92.png)

### Example - Setup Commands

```bash
mkdir soft_reset       
cd soft_reset
git init
touch F1
echo "F1A" >> F1
touch F2
echo "F2A" >> F2
git add .
git commit -m "Commit A"
touch F3
echo "F3A" >> F3
echo "F2B" >> F2 
git add .
git commit -m "Commit B"
echo "F1B" >> F1
echo "F2C" >> F2
git add .
git commit -m "Commit C"
echo "F1C" >> F1
echo "F3B" >> F3
git add .
git commit -m "Commit D"
echo "F2D" >> F2
touch F4
echo "F4A" >> F4
git add .
git commit -m "Commit E"
echo "F2E" >> F2
git add F2
echo "F3C" >> F3
```

### Example - Setup State

```bash
$ git log --name-only

commit b8ed5b6fb95681eb02a1de265a374ef300ad27b9 (HEAD -> main)
Author: ...
Date: ...

    Commit E

F2
F4

commit e961b6e8eee949fc6f83b89861cafec036686212
Author: ...
Date: ...

    Commit D

F1
F3

commit 78dffe9c5fa346d03458d15862a38325ebb2e896
Author: ...
Date: ...

    Commit C

F1
F2

commit b132b2646c3f6203d27319d6296a1292710f2782
Author: ...
Date: ...

    Commit B

F2
F3

commit 75f789b08f5f7dc11557614fddcb941270829fdc
Author: ...
Date: ...

    Commit A

F1
F2
```
```
$ git status

On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified: F2

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: F3
```

### Example - Soft Reset to Commit C

After running `git reset --soft 78dffe9c5fa346d03458d15862a38325ebb2e896`:

```bash
$ git status

On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified: F1
    modified: F2
    modified: F3
    new file: F4

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: F3
```

To show that we have the correct F2<sub>E</sub> change we can run `git diff --cached F2` to get the difference between the index and HEAD versions. It shows the inclusion of the 2 lines from both changes, F2<sub>D</sub> and F2<sub>E</sub>.

```bash
$ git diff --cached F2

diff --git a/F2 b/F2
index a2e06c8..3a24ea5 100644
--- a/F2
+++ b/F2
@@ -1,3 +1,5 @@
 F2A
 F2B
 F2C
+F2D
+F2E
```

## Mixed Reset

For a mixed reset, all changes in the orphaned commits are placed back in the working directory and all currently staged changes are unstaged, i.e., moved back into the working directory. This means the index now matches the HEAD.

> 💡
> The mixed mode is the default mode when performing a reset, and in this case the_mode_flag can be omitted. The following two commands are equivalent:

```bash
git reset --mixed COMMIT_HASH`


git reset COMMIT_HASH
```

To demonstrate a mixed reset we can use the same setup as before. From the example below, we again start with a series of five commits: A, B, C, D, and E.<sup>1</sup> Also, as before, in this state we have changes in the working directory with F3<sub>C</sub> that haven't been staged, and changes in the index with F2<sub>E</sub> that haven't been committed.

Performing a `git reset --mixed C`<sup>2</sup> moves the HEAD back to commit C. This causes commits D and E to be orphaned. All changes from the orphaned commits _and_ any staged changes are placed back into the working directory. Similarly to the previous example, there are overlaps between F2<sub>D</sub>/F2<sub>E</sub>, and F3<sub>B</sub>/F3<sub>C</sub> resulting in the most recent changes being kept, F2<sub>E</sub> and F3<sub>C</sub>. One last thing to note is that F4<sub>A</sub> is now considered an _untracked_ file because from the commit history's perspective it's a new file that has never been staged.

![Example of how a mixed reset affects the HEAD, index, and working directory](https://cdn.hashnode.com/res/hashnode/image/upload/v1701830024428/9984a89a-cb7b-4cd9-add9-4da315c254ad.png)

### Example - Setup Commands

```bash
mkdir mixed_reset       
cd mixed_reset
git init
touch F1
echo "F1A" >> F1
touch F2
echo "F2A" >> F2
git add .
git commit -m "Commit A"
touch F3
echo "F3A" >> F3
echo "F2B" >> F2 
git add .
git commit -m "Commit B"
echo "F1B" >> F1
echo "F2C" >> F2
git add .
git commit -m "Commit C"
echo "F1C" >> F1
echo "F3B" >> F3
git add .
git commit -m "Commit D"
echo "F2D" >> F2
touch F4
echo "F4A" >> F4
git add .
git commit -m "Commit E"
echo "F2E" >> F2
git add F2
echo "F3C" >> F3
```

### Example - Setup State

```bash
$ git log --name-only

commit fc52174c53e4f3af9f7c85be2f5e15edd51a4e3f (HEAD -> main)
Author: ...
Date: ...

    Commit E

F2
F4

commit ef5162ee0dc249bc3df73f6330b9bdab20462623
Author: ...
Date: ...

    Commit D

F1
F3

commit efada051fe9b3ac195184008c2dac4a14b501402
Author: ...
Date: ...

    Commit C

F1
F2

commit 4dbd5152f6ee12fc6f8124667513789f3d9466ed
Author: ...
Date: ...

    Commit B

F2
F3

commit 9e3f1ac4b98ab0791183dfa0f06d9a7620706b21
Author: ...
Date: ...

    Commit A

F1
F2
```

```bash
$ git status

On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified: F2

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: F3
```

### Example - Mixed Reset to Commit C

After running `git reset --mixed efada051fe9b3ac195184008c2dac4a14b501402`:

```bash
$ git status

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: F1
    modified: F2
    modified: F3

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    F4

no changes added to commit (use "git add" and/or "git commit -a")
```

To show that we have the correct F2<sub>E</sub> and F3<sub>C</sub> file versions we can just view their contents:

```bash
$ cat F2

F2A
F2B
F2C
F2D
F2E

$ cat F3

F3A
F3B
F3C
```

## Hard Reset

A hard reset is the simplest but should be used carefully because _it is also_ **_destructive_**. All changes from the index, working tree, and orphaned commits are deleted.

> 💡
> While any uncommitted changes in the working directory or index are lost after a hard reset, it is possible to recover changes from orphaned commits using the[reflog](https://stackoverflow.com/questions/34751837/git-can-we-recover-deleted-commits). However, that's outside the scope of this article.

From the example below, we again start with a series of five commits: A, B, C, D, and E.<sup>1</sup> Also, as before, in this state we have changes in the working directory with F3<sub>C</sub> that haven't been staged, and changes in the index with F2<sub>E</sub> that haven't been committed.

Performing a `git reset --hard C`<sup>2</sup> moves the HEAD back to commit C. Changes in the index and working directory are discarded, as well as any changes from the orphaned commits, D and E. This leaves the working directory, index, and head all in sync.

![Example of how a hard reset affects the HEAD, index, and working directory](https://cdn.hashnode.com/res/hashnode/image/upload/v1701830233585/4e26278f-d9ef-4208-944e-024fc25b99dd.png)

### Example - Setup Commands

```bash
mkdir hard_reset       
cd hard_reset
git init
touch F1
echo "F1A" >> F1
touch F2
echo "F2A" >> F2
git add .
git commit -m "Commit A"
touch F3
echo "F3A" >> F3
echo "F2B" >> F2 
git add .
git commit -m "Commit B"
echo "F1B" >> F1
echo "F2C" >> F2
git add .
git commit -m "Commit C"
echo "F1C" >> F1
echo "F3B" >> F3
git add .
git commit -m "Commit D"
echo "F2D" >> F2
touch F4
echo "F4A" >> F4
git add .
git commit -m "Commit E"
echo "F2E" >> F2
git add F2
echo "F3C" >> F3
```

### Example - Setup State

```bash
$ git log --name-only

commit b379b56b724c2ad9074d84ea2bae38c3394769a7 (HEAD -> main)
Author: ...
Date: ...

    Commit E

F2
F4

commit 82e8bfef9df0e6d53ff6cb21ddf17d4196de340f
Author: ...
Date: ...

    Commit D

F1
F3

commit 967ccf7ce4ec657dcbc9d9985349eda675fb0379
Author: ...
Date: ...

    Commit C

F1
F2

commit f3c30c1d6fc908df990673899264022461914ac6
Author: ...
Date: ...

    Commit B

F2
F3

commit 784afdf9e7d3b7189f457bd5c8cfb6df538eaf15
Author: ...
Date: ...

    Commit A

F1
F2
```

```bash
$ git status

On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified: F2

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: F3
```

### Example - Hard Reset to Commit C

After running `git reset --hard 967ccf7ce4ec657dcbc9d9985349eda675fb0379`:

```bash
$ git status

On branch main
nothing to commit, working tree clean
```

## Conclusion

The `git reset` command is a useful feature of Git that allows us to rewind a branch by moving the HEAD to an earlier commit. When rewinding a branch, we can control what happens to changes in the working directory, index, and any orphaned commits by using the three different reset modes: _soft_, _mixed_, and _hard_.

A _soft_ reset will preserve any changes in the index at the time of reset while also placing any changes from orphaned commits back into the index. The working directory is left completely unchanged.

A _mixed_ reset preserves all current changes in the working directory. Also, all changes from the index and the orphaned commits are moved back into the working directory. The index is left empty and in sync with the HEAD.

Lastly, a _hard_ reset is the simplest because all changes from the working directory, index, and orphaned commits are just deleted.

* * *

Thank you for taking the time to read my article. I hope it was helpful.

If you noticed anything in the article that is incorrect or isn't clear, please let me know. I always appreciate the feedback.

* * *