Reputation: 3568
I have a local repository and trying to discard all changes it it since the last commit via
git checkout HEAD -- *
command. Everything works fine, even if the changes were at some subdirectory. But when I add some untracked file (satisfying a mask in .gitignore
), say 'Ignored.txt' to the root of the repository, the above command fails with the message
error: pathspec 'Ignored.txt' did not match any file(s) known to git
In contrast,
git checkout HEAD -- .
works as expected. So I want to know:
What is the difference between
.
and*
wildcards in git?
Upvotes: 2
Views: 2627
Reputation: 489083
The major difference isn't in Git at all.
Because it's not in Git, whether it makes any difference, and if so, what difference, depends on the not-Git command interpreter you are using.
On a Unix-ish system (including Linux), it's the command interpreter (or "shell") that expands *
for you. The various shells, such as bash
and zsh
and fish
and tcsh
and dash
and so on (most of them have names ending in sh
), interpret *
to mean "most of the files in the current working directory". This kind of expansion is called globbing.
These shells do not interpret and expand .
in any way. This means that:
git xyzzy -- .
invokes git
with three parameters, xyzzy
, --
, and .
. But:
git xyzzy -- *
invokes git
with, say, seven parameters: xyzy
, --
, a
, b
, c
, d
, e
, if there are files named a
, b
, c
, d
, and e
in the current working directory.
Git is primarily interested in commits, stored in a repository. To build new commits, Git uses its index, also stored in the repository. The index actually holds the files, in a special, Git-only, compressed format—essentially the same as what's in the commits.
The index copy of a file is writable, while the committed copies of any file are all read-only. (Technically, Git just keeps objects in its main object database, and both the index and commit copies are just references to these objects. You cannot overwrite any object, but you can add a new object and switch the index reference around; you cannot change the commits' references. The effect is that files in commits are frozen, while files in the index are thawed / writable.)
This internal Git-only file format is not useful to you, the user, nor to most programs on your computer. So these files have to be expanded. They get expanded into your work-tree, which lives in a directory (or folder) that may have subdirectories (sub-folders). You can set your current working directory to any of these work-tree directories. The shell will then expand *
to the names of the files found in this current directory within the work-tree.
Git primarily uses your current working directory to find the repository database, where Git's real files live. The work-tree copies are just for you to fiddle with as you please.
Of course, git checkout
and git add
both use the work-tree in various ways. Note, however, that since the work-tree is for your use, you can put files into it that do not exist in the repository itself.
A file can be in your work-tree, but not in your index. (The index lives, in effect, "between" the work-tree and the repository proper, and provides a place for Git to store files that will go into the next commit.) A file that is in this state—in your work-tree, but not in your index—is said to be untracked.
Git inherently does not know about untracked files. But they are in your work-tree (by definition), so if that is also your current working directory, and you use *
and have the shell expand the *
to these file names, you'll pass to Git a file name that it doesn't currently know about.
If the command you use is git add
, you will tell Git: copy this file from the work-tree into the index. That will create the file in the index, so that it is now going to be in the next commit you make. Git is fine with that!
But if the command you use is git checkout
, you will tell Git: copy this file from the index into the work-tree. Git won't find the file in the index, so it will complain. (It won't touch the file in the work-tree.)
Note that a file that is untracked can be, but isn't necessarily, also ignored (this is not a very good term, but it's the one Git uses). You tell Git not to complain about untracked files by listing their names, or patterns for their names, in files named .gitignore
. In this case, git add
will warn you that an untracked-and-ignored file is ignored: Git won't copy that file into the index, even if you use git add *
.
.
If you use git add .
or git checkout -- .
, the shell does no expanding at all. Git sees the .
and knows that this means "the current working directory", so if it's appropriate—e.g., for git add
—Git will read the current working directory. It can compare the files it finds there to those that are already in the index, and know how to update them for git add
, including to not add files that are (1) untracked and (2) listed in .gitignore
. (Files that are already in the index aren't ignored, by definition, so those also get updated from the work-tree into the index.)
With git checkout -- .
, Git looks directly in the index, and doesn't see files that are untracked at all.
The above is fairly simple, but there are several special cases that complicate it:
Most shells do not match dot-files (such as .profile
or .gitignore
) when expanding *
. So, if you have files .gitattributes
and .gitignore
in the current directory, and use git checkout -- *
, you probably won't copy the index version of these files into the work-tree.
If there are no files at all in the current directory, or none that match a glob pattern like *.asdf
, some shells complain and abort the command, but others just pass the pattern—*.asdf
or even *
—on to the program you're running.
DOS-style CMD.EXE does not expand *
.
For the last two cases, Git itself does see the *
. Now Git gets a chance to do glob expansion, and in Git's case, Git does match .
-files like .gitattributes
and .gitignore
. So if the current working directory has no non-dot-files, but does have .gitattributes
and .gitignore
, and you run:
git checkout -- *
then in this particular case, Git will copy .gitattributes
and .gitignore
from the index to the work-tree.
Upvotes: 7
Reputation: 45719
The first thing to know about this is, .
is not a wildcard.
.
and *
can both be path specs (and that's how you mean to be using them in the commands you're using). To understand how path specs are interpreted, you can look under path spec in the git glossary (https://git-scm.com/docs/gitglossary)
But another complication is that in your "failing" example, git is not receiving a pathspec of *
, because your shell is expanding the *
before passing it to git. So to fully understand this behavior, you would also refer to your shell's documentation regarding how it pre-processes command lines.
to pass *
as a path spec without interference from your shell, you could escape it. (Assuming you're using bash or something similar, this could look like
git checkout -- '*'
but again, it depends on which shell is in use.)
Upvotes: 2
Reputation: 30267
That's file globbing. https://www.w3resource.com/linux-system-administration/file-globbing.php
.
represents "current directory" whereas *
means "substitute it for all possible values". Normally what happens is that bash (if you are using bash) will take the * and will actually replace it for all the possible values where you are (if you only provide a single * as parameter) and will (after replacing all the values) hand it over to git. Only if there are no matching values (for example, you write 'blahblah*' and there are no files there that match the pattern) will then bash give up replacing the value and will handover a '*' to git.
Upvotes: 1
Reputation: 241978
Git doesn't see the asterisk. It gets expanded by the shell to all the file and directory names in the current directory (that don't start with a dot). The dot is not a wildcard, it just means "the current directory". So
git checkout HEAD -- .
checks out the current directory with all its subdirectories, i.e. everything git knows about. With the asterisk, git sees
git checkout HEAD -- tracked-file1 tracked-file2 Ignored.txt
but it doesn't know how to checkout the ignored file: hence the error.
Upvotes: 3