# Coding Python Gitignore

> Use when parsing .gitignore files, deciding whether paths are ignored, or filtering/whitelisting large numbers of files in .gitignore style (including as a shutil.copytree filter or a `find | ...` pipeline). Use this whenever you would otherwise hand-roll fnmatch/glob/regex gitignore matching, or reach for pathspec or gitignore_parser - prefer the `igittigitt` library/CLI, which is 100% git-compatible, memory-bounded for millions of paths, and ships an include/whitelist mode and a streaming CLI. Covers install, config (deploy + config.d overrides), library API, the CLI, and bash piping.

- Skill: `bitranox/coding-python-gitignore` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add bitranox/coding-python-gitignore`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bitranox/coding-python-gitignore/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: bitranox (https://skillmd.com/u/bitranox)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bitranox/coding-python-gitignore

---


# igittigitt - git-compatible .gitignore parsing and file filtering

`igittigitt` is a spec-compliant `.gitignore` parser for Python - a **library and a CLI**.
Use it whenever you need to know "would git ignore this path?" or to filter / whitelist a
large set of files using `.gitignore`-style patterns.

## When to reach for this (and what to avoid)

Reach for `igittigitt` instead of writing your own matcher. Getting `.gitignore` semantics
right is deceptively hard - negations (`!pattern`), the rule that a file under an excluded
directory cannot be re-included, per-directory precedence, anchoring, `**`, trailing-slash
directory-only patterns, character classes. A quick `fnmatch`/`glob`/`re` solution will be
subtly wrong on real repositories.

| Need                                 | Use                                                    | Avoid                                                                                  |
|--------------------------------------|--------------------------------------------------------|----------------------------------------------------------------------------------------|
| Is this path ignored (git-exact)?    | `igittigitt.IgnoreParser`                              | hand-rolled `fnmatch`/`glob`/`re`; `gitignore_parser` (incomplete negation/precedence) |
| Copy a tree minus ignored files      | `IgnoreParser.shutil_ignore` as `copytree(ignore=...)` | manual `os.walk` + skip lists                                                          |
| Keep only matching files (whitelist) | `igittigitt.IncludeParser` (directory-aware)           | inverting a matcher by hand (breaks on parent dirs)                                    |
| Filter a huge `find` stream          | `igittigitt filter` (CLI, streaming)                   | loading all paths into memory; `pathspec` (no include mode, heavier for this)          |

Why igittigitt specifically: it is validated against real `git check-ignore` (differential
tests), it is **memory-bounded** (per-path cost `O(depth x rules)`; memory scales with the
number of rules, not the number of files), and it has an include/whitelist mode plus a
script-friendly CLI. It is pure Python with no runtime dependency on the `git` binary.

## Install

```bash
# as a project dependency (preferred)
uv add igittigitt

# as a standalone CLI tool on PATH
uv tool install igittigitt

# run the CLI once without installing
uvx igittigitt --help

# or pip / pipx
pip install igittigitt
pipx install igittigitt
```

Requires Python 3.10+.

## Library usage

### Ignore mode (the common case)

```python
import igittigitt

parser = igittigitt.IgnoreParser()

# discover and parse every .gitignore under base_dir (correct per-directory precedence)
parser.parse_rule_files(base_dir="/path/to/project")

# or add rules explicitly (last matching rule wins, exactly like git)
parser.add_rule("*.py[cod]", base_path="/path/to/project")
parser.add_rule("!keep.pyc", base_path="/path/to/project")

parser.match("/path/to/project/main.pyc")  # True  -> ignored
parser.match("/path/to/project/keep.pyc")  # False -> re-included by the negation
```

Key methods on `IgnoreParser`:
- `parse_rule_files(base_dir, filename=".gitignore", add_default_patterns=...)` - recursively
  find and parse ignore files.
- `parse_rule_file(rule_file, base_dir=None)` - parse one file.
- `add_rule(pattern, base_path)` - add a single rule string.
- `match(path) -> bool` - True if the path is ignored.
- `match_with_rule(path) -> (bool, rule|None)` - also returns the deciding rule.
- `shutil_ignore(base_dir, names) -> set[str]` - drop-in `ignore=` callback for `copytree`.

### Copy a tree, skipping ignored files

```python
import shutil, igittigitt

parser = igittigitt.IgnoreParser()
parser.parse_rule_files(base_dir="src_tree")
shutil.copytree("src_tree", "dst_tree", ignore=parser.shutil_ignore)
```

### Include / whitelist mode (the inverse)

Patterns describe what to **keep**; everything else is dropped. It is directory-aware
(rsync-like), so including a deep file keeps its parent directories - which means it works
correctly as a `copytree` filter. Do NOT try to build this by negating `match()`; a naive
inversion prunes the parent directories and never reaches the file.

```python
import shutil, igittigitt

inc = igittigitt.IncludeParser()
inc.add_rule("*.py", base_path="src_tree")  # keep python files anywhere
inc.add_rule("docs/", base_path="src_tree")  # keep the whole docs/ subtree

shutil.copytree("src_tree", "dst_tree", ignore=inc.shutil_include)
inc.match("src_tree/pkg/deep.py")  # True -> kept
```

## CLI usage

```bash
# which of these paths are ignored? (mirrors `git check-ignore`)
igittigitt check -C /path/to/repo a.log src/main.py      # exit 0 if any ignored, 1 if none

# show the matching source:line:pattern (like `git check-ignore -v`)
igittigitt check -C /path/to/repo -v a.log

# inline rules and explicit ignore files, reading paths from stdin
igittigitt check -r '*.tmp' -f .gitignore --stdin < paths.txt

igittigitt info        # package metadata
igittigitt --version
```

Other commands: `config` (show merged config), `config-deploy` (write editable config files),
`config-generate-examples`, `logdemo`. Global options: `--profile`, `--set SECTION.KEY=VALUE`,
`--env-file`, `--traceback`.

## Bash usage (piping)

`filter` is a Unix filter: it reads paths from stdin and prints the survivors, streaming so
it stays memory-bounded even for millions of paths, with clean `SIGPIPE` handling.

```bash
# drop paths ignored by ./.gitignore
find . -type f | igittigitt filter

# keep only what matches the include patterns (whitelist mode)
find . -type f | igittigitt filter --include -r '*.py'

# NUL-separated I/O for paths with spaces or newlines
find . -print0 | igittigitt filter -z | xargs -0 tar czf out.tgz

# stops cleanly when the reader closes the pipe (no traceback)
find . -type f | igittigitt filter | head -n 20
```

Shared `check`/`filter` options: `-C/--base-dir`, `-f/--gitignore FILE` (repeatable),
`-r/--rule PATTERN` (repeatable), `--scan/--no-scan`, `--default-patterns`, `-z/--zero`.

## Configuration (lib_layered_config)

Settings merge across layers, later layers winning:

```
bundled defaults -> app -> host -> user -> .env -> environment variables -> --set
```

- Inspect the effective config (with provenance): `igittigitt config`
- Deploy editable copies into the standard directories: `igittigitt config-deploy --target user`
  (`--target` is required: `app`, `host` or `user`)
- Generate example files to copy: `igittigitt config-generate-examples --destination ./examples`
  (`--destination` is required)

### Adding your own config (config.d drop-in)

The config directories are scanned for `config.d/*.toml` fragments, merged in filename order.
To override a setting system-wide on Linux, drop a file into the app layer's `config.d`:

```bash
sudo mkdir -p /etc/xdg/igittigitt/config.d
sudoedit /etc/xdg/igittigitt/config.d/50-myperf.toml
```

```toml
# /etc/xdg/igittigitt/config.d/50-myperf.toml
[performance]
dir_cache_max = 32768   # larger directory cache for very deep trees
```

Per-user, use `~/.config/igittigitt/config.d/50-myperf.toml` instead. The same value can be
set ad hoc with `igittigitt --set performance.dir_cache_max=32768 ...`, in a `.env` file as
`PERFORMANCE__DIR_CACHE_MAX=32768`, or as the env var
`IGITTIGITT___PERFORMANCE__DIR_CACHE_MAX=32768`.

### `[performance]` knobs (speed/memory only - never change matching)

Defaults below are igittigitt 2.2.3 (checked 2026-08-28). Confirm the one you care about
against your own install rather than this table:
`python -c "import igittigitt, inspect; print(inspect.signature(igittigitt.IgnoreParser.__init__))"`

| Key                 | Default   | Meaning                                                                                                               |
|---------------------|-----------|-----------------------------------------------------------------------------------------------------------------------|
| `dir_cache_max`     | `8192`    | Per-parser directory-decision LRU capacity (`0` disables). Main speed-up on trees; memory `O(this)`, not `O(#files)`. |
| `pattern_cache_max` | `4096`    | Process-wide compiled-regex cache capacity.                                                                           |
| `stdin_chunk_bytes` | `65536`   | Stdin read granularity for the streaming commands.                                                                    |
| `max_token_bytes`   | `1048576` | Per-path safety bound; a separator-less token larger than this is rejected rather than buffered unbounded.            |

Logging is configured under `[lib_log_rich]` (console level/theme, journald/eventlog/Graylog,
queueing, scrubbing); every key is documented inline in the deployed `config.d/90-logging.toml`,
e.g. `igittigitt --set lib_log_rich.console_level=DEBUG info`.

## Default patterns

`parse_rule_files(add_default_patterns=True)` (the default) also loads git's user-level
default ignore file (`$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore`). Pass
`add_default_patterns=False` to match only the in-tree `.gitignore` files.

