Search code examples
gitgitignore

.gitignore The different between /foo and /foo/


Are there any different between foo/ and /foo/ in .gitignore? I've been reading in git-doc but I found nothing. Are those two the same?


Solution

  • There is a difference between /foo and foo.

    To understand the difference, think about directories (or folders if you prefer that word) and the fact that they may contain nested sub-directories (sub-folders):

    foo/a/file1
    foo/b/file2
    foo/file3
    quux/foo/file4
    

    Here, the top level directory foo contains two sub-directories a and b plus one file, and the sub-directories contain one file each. The top-level directory quux contains a sub-directory foo, which contains one file.

    If your .gitignore lists foo

    If you tell Git to ignore foo, Git won't have to look inside foo to find foo/a, foo/b, and foo/file3. Having not looked inside foo, Git won't look inside foo/a or foo/b either, and won't find either of foo/a/file1 or foo/b/file2.

    Meanwhile, nothing tells Git to skip quux, so Git will look inside quux and find foo. If you tell Git to ignore foo, Git won't have to look inside quux/foo, so it won't find quux/foo/file4.

    If your .gitignore lists /foo instead

    However, if you have told Git to ignore /foo rather than foo, when Git finds foo inside quux, it won't match /foo because /foo means only ignore foo at the top level. So Git will look inside quux/foo and hence will find quux/foo/file4.

    Suffix slashes

    As Void Raider answered, writing foo/ in a .gitignore tells Git that it should match (and hence not look inside) the directory foo, but should not match (and hence will gripe about—see below) a plain file named foo. Here, too, the leading slash remains significant: it tells Git whether to apply this to all things named foo, or only those things at the top level.

    More detail, because the details matter

    All of the above describes the action as if you have only one .gitignore file stored at the top level of your work-tree. But you can store one .gitignore file per directory. If you have a quux directory, you can create quux/.gitignore, for instance. And, in the top level .gitignore, you could list a name like quux/foo, or you could list foo in quux/.gitignore. What do each of these do?

    First, let's define some terms:

    • Unix-style path names are normally called either absolute if they start with /, such as /a/b/c, or relative if they do not, such as a/b/c. An absolute path tells the operating system to start at the top of the host's directory tree, while a relative path tells the operating system to start at the current directory.

    • Unix-style shells support what is called globbing or glob actions on path names. Here, for instance, * matches any number (including zero) of any character, so that a name like a*c matches ac, abc,abbbc,axyzc`, and so on.

    • Git supports (in multiple places, including in .gitattributes and as command line options) path name patterns, which Git calls pathspecs. Different features in Git support different kinds of pathspecs. Ignore files in particular implement glob patterns.

    In a .gitignore file, any absolute path always refers to the current directory. Hence, if quux/.gitignore exists, and contains /foo, that matches a file or directory named quux/foo. You could put this into the top level .gitignore file as /quux/foo.

    In any .gitignore file, any path name that contains an embedded / (not at the end of the pattern) is also treated as absolute! This means that putting quux/foo into the top level .gitignore is the same as putting /quux/foo into the top level .gitignore!

    Putting foo (with no leading slash) into quux/.gitignore means that Git would ignore quux/sub/foo, while putting quux/foo into the top-level .gitignore means that Git would not ignore quux/sub/foo, because of this peculiarity of treating embedded-slash pathnames as if they were absolute.

    An important caveat: to be ignored, a path must not reside in the index

    If a path name appears in your repository's index, no amount of fiddling with .gitignore files will make it ignored.

    To see everything that is currently in the index, use git ls-files --stage. Note that this prints a lot of names in a large repository! This is why you normally don't ever look at the index directly: it's just too much, like trying to look at the sun. It's much better to compare what's in the index to something else. That's what git status does, and that's where .gitignore files really enter the picture.

    When you run git status, Git runs two comparisons. The first one compares the current, or HEAD, commit to the index. Whatever is different here, git status calls this staged for commit. This is because git commit will use whatever is in the index right now to make the new snapshot. We're usually concerned not with every file that will be in the snapshot, but rather with those files that will be different in the new snapshot, as compared to the current snapshot. So that's what git status shows us: files that are different, or staged for commit.

    The second comparison compares what's in the index to what is in your work-tree. Whatever is different here, git status calls this not staged for commit or—for some files—untracked. Some of these untracked files will be files that you do not want to commit, and you do not want Git bothering you about them.

    An untracked file is quite simply defined as a path name that is not in the index at all. If that same path name exists in the work-tree, the file is untracked. Git will whine about it. Listing that path name in a .gitignore file will tell Git: Shut up about this file, it's supposed to be untracked. But if it's tracked—if the file is already in the index—Git won't check .gitignore for its name; it will just assume that the file should be committed.