# vcs-ignore
[](https://github.com/xwinus/vcs-ignore/actions/workflows/ci.yml)
[](https://hackage.haskell.org/package/vcs-ignore)
[](https://www.stackage.org/package/vcs-ignore)
`vcs-ignore` is a Haskell library for querying Git ignore rules and traversing
the visible part of a working tree. It uses one lazy repository session for
both operations: opening a repository never scans its working tree, and
per-directory `.gitignore` files are loaded only when a visible branch needs
them.
## Contents
- [Opening a repository](#opening-a-repository)
- [Checking a path](#checking-a-path)
- [Listing visible entries](#listing-visible-entries)
- [Processing without building a list](#processing-without-building-a-list)
- [Pruning and stopping early](#pruning-and-stopping-early)
- [Traversal and cache semantics](#traversal-and-cache-semantics)
- [Migrating from 0.0.x](#migrating-from-00x)
- [Using the executable](#using-the-executable)
## Opening a repository
Use `openGitRepository` when the working-tree root is already known:
```haskell
import Data.VCS.Ignore
openProject :: IO GitRepository
openProject = openGitRepository "/path/to/project"
```
Use `findGitRepository` to search from a file or directory towards its
parents. It returns `Nothing` when no enclosing Git worktree exists.
```haskell
import Data.VCS.Ignore
findProject :: IO (Maybe GitRepository)
findProject = findGitRepository "/path/to/project/src/Main.hs"
```
Both regular `.git` directories and gitfiles used by linked worktrees and
submodules are supported.
## Checking a path
Queries take a repository-relative path and an explicit `PathKind`. The path
does not need to exist, and the library never guesses its kind from the
filesystem or a trailing slash.
```haskell
import Data.VCS.Ignore
checkBuildDirectory :: IO Bool
checkBuildDirectory = do
repo <- openGitRepository "/path/to/project"
isIgnored repo Directory "dist"
checkGeneratedFile :: IO Bool
checkGeneratedFile = do
repo <- openGitRepository "/path/to/project"
isIgnored repo RegularFile "generated/output.log"
```
Absolute paths and paths containing a `..` component are rejected. `.` denotes
the repository root and is never ignored.
## Listing visible entries
`listRepo` returns non-ignored files and directories. Each path is relative to
the repository root, and the root itself is not included.
```haskell
import Data.VCS.Ignore
listVisibleFiles :: IO [FilePath]
listVisibleFiles = do
repo <- openGitRepository "/path/to/project"
entries <- listRepo repo
pure
[ entryPath entry
| entry <- entries
, entryKind entry == RegularFile
]
```
`listRepo` intentionally materializes its result. Use `forRepo_` or `foldRepo`
for large repositories when a complete list is unnecessary.
## Processing without building a list
`forRepo_` performs an action for every non-ignored entry without accumulating
the results.
```haskell
import Data.VCS.Ignore
import System.FilePath ((</>))
printVisiblePaths :: IO ()
printVisiblePaths = do
repo <- openGitRepository "/path/to/project"
forRepo_ repo $ \entry ->
putStrLn (repositoryRoot repo </> entryPath entry)
```
The callback receives the kind already discovered by the walker. Directory
symlinks are reported as `SymbolicLink` and are never followed.
## Pruning and stopping early
`foldRepo` is the underlying traversal primitive. `Prune` skips a visible
directory for caller-specific reasons, while `Stop` terminates the entire
traversal immediately.
```haskell
import Data.VCS.Ignore
import System.FilePath (takeFileName)
countAtMost :: Int -> GitRepository -> IO (WalkResult Int)
countAtMost limit repo =
foldRepo repo 0 $ \count entry ->
if entryKind entry == Directory
&& takeFileName (entryPath entry) == ".cache"
then pure (count, Prune)
else
if count >= limit
then pure (count, Stop)
else pure (count + 1, Continue)
```
Traversal is depth-first and pre-order. Sibling order is intentionally
unspecified. `Prune` on a non-directory is equivalent to `Continue`.
## Traversal and cache semantics
- Ignored entries are not passed to callbacks.
- Ignored directories are never opened or scanned.
- Git metadata belonging to the opened repository is never emitted.
- The default XDG global ignore file and `info/exclude` are loaded when the
repository is opened.
- A visible directory's `.gitignore` is loaded on first use and at most once
per repository session.
- A nested `.gitignore` is never loaded below an ignored or caller-pruned
directory.
- Later rule changes become visible after opening a new `GitRepository`.
- Missing, symlinked, and non-regular `.gitignore` files are treated as empty.
Other I/O failures are reported instead of being silently ignored.
These rules ensure that individual queries and traversal use the same Git
precedence and ignored-parent behavior.
The current matcher intentionally does not evaluate Git configuration. In
particular, `core.excludesFile`, `core.ignoreCase`, conditional config includes,
and index-backed ignore files in sparse checkouts are not supported. It reads
the default `$XDG_CONFIG_HOME/git/ignore` (or its platform equivalent) and uses
case-sensitive matching. Paths use Haskell `FilePath`; exact round-tripping of
arbitrary byte-string filenames is not guaranteed.
## Migrating from 0.0.x
Version `0.1.0.0` replaces the eager and lazy APIs with one repository session:
| Before | Now |
|---|---|
| `scanRepo @Git root` | `openGitRepository root` |
| `openGitIgnoreMatcher root` | `openGitRepository root` |
| `isIgnored repo path` | `isIgnored repo kind path` |
| `isIgnoredPath matcher kind path` | `isIgnored repo kind path` |
| `repoRoot repo` | `repositoryRoot repo` |
| `listRepo :: IO [FilePath]` | `listRepo :: IO [Entry]` |
| list-returning callback traversal | `foldRepo`, `walkRepo`, or `forRepo_` |
The `Repo` type class, eager `Git` value, `scanRepo`, and public pattern and
filesystem helpers have been removed. Paths returned in `Entry` remain
repository-relative.
## Using the executable
The package includes an executable named `ignore`. Run it from inside a Git
working tree to check a path:
```console
$ ignore --help
vcs-ignore, v0.1.0.0 :: https://github.com/xwinus/vcs-ignore
Usage: ignore (-p|--path PATH) [--debug] [-v|--version] [--numeric-version]
```
The command exits with status `0` when the path is ignored and status `1` when
it is visible or no repository is found.
```console
$ ignore -p generated/output.log
Found repository at: /path/to/project
Path 'generated/output.log' IS ignored
$ echo $?
0
```