guide

gitignore Precedence and Negation (!) Patterns

How the last matching rule wins, the precedence between .gitignore, info/exclude and global settings, and why ! cannot restore files when a parent directory is excluded, with fixes.

When gitignore rules do not behave as expected, the cause is usually order or a parent directory. This article explains which of several rules git follows.

Within one file: the last matching rule wins

Within a single .gitignore, git reads from top to bottom and the last matching pattern determines the result. If that pattern starts with !, the path is not ignored; otherwise it is ignored.

*.log
!important.log

important.log matches both lines, but the last one is a negation, so the file is tracked. Reverse the order and the result is reversed too.

!important.log
*.log

Now the last match for important.log is *.log, so it is ignored. This is why, when you merge several templates in the generator, the selection order becomes the rule order. For example, when you use Java's *.jar together with Gradle's !gradle-wrapper.jar, Gradle must come after Java.

Precedence across files

git reads rules from the following four places. The higher in the list, the higher the precedence.

  1. Patterns given on the command line (some commands, such as git ls-files --exclude)
  2. .gitignore in the same directory as the path or in a parent directory — files deeper in the tree override those above
  3. $GIT_DIR/info/exclude (usually .git/info/exclude)
  4. The file pointed to by the core.excludesFile setting (the global gitignore)

For example, if the root .gitignore contains *.tmp and sub/.gitignore contains !*.tmp, sub/y.tmp is tracked according to the negation rule in the lower file. Conversely, even if you write !x in .git/info/exclude, the x in .gitignore takes precedence, so x is still ignored.

$ git check-ignore -v x y.tmp sub/y.tmp
.gitignore:1:x	x
.gitignore:2:*.tmp	y.tmp
sub/.gitignore:1:!*.tmp	sub/y.tmp

The most important exception: files cannot be restored once a parent directory is excluded

The git documentation states it this way: it is not possible to re-include a file if a parent directory of that file is excluded. For performance reasons git does not look inside excluded directories at all, so ! rules for files inside them are never even read.

logs/
!logs/keep.txt

With these rules, logs/keep.txt is still ignored. Running git check-ignore -v shows logs/ as the deciding rule.

The fix is to exclude the contents of the directory rather than the directory itself.

logs/*
!logs/keep.txt

logs/* does not match the logs directory itself, so git goes inside and applies the negation rule to keep.txt. The Rails template's /log/* + !/log/.keep and the VS Code template's .vscode/* + !.vscode/settings.json both rely on this principle.

To restore a deeper path, you must also restore each intermediate directory one by one.

config/*
!config/app/
config/app/*
!config/app/defaults.yml

Allowlist patterns

The same principle applies when you want to list only what to track instead of listing what to ignore.

# Ignore everything at the root
/*
# Re-include only what is needed
!/.gitignore
!/src/
!/README.md

/* only matches each entry at the root and does not exclude the root itself, so !/src/ works. With this approach .gitignore itself is also caught by /*, so do not forget !/.gitignore. In fact, if you run git status --ignored without that line, .gitignore appears in the ignored list.

Duplicate rules and order

When the same rule appears several times, the earlier ones do not affect the result, because the later identical rule always matches afterward. On the other hand, removing the later duplicate can change the result if there is a negation rule in between.

*.log
!debug.log
*.log

Here, removing the last *.log makes debug.log tracked. This site's generator detects such cases and removes a later duplicate only when there is no negation rule in between, leaving the rest in place.

References

← PreviousThe Complete Guide to .gitignore Pattern Syntax