Home > OS >  .gitignore will not untrack a file after removing it from the working tree
.gitignore will not untrack a file after removing it from the working tree

Time:10-23

I am running into an issue whereby .gitignore will not ignore a file ('private/config.php').

The .ignore file is as follows:

public/uploads/
logs/
private/config.php

The file was initially included in the tracked files so I removed it from the branch as follows, but as you will see from the git ls-tree command, it still shows in the list of tracked files...

ubuntu@MYMACHINE:/var/www/gig$ git rm --cached private/config.php
rm 'private/config.php'
ubuntu@MYMACHINE:/var/www/gig$ git ls-tree -r main --name-only
.gitignore
README.md
private/config.php
private/initialize.php
[...]

Strangely, I run into the same issue when issuing the commands in the local folder:

ubuntu@MYMACHINE:/var/www/gig/private$ git rm --cached config.php
fatal: pathspec 'config.php' did not match any files
ubuntu@MYMACHINE:/var/www/gig/private$ git rm --cached private/config.php
fatal: pathspec 'private/config.php' did not match any files
ubuntu@MYMACHINE:/var/www/gig/private$ git ls-tree -r main --name-only
config.php
initialize.php

Any clues on how to purge this file out of the tracked files?

CodePudding user response:

Listing a file in .gitignore never causes it to become untracked.

A file either is tracked, or is untracked. This property is determined by the answer to a single question:

  • Is the file currently in Git's index?

If the answer is "yes", the file is tracked. If the answer is "no", the file is untracked.1


1If the file is also missing from your working tree, most people would just call it "non-existent", but it's true that all non-existent untracked files are also untracked. The non-existent file "zaphod_beeblebrox" is untracked right now.


About Git's index

Now, the actual tricky part here has to do with Git's index. The index—which is an important thing in Git; you need to know all about it—is very hard to see. You "see" the index mostly by contrast. But what the index is is pretty easy to describe, although this misses some nuance:

Git's index holds your proposed next commit.

All commits, once made, are completely read-only, frozen in time for all time. No part of any existing commit can ever be changed.

You make new commits by:

  • extracting some existing commit (into your working tree, which gets you ordinary read/write files that you can change);
  • doing some work there, with files that you can see and work with; and
  • eventually, using git commit to make a new commit.

In most version control systems, their "commit" verb takes whatever is in your working tree at that time, and uses that to make the new commit. Git is ... different. Git ignores your working tree when you run git commit, and instead, uses all the files that Git has stored in Git's index.

Since Git actually builds the commit from the index files, the files that are in the index are critically important. You then need to know that extracting a commit—as in git checkout somebranchfills in Git's index from that commit.

Once the index is filled in from some commit, and the files are extracted from the commit, all three active copies match. You have one file that literally can't be changed, in the current commit. You have another file of the same name stored in Git's index. You have a third copy of that file as a regular, read/write file in your working tree.

Once you've changed some file in your working tree, you need to run git add. This copies the file from your working tree, back into Git's index.

As a result, Git also uses the term staging area for the index. The index copy is said to be "staged for commit". Running git commit commits whatever is in the index—whether that's the original copy of the file, extracted earlier, or the updated copy, from your recent git add.

If you remove a file from Git's index, that file won't be in the next commit. This is what git rm is about.

Alas, if you run git rm somefile, Git removes the file somefile from both Git's index and your working tree. So now it's really gone. It's untracked too, of course, but without a working tree copy, it's just plain gone.

So, you can git rm --cached: this removes only the index copy, leaving the working tree copy alone. Now the file is untracked (won't be in the next commit), but still exists in the working tree.

Once you make the new commit, extracting the new commit won't extract the file, as it's not in that commit. But remember, extracting the old commits—those that have the file—will extract the file, both to Git's index and your working tree. So the file will magically—actually, rather un-magically—become tracked at that point. Switching to a new commit that lacks the file will now remove the file from both Git's index and your working tree, and now it's untracked—but also gone from your working tree.

Ultimately, this means that having committed a file, then removing it and committing the removal, sets up a trap: if you git rm --cache the file so that you commit the removal without erasing the working tree copy, you're still left with:

  • old commit has the file
  • new commit lacks the file
  • so switching from old to new removes the file

which means you must now be very careful every time you go back to the old commits.

(See also IMSoP's answer.)

CodePudding user response:

You seem to be muddling a couple of concepts here:

  • The "cached" in git rm --cached refers to the "cache", "index", or "staging area", which is basically the set of files to include in the next commit.
  • The "main" in git ls-tree -r main refers to the branch called "main" - or, more specifically, to the tree of files pointed to by the commit, pointed to by the branch reference called "main". In other words, it's listing files from the last commit you made on that branch.

So there is no surprise at all that removing something from the next commit doesn't change what is in an existing commit. You need to create a commit that doesn't contain that file.

  • Related