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
Use openGitRepository when the working-tree root is already known:
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.
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.
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.
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.
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.
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:
$ 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.
$ ignore -p generated/output.log
Found repository at: /path/to/project
Path 'generated/output.log' IS ignored
$ echo $?
0