gitgit-resetgit-rmgit-stagegit-track

Git: how to untrack files without staging them for deletion


I have some config files that I want to change locally but not risk accidentally committing those changes. They also cannot be added to gitignore because they need to be tracked for the project as a whole. When I modify those files to suit my environment's needs, my git status looks like this:

Changes not staged for commit:
    (use "git add <file>..." to update what will be committed)
    (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   config1.cfg
        modified:   config2.cfg

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

        whatever.html
        somethingElse.js

then I run:

git rm --cached config1.cfg config2.cfg

and git status looks like this:

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        deleted:    config1.cfg
        deleted:    config2.cfg

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

    config1.cfg
    config2.cfg
    whatever.html
    somethingElse.js
    

Running git reset --HEAD config1.cfg config2.cfg returns them to the staging area, predictably. But it kind of doesn't make sense that something could be untracked and still staged at the same time, even if that staging is for deletion. But also -- I didn't delete the files, but if I commit now then they will be deleted.

I realize that this might be a better use case for smudge and clean filters, but is it possible to reach a state where a previously tracked file is untracked and it is as if it has never been tracked? If not, is there a good reason it isn't possible?

Editing to clarify now that I understand the situation a little better: the scenario I'm trying to create is one where the files are untracked on my local while I'm working, without affecting their tracking status on the remote.


Solution

  • but if I commit now then they will be deleted.

    No. The files (the things you see in front of you in the work tree) will not be deleted. Committing has no effect on the work tree.

    The commit will record them as deleted, as it must do, because they were present in the previous commit and now you are saying they should not be present in this next commit. That is what a deletion is: something that was present is now no longer present.

    The Three Places of Git

    It sounds like you might benefit from knowing a bit about what Git "is". Git exercises sway over three places:

    When you check out a branch, you are checking out a commit. The commit is used to populate the work tree and the index; they all match up at that moment.

    If you change one file in the work tree (which is the only place where you can change anything) and add it and commit, then only that one file changed, true, but all the other files are still there in the work tree and the index, so this commit contains all the files — not just the file that you changed (that is a common misconception). Every commit is a snapshot of the whole state of the index at the time it was created, which in turn is a reflection of the whole state of the work tree, mediated by what you have elected to add to the index.

    So the index is the place where you get to manipulate the relationship between what you did in the work tree and what will go into the next commit. When you say git add ., you are saying, in effect, "the index should reflect everything I did in the work tree". But that is a very broad command; you have absolute file-by-file fine control of what the index should look like in the run-up to the next commit.

    What You Did

    When you said git rm --cached config1.cfg, you said, "Remove the reference to config1.cfg from the index." This will have no effect on the work tree when you commit; it will just make a commit in which config1.cfg happens to be absent.

    But now you are thinking about that, and you are realizing that isn't what you meant to say, because the next commit will constitute a deletion of config1.cfg. You don't like that. You do want the next commit to still contain config1.cfg; you just don't want it to be changed from the previous commit.

    That, I take it, is what you mean when you say:

    I have some config files that I want to change locally but not risk accidentally committing those changes

    What You Should Have Done

    Your goal was not to make a commit in which config1.cfg was not present. Your goal was to make a commit in which config1.cfg was unchanged from its previous state, even though you have in fact edited it in the work tree.

    Okay, so what should you have done in order to make that happen? Well, the first line of defense is: don't say git add . or some blanket globbing statement like that. Instead, in order to not add changed files to the index, don't add them. Just add to the index the changed files that you do want to reflect in the next commit.

    But let's say you recklessly blew past that safeguard and you did add config1.cfg in its changed state to the index. (You did.) Then say git restore --staged config1.cfg. This copies config1.cfg out of the last commit (every commit contains all the files, remember?) into the index. So now the config1.cfg in the index looks like the previous commit, not like the work tree version. The status will be then that git knows you have modified config1.cfg in the work tree (unless you have added it to the gitignores file), but that change is not slated to be included in the next commit you make.

    (Before Git version 2.25.0, you would have used a form of git reset to accomplish this goal, namely git reset config1.cfg. But git reset is scary, so if you have a sufficiently new Git, use restore instead.)