.gitignore Syntax: bin vs bin/ vs. bin/* vs. bin/**

bin matches any files or directories named 'bin'.

bin/ matches any directories named 'bin', which in effect means all of its contents since Git doesn't track directories alone.

bin/* matches all files and directories directly in any bin/. This prevents Git automatically finding any files in its subdirectories, but if, say a bin/foo subdirectory is created, this rule will not match foo's contents.

bin/** matches all files and directories in any bin/ directory and all of its subdirectories.

The word "any" is critical here since rules are not relative to the repository root and apply anywhere in the filesystem tree. You must begin rules with a / (or !/ to un-ignore) which means the repository's root, not the system's root, in order to match only what was intended.

WARNING: You should never use rules like dir/*, /dir/**, etc. alone unless you also un-ignore something that exists inside that directory. Omit the asterisk or you could permanently lose a lot of data from certain invocations of git gc, git stash and more.

I don't really know what tmp/**/* is meant to do. I initially thought it could be used to match files in the sub-directories of tmp/ but not files directly present in tmp/ itself. But a simple test seems to suggest that this ignores all files in tmp/.


bin and bin/ differ only in that the latter will only match a directory.

bin/**/* is the same as bin/** (apparently since 1.8.2, according to @VonC's answer).

The tricky one, that I just spent an hour or so ripping my hair out over, is that bin/ and bin/** are not quite the same! Since the earlier ignores the directory as a whole, and the latter ignores each of the files within it, and git in nearly all cases doesn't care about directories, there is normally no difference. However, if you try to use ! to un-ignore a subpath, then you will find that git (ahem) ignores it if you ignored the parent directory! (again, rather than the directory contents)

This is clearest by example, so for a newly init-ed repository set up so:

$ cat .gitignore
ignored-file
or-dir
dir-only/
!dir-only/cant-reinclude
dir-contents/**
!dir-contents/can-reinclude

$ mkdir or-dir dir-only dir-contents

$ touch file ignored-file or-dir/ignored-file dir-only/cant-reinclude dir-contents/can-reinclude

The following untracked files exist:

$ git ls-files --other
.gitignore
dir-contents/can-reinclude
dir-only/cant-reinclude
file
ignored-file
or-dir/ignored-file

But you can see the following files are not ignored:

$ git ls-files --other --exclude-standard
.gitignore
dir-contents/can-reinclude
file

And if you try to add, you get:

$ git add dir-only/cant-reinclude
The following paths are ignored by one of your .gitignore files:
dir-only/cant-reinclude
Use -f if you really want to add them.
fatal: no files added

I consider this behavior a bug. (This is all on git version 1.8.4.msysgit.0)


Note that, strictly speaking, git does not track directories, only files. It is hence not possible to add a directory, only its contents.

In the context of .gitignore however, git pretends to understand directories for the sole reason that

It is not possible to re-include a file if a parent directory of that file is excluded.
https://git-scm.com/docs/gitignore#_pattern_format

What does this mean for the exclude patterns? Let's go through them in detail:

bin

This ignores

  • files named bin.
  • the contents of folders named bin

You can whitelist ignored bin files and folders by adding subsequent ! entries, but you cannot whitelist the contents of folders named bin

bin

!bin/file_in_bin # has no effect, since bin/ is blacklisted!
!bin/* # has no effect, since bin/ is blacklisted!
!file_in_bin # has no effect, since bin/ is blacklisted!

!bin # this works

bin/

Same as above, except it does not match files named bin. Adding a trailing / tells git to match directories only.

bin/*

This ignores

  • files contained in a folder named bin
  • contents of direct subfolders of folders named bin
bin/*  # blacklists bin/file_in_bin and bin/subfolder/

!bin/subfolder/file_in_sub # has no effect, since bin/subfolder is blacklisted!
!bin # whitelists files named bin/bin, since bin/ itself is not blacklisted
!bin/ # has no effect, since bin/ itself is not blacklisted


!bin/file_in_bin # works since bin/ itself is not blacklisted
!file_in_bin # works too
!bin/subfolder # works (so implicitly whitelists bin/subfolder/file_in_sub)
!bin/subfolder/ # works just as well
!bin/* # works for file_in_bin and subfolder/

bin/**

This ignores

  • contents of bin
  • contents of subfolders (any level of nesting) within bin
bin/**  # blacklists bin/file_in_bin and
        # bin/subfolder/ and bin/subfolder/file_in_sub and
        # bin/subfolder/2/ and bin/subfolder/2/file_in_sub_2

!bin/subfolder/file_in_sub # has no effect, since bin/subfolder is blacklisted
!bin/subfolder/2/ # has no effect, since bin/subfolder is blacklisted
!bin/subfolder/2/file_in_sub_2 # has no effect, since bin/subfolder is blacklisted

!bin/subfolder # works only in combinations with other whitelist entries,
               # since all contents of subfolder are blacklisted (1)

!bin/file_in_bin # works since bin itself is not blacklisted
!bin/* # works for file_in_bin and subfolder; see (1)