John McClane
John McClane

Reputation: 3568

Difference between . (dot) and * (asterisk) wildcards in git

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

Answers (4)

torek
torek

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.

Shell vs Git

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 doesn't use the working directory all that much

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.

Some Git commands do use the work-tree

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 *.

Git does know about .

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.

Special cases: "dot-files", no files at all, and CMD.EXE

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

Mark Adelsberger
Mark Adelsberger

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

eftshift0
eftshift0

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

choroba
choroba

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

Related Questions