Gitignore

CIHex pmLicense: MIT

Correct gitignore and wildmatch semantics for Elixir.

Gitignore answers whether a normalized, /-separated relative path is ignored by gitignore-style rules. It supports negation, directory-only rules, anchoring, **, last-match-wins, nested ignore files, and optional case-insensitive matching.

Use it when a tool needs to respect .gitignore: language servers, AI agents, MCP servers, code generators, refactoring tools, file watchers, static site generators, and anything else that reads a source tree.

Why not glob?

.gitignore is not a plain glob format. It is an ordered rule language with negation, directory-only rules, nested files, and Git's wildmatch behavior.

FeaturePath.wildcard/2 / globGitignore
Negation with !NoYes
Last matching rule winsNoYes
Directory-only rules like build/NoYes
Anchoring like /tmp or src/*.beamDifferent languageGit-compatible
Nested .gitignore filesNoYes
Parent-exclusion behaviorNoYes
Git wildmatch ** semanticsDifferent languageVerified against Git

This matters for tooling. A matcher that treats .gitignore as "just glob" can index ignored build artifacts, miss generated files, or accidentally read paths that Git itself would ignore.

Installation

Add gitignore to your list of dependencies in mix.exs:

def deps do
[
{:gitignore, "~> 0.1.0"}
]
end

Or depend on the Git repository:

{:gitignore, git: "https://github.com/ivan-podgurskiy/gitignore.git"}

Quick start

rules = Gitignore.parse("""
_build/
*.beam
!important.beam
""")
matcher = Gitignore.compile(rules)
Gitignore.ignored?(matcher, "_build/dev/lib", type: :directory)
#=> true
Gitignore.ignored?(matcher, "important.beam", type: :file)
#=> false

Use check/3 when you need the rule that made the decision:

Gitignore.check(matcher, "other.beam", type: :file)
#=> {:ignored, %Gitignore.Rule{source: "*.beam", line: 2}}
Gitignore.check(matcher, "important.beam", type: :file)
#=> {:unignored, %Gitignore.Rule{source: "!important.beam", line: 3}}

Nested ignore files

Use Gitignore.Stack when rules come from nested .gitignore files. Deeper ignore files have stronger priority, matching Git's behavior.

stack =
Gitignore.Stack.new()
|> Gitignore.Stack.push(".", Gitignore.parse("*.beam\n"))
|> Gitignore.Stack.push("apps/web", Gitignore.parse("!important.beam\n"))
Gitignore.Stack.ignored?(stack, "apps/api/important.beam", type: :file)
#=> true
Gitignore.Stack.ignored?(stack, "apps/web/important.beam", type: :file)
#=> false

For convenience, Gitignore.load/2 can load .gitignore files from disk:

{:ok, stack} = Gitignore.load(".", recursive: true)
Gitignore.Stack.ignored?(stack, "_build/dev/lib/app/ebin/app.beam", type: :file)

The core matcher does not walk your project tree or enumerate files. It answers whether a path you provide is ignored. The Gitignore.load/2 helper is the only API that reads from disk, and it only loads .gitignore files.

Semantics

Some examples that often break naive matchers:

# Parent exclusion: keep.txt is still ignored because build/ is ignored.
build/
!build/keep.txt
# Directory-only rule: matches a directory named cache at any depth.
cache/
# Anchored path rule: matches tmp only beside this .gitignore file.
/tmp
# Globstar follows Git wildmatch rules.
**/logs
a/**/b

What is not included

Version 0.1 focuses on Git-compatible .gitignore matching. It does not include a filesystem walker, .dockerignore or .npmignore dialects, global Git excludes, or $GIT_DIR/info/exclude helpers. Callers can load those rule files themselves and push them into a Gitignore.Stack.

Verification

Two fixture suites pin the library to real git behavior:

Development

mix test
mix credo --strict
mix dialyzer
mix docs

License

MIT (c) Ivan Podgurskiy. See LICENSE.