Icon of sun for "Light Mode" theme

The Three Trees of Git (Part 2)

Interacting With Git’s Working Directory, Index and HEAD Mechanisms to Undo

Image of Three Bars by Franck V. by UnsplashImage of Three Bars by Franck V. by Unsplash

Photo by Franck V. on Unsplash

In my previous blog post I gave an introduction to Git’s Working Directory, Index and HEAD mechanisms and discussed some of the different ways you can interact with them to track, stage, commit and inspect files in a project. In this post I’m going to continue discussing how you can interact with these mechanisms, but in the context of actions that can untrack, unstage, and undo changes to files.

checkout

In the prior post I briefly discussed how the git checkout command can allow you and the HEAD mechanism to travel around your project and inspect whole branches and individual commits, but checking out can also be used to undo changes to specific files:

Checkout the <filename> in the state it was in the last commit and permanently undo any changes that have been made to the <filename> since the last commit.

git checkout -- <filename>

The two hyphens in between checkout and the <filename> are used to indicate that the name represented by <filename> is an actual file as opposed to a branch or an available command option that can be used with `git checkout`. If <filename> doesn’t match a branch name, or match an available option, or match a filename that starts with a hyphen, then you can omit the hyphens.

Checkout <filename> in the state it was in <n> commits prior.

git checkout HEAD~<n> <filename>

If you perform this action and then stage and commit the file it will permanently undo any changes you made to <filename> after the corresponding commit you checked out.

This behaves the same as the command above but uses the <commit> instead of the HEAD to target and checkout <filename> in the state it was in at <commit>.

git checkout <commit> <filename>

If you perform this action and then stage and commit the file it will permanently undo any changes you made to <filename> after the corresponding commit you checked out.

rm (remove)

If you’ve used git add to start tracking a file but you later decide that you don’t need that file then you can use the git rm command to remove the file from the Index or from both the Index and the Working Directory. git rm is often thought of as the opposite of git add, and while this can be a helpful way to remember what git rm does, it’s not always accurate. In fact, when you run git rm Git actually uses git add under the hood to stage the removed file from the Index so that it won’t be included in your next commit.

Permanently remove an unmodified file from the Git repository and delete it from your computer.

git rm <filename>

By “unmodified” I mean a file that has previously been tracked and committed but hasn’t been changed since the last commit or since being added to the Index. To remove “modified” files see examples below that use the `--force` option.

Permanently remove a modified file your repository and delete it from your computer.

git rm -f <filename>(-f is a shortcut for --force)

As a safety precaution Git ordinarily doesn’t allow you to remove a modified file (a file that has been tracked and committed but has unsaved changes) but the `--force` option is what allows you to override this.

Unstage an unmodified file from the Index and stop tracking that file but keep the file in the Working Directory as an untracked file.

git rm --cached <filename>

`--cached` is what allows you to keep a file in the Working Directory after you remove it from the Index and stop tracking it.

Same as above but target a folder that contains sub-directories and files.

git rm -r --cached <folder_name>

This one is particularly helpful if you ever forgot to add your `node_modules` folder to your `.gitignore` file. For example, if you forget and accidentally push your `node_modules` to your remote repository you can add `node_modules` to your `.gitignore` file, run `git rm -r --cached node_modules` and then after you commit and push, `node_modules` will be removed from your remote repo and Git will no longer track it.

Unstage a modified file from the Index, stop tracking that file but keep any file modifications and keep the file in the Working Directory as an untracked file.

git rm --cached -f <filename>

clean

If you’ve introduced a new file to your project but haven’t started tracking it yet and you decide that you’ll no longer need it, you can use the git clean command to remove it from the Working Directory:

Remove the untracked <filename> from the Working Tree and permanently delete it from your computer.

git clean -f <filename> (-f is a shortcut for --force)

Same as above but it operates on all untracked files in the current directory.

git clean -f

If you want to first check to see what file would be removed you can use `git clean -n` (`-n` is a shortcut for `--dry-run`)

Same as the commands above but the addition of the `-d` option will also recursively remove all untracked sub-directories and/or any untracked files within those directories.

git clean -fd

Similar to the command above, you can use `git clean -nd` to see which files and sub-directories would be removed. If a sub-directory you attempt to clean has both tracked and untracked files then only the untracked files will be removed and the sub-directory will be kept in place.

reset

One of the more flexible and multi-functional commands in Git is the git reset command. Depending on the options that you use with git reset it will operate differently in regard to how it affects the Working Directory, Index, and HEAD mechanisms, but at it’s most basic level git reset will reset/move the HEAD to the specified target state (by “specified target state” I’m referring to a commit or file). This is similar to what git checkout does except reset typically also moves the associated branch reference, which prevents the HEAD from being “detached” like it is if you use git checkout <commit>. Keep in mind that git reset is a potentially dangerous command that can delete your commit history so you should not reset code that has been pushed to a shared repository because other contributers may already have added work on top of any commits that are being undone and it can be difficult to fix these complications. It’s also important to note that if you reset back to a commit that happened multiple commits ago you will lose all commits that occurred after the target commit (there is a way to recover deleted commits by using the git reflog command but you can only recover them for a limited time because Git will regularly and permanently delete commits that have been orphaned and are no longer tied to active branches of development).

Unstage all modified files from the Index and move them back into the Working Directory with the modifications intact and move the HEAD and branch to the prior commit. The Index now matches the state of the prior commit.

git reset (shorthand for git reset HEAD --mixed)

For all reset commands where you don’t specify a specific commit, Git will assume you want to target the HEAD, which is typically going to be the most recent commit.

Unstage a specific file from the Index with the modifications intact. This is basically the opposite of using `git add` to stage a file to the Index.

git reset <filename>

Move the HEAD and branch to the specified commit but leave the Index and Working Directory intact.

git reset --soft <commit>

Similar to the `git commit --amend` command, this will allow you to edit and add changes to a commit, but instead of only working on the most recent commit it will allow you first undo any commits that come after the target commit.

Move the Index, Working Directory, HEAD and branch to the prior commit. This will delete any modifications you’ve made to tracked files since the last commit (any untracked files will still exist).

git reset --hard

Same as above but this will also delete any commits you made after the target commit.

git reset --hard <commit>

revert

The git revert command is often confused with the git reset and git checkout commands in that they all allow you to undo something, but the way revert goes about it is less destructive, and unlike git reset and git checkout, which can perform different actions on individual files as well as commits, revert only works on commits. git revert is less destructive because instead of deleting a commit or deleting uncommitted changes it will preserve the history that contains the changes you want to undo and then it will make a new commit with those changes undone. Because of its non-destructive nature you should use git revert when undoing any changes that have been pushed to a shared repository. Note that in order to perform a revert you must not have any modified files.

Create a new commit with the changes introduced by <commit> undone but keep <commit> and any commits that occurred after it in the history.

git revert <commit>

Git will automatically initialize a new commit but by default it will enter VIM to allow you to enter a custom message. If you don’t care about customizing the message you can add the `--no-edit` option. Also, if you don’t want Git to automatically initialize a commit you can add the `-n` option (shorctut for` --no-commit`) and Git will apply the necessary changes from the reverted commit but will just add them to the Working Directory and Index so you can include additional updates in the next commit.

Same as above but target the <n>th most recent commit (e.g. git revert HEAD~2 will target the 3rd most recent commit).

git revert HEAD~<n>

Amending Commits

The last command I want to discuss here is git commit --amend, which I also discussed in the prior article but I wanted to also discuss it here since it’s technically a destructive command that allows you to rewrite history and edit the most recent commit. Similarly to how you shouldn’t use the git reset command on commits that have been pushed to a shared repository, you also generally shouldn’t use git commit --amend on shared work because the intial commit will be deleted before it adds the amended commit. If you've pushed an amended commit to a personal repo that nobody else has added work to, then after you make the amended commit you can run git push --force to force git to disregard the original remote commit. Otherwise, you'll need to first run git pull to get your local repo back in sync with the remote repo before you can push the amended commit.

Edit the commit message for the prior commit. Also, if you’ve staged any new changes before running the `amend` command then those changes will also be included in the amended commit.

git commit --amend -m “<your revised commit message>”