3
git checkout head "IgnoreMyChanges.cs"
IgnoreMyChangesdid not match any file(s) known to git.

Is there a parameter to the git command that says "get the file even though it's on the excluded list"

  • 2
    Why would you want so? An ignored file can't be checked out. It's not tracked. – ElpieKay May 27 '20 at 09:01
  • 1
    It's not tracked on my PC because I don't want to push any changes to it. It is in the remote repository and I want to get any updates to it. – AriesConnolly May 27 '20 at 09:16
  • 1
    I don't understand. If the file is tracked in the remote you `git pull` commits from the remote but then the file will no longer be ignored locally because it's tracked. – phd May 27 '20 at 12:46
  • @AriesConnolly So it's indeed ignored on your local branch (not added and committed), but it is tracked on some other branch in the remote repository? If so, `git checkout` can't work on `head`. You can try `git fetch origin && git show FETCH_HEAD:IgnoreMyChanges.cs > IgnoreMyChanges.cs` to retrieve its latest version on the remote repository and overwrite the local ignored one. And of course, please do backup the local `IgnoreMyChanges.cs` first. – ElpieKay May 27 '20 at 13:21
  • Did not work unfortunately. Not to worry I can manually download it from GitLab – AriesConnolly May 27 '20 at 13:42
  • Just FYI, `HEAD` is not case insensitive. It will work in some cases on case-insensitive file systems but will break in other cases. Always write it in upper case. – bk2204 May 27 '20 at 22:36

1 Answers1

1

TL;DR

If the file is in some commit, you can get it out of that commit with:

git checkout <commit-specifier> IgnoreMyChanges.cs

after which you perhaps should run:

git rm --cached IgnoreMyChanges.cs

The commit-specifier will almost certainly not be HEAD.

If you have Git 2.23 or later, you can use:

git restore --source=<commit-specifier> -w IgnoreMyChanges.cs

and you don't need the subsequent git rm --cached. Alternatively, you can, as ElpieKay suggested in a comment, use:

git show <commit-specifier>:IgnoreMyChanges.cs > IgnoreMyChanges.cs

which relies on your shell's redirection to write the file to your work-tree. Be aware that in some cases, using git show like this bypasses CRLF-line-ending handling.

Long

First, some general advice:

  • You should always spell HEAD in all uppercase. The lowercase spelling sometimes works, and sometimes does not. Don't do that. If you don't like typing that many uppercase letters, consider using @ instead of HEAD.

  • If your Git version is 2.23 or later, consider using the two commands git switch and git restore to do the two separate jobs that git checkout piles into a single command.

Given that you did not mention git restore, I'll stick with git checkout below, but note that there are two kinds of git checkout:

  • One kind of checkout checks out a whole commit. That's what git switch does when you use the new commands. This kind of checkout also changes which commit and branch the name HEAD (or @) refers to. (Remember, the "real name" of any commit is its raw hash ID. HEAD is short for both the current branch name and the current commit hash ID.) This kind of checkout is safe, in that it checks first to make sure it will not wreck any of your files.

  • The other kind of checkout checks out some specific files. That's what git restore does when you use the new commands. This kind of checkout does not change HEAD at all, and this kind of checkout is not safe: it will destroy unsaved work if you ask it to.

Both kinds of checkout are spelled git checkout using the pre-2.23 commands. It's occasionally hard to know for sure which kind you will get. In Git 2.23 or later, if the git checkout command could do either one, git checkout will force you to pick one, instead of just doing the wrong one. Unfortunately, in pre-2.23 Git, git checkout will sometimes pick the destroy your unsaved work variant when you did not expect that.

Using:

git checkout <commit-specifier> -- <file>

always invokes the second kind of git checkout—the kind that is now handled more directly by git restore. The commit-specifier part can be anything that specifies a specific commit, including:

  • a raw hash ID;
  • a branch name; or
  • the special name HEAD.

The double hyphen separates this part from the file-name part, and is optional. You'll need it if the file's name resembles a git checkout option, e.g., if the file is named --force or -m or something along those lines.

You can put the file's name in double quotes, to protect it from your command-line-interpreter. When and whether you need to do that depends not on Git but rather on your command-line interpreter.

With that out of the way, let's look at what goes in a commit and what .gitignore does

Now, files that are in Git are never ignored. In fact, the word "ignore" is the wrong verb. We use that word because the correct set of words is too long to bother saying, most of the time. The real trick here is to distinguish correctly between files that are in Git, and files that are not in Git.

Every commit—as found by a hash ID, or by some other name acceptable according to the gitrevisions documentation—contains a set of files, as a snapshot. So any given file either is in some commit, or is not in that commit.

If:

git checkout HEAD -- IgnoreMyChanges.cs

says:

IgnoreMyChanges.cs did not match any file(s) known to git.

then that file is not in that commit. If you want to know which commit that is—which hash ID, specifically—you can run:

git rev-parse HEAD

which asks Git the question: What hash ID does the special name HEAD mean?

The fact that IgnoreMyChanges.cs is not in that commit does not tell you whether the file IgnoreMyChanges.cs is in any other commit. While each commit is a full and complete snapshot of all of your files, it's a snapshot of all the files that you—or whoever—told Git to put in it at the time you (or whoever) made it. Once made, that snapshot holds that version of those files forever ... or at least, as long as that commit itself continues to exist. If you can supply the hash ID to Git, and that commit is in Git's big database of all-Git-objects, Git can get those versions of those files back.

These files are in Git. They are frozen into the commits, forever. Each commit has a full snapshot of every file, with de-duplication in effect. Since most commits mostly contain the same files as some other commit, this de-duplication means that the frozen commit file collection does not grow very fat very fast. But since they're frozen and in this special, weird, Git-only format, nothing else on your computer can use them.

To use these files, you have to have Git take them out of the commit. That's what the first kind of git checkout is about: we pick a commit—often by branch name—and have Git extract every file that is in it. These files get copied to a working area, where they are now normal everyday files. The working area is called your working tree or work-tree. These everyday files in your work-tree are yours, not Git's.

It's important to keep this in mind at all times. Your work-tree files are not in Git at all. They're just in your work-tree; they are yours to use as you see fit; and they are not the files that will go into a new commit either.

Git's index

When you have Git extract some commit—as in git switch or the first kind of git checkout—Git actually first copies the frozen-format files into Git's index. This index, which is so important and/or poorly named that it has two more names—it's also called the staging area, or sometimes (rarely now) the cache—has files that are sort of "half in" Git.

Extracting a full commit:

  • copies all the files to Git's index,1 where they have the frozen format but are no longer actually frozen;
  • then copies all the files from Git's index to your work-tree, overwriting the work-tree files.

That's the git switch style git checkout and it normally make sure that your work-tree files won't be destroyed by making sure they're all saved in the current commit—the one that is current before switching, that is.

Now that all the target (switch-to) files are in Git's index, Git can update your work-tree: Git will remove any file that's not in the target commit, that was in the index and your work-tree before, so as to get it out of the way. Git will replace any file that is in the target commit but is different. Files that are the same in both the previously-current and target commit can be left alone, and Git does that. So switching from one commit to another only updates or removes work-tree files that need to be updated or removed.

The end result is that both your work-tree and Git's index now match the commit you've picked out.2 That's important, because when you make a new commit, what Git really does is freeze into that new commit, exactly those files that are in the index right then.

Hence, what you do when preparing a new commit is:

  • adjust your files in your work-tree however you like, using any command you like; but then
  • use git add (and/or git rm) to update Git's index, because the index holds the proposed next commit.

Once you have the index's files—the staged copies—arranged the way you'd like, you run git commit and Git makes a new snapshot of the files that are in the index, or staging area. Since the index did match the commit you checked out, only anything you copied back into the index is actually changed.


1Technically, what's in the index is not a full copy of the file, but rather its name and mode and a blob hash ID. You can see the difference if and when you break down the index into its individual entries, using git ls-files --stage or git update-index. But when you're not using these low-level build-a-new-Git-command sub-commands, you don't need to worry about it: you can just think of the index as holding a copy of the file, ready to go into the next commit.

2There is a special case that occurs when you switch from one commit to another while holding some changes uncommitted. You can't always do this, and it's a bit complicated to describe when you can do it, but it has to do with whether Git can switch commits without overwriting any modified data in both Git's index and your work-tree. For a more complete picture of this special case, see Checkout another branch when there are uncommitted changes on the current branch.


Tracked vs untracked

Files in your work-tree are either tracked or untracked. A tracked file is, very simply, a file that is in your work-tree and in Git's index right now. An untracked file is a file that is in your work-tree but not in Git's index right now. And that's really all there is, but that is a lot.

Once you really understand how Git uses Git's index, the concept of a tracked file makes sense. A tracked file will be in the next commit. An untracked file won't be. Git builds new commits from the files that are in the index, at the time you run git commit. If the file is in there, it's in the new commit. If the file isn't in there, it's not in the new commit.

Files that are in the index are, as I put it earlier, sort of "half in" Git: they're ready to be frozen into a commit, and once they are frozen into a commit, they are safely stored. Now they are in Git for sure.

The files in your work-tree are not in Git at all. They can be copied to the index, and then they're half-in and committing will make them definitely all the way in, but until then they're just ordinary files that are not in Git.

.gitignore

Listing a file in .gitignore does three things:

  1. It tells Git: if this file is not now in the index (i.e., is untracked), don't complain about it when I run git status.

    The git status command prints some general helpful data, then runs two comparisons:

    • The first comparison compares the current commit (HEAD) vs Git's index. For every file that is exactly the same, git status says nothing. For every file that is different, in any way—modified, removed, or new—git status lists the file's name under the section changes staged for commit.

    • The second comparison compares Git's index to your work-tree. For every file that is exactly the same, git status says nothing. For files that are different (modified or removed), git status lists the file's name under the section changes not staged for commit.


    Note that untracked files are not yet listed in this second comparsion: files that aren't in the index, but are in the work-tree, don't have anything said about them yet. Having listed staged and unstaged files if any, now Git will gripe about these untracked files ... unless you've listed them in a .gitignore.

    Hence, listing the file in .gitignore tells Git: if the file is untracked, be silent. But it has no effect on a tracked file as the tracked file will go into the new commit.

  2. Git has en-masse git add operations, such as git add * or git add .. Listing a file in .gitignore tells Git that if the file is untracked, an en-masse add should not copy it into the index.

    Normally, an untracked file—one not in the index—would just be copied into the index, and now it would be a tracked file, with the index copy matching the work-tree copy as of the time you ran git add on it. But presumably you've listed the file in .gitignore to prevent it from being committed. Since Git will commit whatever is in Git's index, you want it not to get added to Git's index. Making git add skip over the untracked file achieves that goal.

    Once again, though, this has no effect on a tracked file. The tracked file is already in Git's index.

  3. Last—and usually invisible, but sometimes this causes problems—listing a file in .gitignore gives Git extra permission to wreck the contents of the file. Normally Git will tell you that some file would be overwritten or removed by a git checkout or git merge, but being listed in a .gitignore gives Git permission to destroy (or remove) the file in some cases.

So rather than calling this file .gitignore, Git perhaps should have called it .git-ignore-these-files-when-they-are-untracked-and-also-do-not-automatically-add-them-with-en-masse-add-operations-unless-they-are-already-tracked. (It might even mention the occasionally-clobber-them aspect.) But that's a ridiculous thing to have to type in, so .gitignore it is.

Conclusion

It's important to be aware of what tracked vs untracked really means: in the index, or not. It's important to be aware of the fact that Git makes new commits from the index, not from your work-tree, and that none of your work-tree files are actually in Git at all. And it's important to be aware that listing a file in .gitignore has no effect when the file is tracked.

Usually, if a file isn't in the current (HEAD) commit, it also isn't tracked now. That's because you got into this state by running git checkout or git switch to completely fill Git's index, and your work-tree, from the HEAD commit. So if it was not, and is not, in HEAD, it was not put into (or was even taken out of) the index, and therefore is untracked. But because you can deliberately take things out of the index:

git rm --cache somefile

or put things into the index:

git add somefile

that's not a hard and fast guarantee.

If some file is untracked, listing it in .gitignore matters. But if it's tracked, that's all that really matters: it is tracked and it will be in the next commit.

torek
  • 448,244
  • 59
  • 642
  • 775