Manage .gitignore as structured, reviewable configuration instead of a
hand-edited flat file.
Status: pre-alpha. All five surfaces work: import a
.gitignore, keep it as TOML, JSON or YAML, generate it back, and reach it from the CLI, a browser, an MCP client or an editor. The format is not stable yet. See docs/ROADMAP.md.
A .gitignore accumulates. Sections drift, rules get duplicated across repos, a
negation lands above the pattern it was meant to override, and nobody can tell
which of forty lines is still load-bearing. The file is append-only in practice
because editing it is risky.
ignorefile treats the ignore rules as data: import an existing
.gitignore into a structured config, review and compose that config like any
other source file, and generate the .gitignore back out.
Import an existing file:
# Cargo
/target
## Mise
/mise.local.toml
!mise.lock
# Always Addand get structured configuration back:
[[cargo]]
ignores = [
"target"
]
[[Mise]]
ignores = [
"/mise.local.toml"
]
add = [
"mise.lock"
]
[["Always Add"]]
add = []The shape above is the original sketch. What the tool writes is an ordered array of sections under the file they describe, with names as values rather than table keys:
version = 1
name = "ignore-as-config"
[[gitignore.section]]
name = "Logs"
note = "Ignore Logs Pattern"
[[gitignore.section.rule]]
ignore = ["*.log"]
[[gitignore.section.rule]]
note = "keep the one we actually read"
add = ["important.log"]which renders to
### ignore-as-config
## Logs
# Ignore Logs Pattern
*.log
# keep the one we actually read
!important.log### names the configuration, ## names a section, # is a note. Every one of
those choices is forced by a measured constraint, not taste: serde_json sorts
object keys by default, so a section name in key position would be silently
alphabetized and rule order is semantic; patterns are stored verbatim so
/target never becomes target; add omits the leading ! so one rule has one
spelling; and comments are fields because JSON has no comment syntax and the
three encodings must be interchangeable.
docs/design/config-format.md records the
evidence.
A fourth field, header, carries a verbatim comment block above the ###
banner -- which is where a licence header goes. This repository's own
ignorefile.toml uses it:
header = """
SPDX-FileCopyrightText: ignorefile contributors
SPDX-License-Identifier: MIT OR Apache-2.0"""Each of its lines renders as its own # line, so an empty line becomes a bare
# and the block reproduces byte for byte. It sits on the configuration rather
than on a section because a licence header is the same for every file generated
from one config.
Note that the lines are not indented to match the surrounding TOML. Every
line is emitted verbatim after a # , so an indent here would render
# SPDX-FileCopyrightText: ..., which no SPDX reader recognises.
Import is lossless or it refuses. Rather than quietly changing which files
git ignores, import re-renders the config it just built and compares it to the
source, reporting the first differing line if they disagree. ignorefile fmt is
the answer when it refuses: it rewrites the file into the one canonical layout
import reproduces, as a diff you can review. Only comments and blank lines ever
move, so the set of paths git ignores is identical either side.
Editors get a schema. Point them at
schema/ignorefile.schema.json; for a
YAML config, a # yaml-language-server: $schema=... comment on the first line
does it. A test holds the schema to the Rust types so it cannot drift.
gitignore is a table rather than an array so that a [dockerignore] sibling
can be added later without disturbing it. That is not implemented yet.
# Import an existing .gitignore, or start an empty config if there is none.
ignorefile init
# Convert a .gitignore into a config (overwrites the config).
ignorefile import
# Add patterns to a section, creating it if needed.
ignorefile add --section Cargo /target /debug
ignorefile add --section Logs --allow --note "keep this one" important.log
# Write the .gitignore back out from the config.
ignorefile generate
# Check the config without writing anything.
ignorefile validate
# Rewrite a .gitignore into the canonical form import accepts.
# --check reports without writing, for CI.
ignorefile fmt
ignorefile fmt --checkEvery command is also available under the short alias ign.
Both paths are configurable, and the encoding follows the config's extension:
ignorefile import --config ignorefile.yaml --gitignore .gitignoreTOML, JSON and YAML (.toml, .json, .yaml, .yml) are interchangeable: a
config written in one can be read and re-emitted as another without loss.
Not yet published. Build from source:
git clone https://github.com/elioseverojunior/ignorefile
cd ignorefile
mise run setup # provisions the pinned toolchain
mise run build:install # release build, both binaries into ~/.local/binThat ships two names for one executable, ignorefile and the short alias
ign; cargo has no first-class alias, so they are two [[bin]] targets over the
same src/main.rs.
To build without installing, use cargo directly:
cargo build --release # -> target/release/ignorefile and target/release/ignmise run build is a debug build of the whole workspace and takes no
--release flag -- passing one is silently ignored, and nothing lands in
target/release/.
One crate per surface, each with its own README:
| Crate | Role | Status |
|---|---|---|
ignorefile |
core library: parse, model, render, match | working |
ignorefile-cli |
the CLI; ships ignorefile and ign |
working |
ignorefile-wasm |
WebAssembly bindings for browser use | working |
ignorefile-mcp |
MCP server, so agents can manage ignore rules | working |
ignorefile-lsp |
Language Server for editor diagnostics | working |
graph TD
core["ignorefile<br/>core library"]
cli["ignorefile-cli<br/>ignorefile, ign"]
wasm["ignorefile-wasm"]
mcp["ignorefile-mcp"]
lsp["ignorefile-lsp"]
cli --> core
wasm --> core
mcp --> core
lsp --> core
All logic lives in the core library; every other crate is a thin surface over
it. That is a rule, not an accident: it is what lets one differential test
against git check-ignore cover the semantics for all of them.
flowchart LR
text[".gitignore"]
model["GitIgnore<br/>line model"]
config["Config<br/>sections and rules"]
file["ignorefile.toml<br/>json / yaml"]
text -->|"import"| model
model --> config
config -->|"encode"| file
file -->|"decode"| config
config --> model
model -->|"generate"| text
Import is lossless or it refuses: the config is re-rendered and compared against the source before it is written.
Everything runs through mise:
mise run setup # install the pinned toolchain and tools
mise run doctor # check what is installed, one row per tool
mise tasks # the full task list
mise run ci:quick # fast gate: fmt + clippy + tests + doctests
mise run pr:ready # auto-format, then check everythingThe project is test-driven, warnings are hard errors, and the coverage gate is 100%. AGENTS.md documents the conventions and the traps in the build configuration; it is worth reading before your first change. docs/RUNBOOK.md covers the operational side: releasing, running each surface, and what to do when a gate goes red.
Please read CONTRIBUTING.md first. In short: a pull request is a long-term maintenance commitment, so what matters is that you understand your change and can support it.
The project has a firm policy on AI-assisted contributions, including a hard prohibition on AI-written pull requests, commit messages and reviewer replies. The full text is in docs/guidelines/CONTRIBUTION.md.
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. The canonical texts live in LICENSES/; the two files
above are symlinks into that directory, which keeps the repository compliant
with the REUSE Specification (mise run comply).
Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in this work shall be dual-licensed as above, without any additional terms or conditions.