Home / Alt manpages / gitignore(5)

  • gitignore(5)
  • File format
  • linux

Make Git Ignore the Right Files, and Prove Which Rule Won

One misplaced pattern in .gitignore and Git starts hiding files you actually wanted, or tracking ones you did not. This guide sets up a small, predictable ignore policy and shows you how to prove exactly which rule matched a given path. The examples use Git 2.43.0 from the Ubuntu git-man package installed on this machine. Allow about 10 minutes for a first setup.

  • You need: a shell, Git, and a repository where you can create test files.

1. Decide where each rule belongs

  • Repository .gitignore: for anything every clone should ignore, such as build output, test artefacts and editor directories the whole team generates.
  • .git/info/exclude: for a rule useful on your checkout only, that should never be committed, such as a local scratch directory.
  • The file named by core.excludesFile: for a personal rule that should apply across all your repositories, such as editor backup files.

The installed manual lists these sources in precedence order: command-line patterns supported by a particular command, nested .gitignore files, $GIT_DIR/info/exclude, then core.excludesFile. Within one level, the last matching pattern wins. That ordering is exactly why a shared rule belongs in the shared file rather than being patched around with a personal exception.

2. Add shared project rules

From the top level of your repository, create or edit .gitignore. Keep comments and blank lines: they make a pattern file easier to review.

# Build output anywhere below this repository
build/

# Compiled object and archive files
*.[oa]

# Local environment files at the repository root
/.env.local

A trailing slash restricts build/ to directories. A pattern without a slash, such as *.o, can match at any level below the directory containing the ignore file. The leading slash in /.env.local anchors that name at the repository root.

Check the file before moving on:

$ sed -n '1,80p' .gitignore
build/

*.[oa]

/.env.local

3. Keep checkout-specific rules local

Use .git/info/exclude for files that should stay out of this checkout without affecting colleagues. This file lives inside the repository metadata and is not normally committed.

$ cat >> .git/info/exclude <<'EOF'
# Personal scratch files for this checkout
/scratch/
EOF

Do not put a rule here that the project actually depends on. A fresh clone will not receive it, so the same generated files can reappear as untracked elsewhere.

4. Set a global personal exclude file

For a rule that belongs to your workstation rather than a project, configure an explicit file. This example creates a directory under your XDG configuration directory and points Git at it.

$ mkdir -p "$HOME/.config/git"
$ touch "$HOME/.config/git/ignore"
$ git config --global core.excludesFile "$HOME/.config/git/ignore"
$ printf '%s\n' '*~' '.DS_Store' >> "$HOME/.config/git/ignore"
$ git config --global --get core.excludesFile
/home/your-user/.config/git/ignore

Replace /home/your-user in the displayed result with your actual home directory; Git prints the path configured on your machine. If XDG_CONFIG_HOME is set, Git's documented default is $XDG_CONFIG_HOME/git/ignore; otherwise it is $HOME/.config/git/ignore. Setting it explicitly beats guessing which location is active.

No elevated privileges are needed here. Avoid sudo: a root-owned global file is almost never the policy you actually want for your own account.

5. Understand matching before adding exceptions

A pattern beginning with ! reverses an earlier match, which is handy for keeping one file while ignoring a whole class of files.

# Ignore generated HTML, except the hand-maintained index
*.html
!index.html

The exception must be reachable. If a parent directory is excluded, Git does not search inside it, so a later negation cannot bring a file back. To keep one path under a mostly ignored tree, leave the route open with directory patterns:

/*
!/docs
/docs/*
!/docs/README.md

Here, /* excludes top-level entries, !/docs keeps the directory reachable, /docs/* excludes its contents, and the final rule restores the selected file. Test a structure like this before committing a broad policy: an accidental pattern such as docs/ can make every apparent exception silently ineffective.

Wildcards do not all cross directories. * and ? match characters except /. So foo/* matches foo/test.json and the directory foo/bar, but not foo/bar/file.txt. Use the documented ** forms when you mean arbitrary directory depth, for example cache/** for everything below a repository-relative cache directory.

6. Verify an ignored path and its rule

git status --short --ignored gives a useful overview, but it will not tell you which pattern won. Use git check-ignore --verbose for that. The command exits successfully when a path is ignored, so the output is safe to use in scripts.

$ mkdir -p build scratch
$ touch build/output.o scratch/notes.txt
$ git check-ignore --verbose -- build/output.o scratch/notes.txt
.gitignore:2:build/    build/output.o
.git/info/exclude:2:/scratch/    scratch/notes.txt

The columns are the matching source, line number and pattern, followed by the path. Your line numbers will differ if your files have different comments. If the command prints nothing and returns a non-zero status, the path is not ignored by any source Git considered.

$ git check-ignore --verbose --no-index -- tracked-file.txt
$ printf 'exit status: %s\n' "$?"
exit status: 1

--no-index is useful when checking a path that is already tracked. Normally an ignore rule does not affect tracked files, which trips people up constantly.

7. Stop tracking a file safely

Adding a tracked filename to .gitignore does not remove it from Git. If the file should stay on disk but stop being versioned, inspect the path first, add the rule, then remove only its index entry.

$ git ls-files --error-unmatch .env.local
.env.local
$ printf '%s\n' '/.env.local' >> .gitignore
$ git rm --cached -- .env.local
rm '.env.local'
$ git status --short --ignored -- .env.local
D  .env.local
!! .env.local

This stages a deletion for the next commit while leaving the working-tree file in place. The !! line confirms the remaining file is now ignored.

Warning

This is a state-changing command. Do not run it on a shared or sensitive file until you have confirmed that removing it from version control is what you actually want.

To undo before committing, restore the index entry and remove the rule you just added:

$ git restore --staged -- .env.local
$ git restore --source=HEAD --staged --worktree -- .gitignore
$ git status --short -- .env.local .gitignore

The second command restores the whole tracked .gitignore, so skip it if that file has other uncommitted edits; in that case, edit out only the new line and keep the staged state under review.

Common traps

  • Ignored does not mean deleted. Git will not remove an ignored file, and it will not hide a file already in the index.
  • Leading slashes are relative to the ignore file. A nested ignore file has a different anchor from the root file.
  • A trailing slash matches directories only. Not a regular file or symbolic link with the same name.
  • Escape literal # and !. Use a backslash before a leading # or ! when those characters are literal pattern text. Trailing spaces are ignored unless escaped.
  • Symlinked ignore files are not followed. Git does not follow symbolic links when reading a working-tree .gitignore, so store the real rules in the repository file, not behind a symlink.

Done means

  • Shared rules committed. Shared generated files are covered by the committed .gitignore.
  • Local rules separated. Checkout-only and workstation-only files sit in their own exclude sources.
  • Matches explainable. git check-ignore --verbose -- PATH identifies the rule for each surprising match.
  • Tracked files sane. No tracked file is being mistaken for an ignored one.
  • Untracking reviewed. Any untracking operation was checked with git status before committing.