1. Overview

Git has become a widely used, distributed version control system. In this tutorial, let’s explore how to remove a file or directory from a Git repository but keep its local copy.

2. Introduction to the Problem

As usual, let’s understand the problem via an example. Let’s say we’re working on a Git repository myRepo:

$ ls -l
total 12
drwxr-xr-x 2 kent kent 60 May 12 23:00 logs/
-rw-r--r-- 1 kent kent 26 May 11 13:22 README.md
-rw-r--r-- 1 kent kent 21 May 11 13:22 some-file.txt
-rw-r--r-- 1 kent kent 16 May 12 22:40 user-list.txt

We’ve cloned the repository to local, and as the ls output shows, we have three files and a logs directory in the repository.

Now, let’s say we would like to remove the file user-list.txt and the logs directory from the Git repository. However, we don’t want to remove them from our local working copy.

A common scenario would be we’ve committed some files or directories, then realized that we should ignore some files. Therefore, we’ll remove the related files from the repository, keep the local copies, and add the corresponding patterns to the .gitignore file so that Git won’t track those files anymore.

We know the git rm user-list.txt command will remove the file from the repository. But, it removes the local file as well.

Of course, we can move the file and directory to another directory, submit a commit, then copy them back to the local working directory. It solves the problem. However, this approach is inefficient, particularly when the file or directory is large.

Next, let’s see how to solve this problem more efficiently.

3. Using the git rm –cached Command

We’ve mentioned that git rm FILE will remove files from the index and local working tree by default.

However, the git rm command provides the –cached option to allow us only to remove files from the repository’s index and keep the local file untouched.

Next, let’s try it with the user-list.txt file:

$ git rm --cached user-list.txt
rm 'user-list.txt'

As the output above shows, the user-list.txt file has been removed. So now, let’s execute the git status command to verify it:

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    deleted:    user-list.txt

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

As we can see, the user-list.txt is “deleted“. Further, since its local copy is still there, it has been marked as “untracked”.

We can similarly remove the logs directory. However, since it’s a directory, we need to additionally pass the -r (recursively) option to the git rm command:

$ git rm --cached -r logs
rm 'logs/server.log'

Now, let’s commit our changes:

$ git commit -m 'remove user-list.txt and logs'
[master ee8cfe8] remove user-list.txt and logs
 2 files changed, 4 deletions(-)
 delete mode 100644 logs/server.log
 delete mode 100644 user-list.txt

Then, let’s check the currently staged files using the git ls-files command:

$ git ls-files -c
.gitignore
README.md
some-file.txt

As the output shows, the target file and directory are not there anymore. Also, the local copies are kept. Therefore, we’ve solved the problem.

If we like, we can add them to the .gitignore file to prevent Git from tracking them again.

4. Remove All Files Defined in .gitignore

Sometimes, we want to check Git’s index and remove all files defined in .gitignore. Assume that we’re done with our .gitignore definition. Then, a straightforward way would be a three-step process:

  • First, remove all files from the index: git rm -r –cached
  • Then, stage all files again. Files defined in .gitignore will be ignored automatically: git add
  • Commit our changes: git commit -m “a proper commit message”

Alternatively, we can find and remove only the files that are currently tracked but should be ignored. The git ls-files command can help us to find the files.

Let’s revert our previous commit and remove the user-list.txt file and logs directory again. This time, let’s first add them to the .gitignore file:

$ cat .gitignore
user-list.txt
logs/

Next, let’s find out the files we’d like to remove from the Git index:

$ git ls-files -i -c -X .gitignore
logs/server.log
user-list.txt

As we can see, the command above listed the staged files we would like to remove.

Now, let’s combine the git rm –cached and git ls-files commands to remove them in one shot:

$ git rm --cached $(git ls-files -i -c -X .gitignore)
rm 'logs/server.log'
rm 'user-list.txt'

It’s worth mentioning that the command will delete all files under the logs directory, so finally, it will delete the empty logs directory from the index. So, in this example, we have only one file under the logs directory.

Now, if we check the staged files, the deleted ones are gone:

$ git ls-files -c
.gitignore
README.md
some-file.txt

And of course, user-list.txt and logs/ are still in our local working tree:

$ ls -l
total 12
drwxr-xr-x 2 kent kent 60 May 13 00:45 logs/
-rw-r--r-- 1 kent kent 26 May 11 13:22 README.md
-rw-r--r-- 1 kent kent 21 May 11 13:22 some-file.txt
-rw-r--r-- 1 kent kent 16 May 13 00:45 user-list.txt

5. The Removed Files Are Still in the Git History

We’ve solved our problem using the git rm –cached command. However, We should keep in mind that we’ve merely removed the file from Git’s tracking index. We can still see the file and its content in Git’s commit history. For example, we can still see user-list.txt‘s content by checking a previous commit:

$ git show 668fa2f user-list.txt
commit 668fa2f...
Author: ...
Date:   ...

    add user-list.txt and some-file.txt

diff --git a/user-list.txt b/user-list.txt
new file mode 100644
index 0000000..3da7fab
--- /dev/null
+++ b/user-list.txt
@@ -0,0 +1,3 @@
+kent
+eric
+kevin

Knowing this is important since sometimes we’ve forgotten to add some sensitive files to the .gitingore file, such as credentials. But we’ve committed them and pushed the changes to the remote repository. After we realize it, we may want to wipe the sensitive files completely from Git history.

If this is the case, we need to remove the files from Git’s commit history.

6. Conclusion

In this article, we’ve shown how to remove a file or directory from a Git repository but keep its local copy through examples.

Also, we’ve addressed a quick way to remove all files defined in the .gitignore file. Finally, we should remember that the files removed by git rm –cached still live in Git’s commit history.