diff --git a/find-files.nix b/find-files.nix index 5d7c560..e52fc06 100644 --- a/find-files.nix +++ b/find-files.nix @@ -2,6 +2,7 @@ let parse-ini = import ./parse-git-config.nix { inherit lib; }; parse-gitignore = import ./rules.nix { inherit lib; }; + path = import ./lib/path.nix { inherit lib; }; in rec { inherit (builtins) dirOf baseNameOf abort split hasAttr readFile readDir pathExists; @@ -27,14 +28,20 @@ rec { lib.optional (extraRules != null) { contextDir = basePath; rules = extraRules; }; patternsBelowP = findPatternsTree extraRules2 basePath; basePathStr = toString basePath; - in - path: type: let - localDirPath = removePrefix basePathStr (toString (dirOf path)); - localDirPathElements = splitString "/" localDirPath; - patternResult = parse-gitignore.runFilterPattern (getPatterns patternsBelowP localDirPathElements)."/patterns" path type; - nonempty = any (nodeName: gitignoreFilter (basePath + "/${nodeName}") != false) - (attrNames (readDir path)); - in patternResult && (type == "directory" -> nonempty); + rawFilter = + path: type: let + localDirPath = removePrefix basePathStr (toString (dirOf path)); + localDirPathElements = splitString "/" localDirPath; + patternResult = parse-gitignore.runFilterPattern (getPatterns patternsBelowP localDirPathElements)."/patterns" path type; + contents = readDir path; + nonempty = localDirPathElements == [] || any (nodeName: memoFilter (path + "/${nodeName}") != false) + (attrNames contents); + in patternResult && (type == "directory" -> nonempty); + memoFilter = + path.memoize rawFilter + (p: throw "Could not find path ${toString p} in memo table. Did path or filtering semantics change?") + basePath; + in path: type: memoFilter path; getPatterns = patternTree: pathElems: diff --git a/lib/path.nix b/lib/path.nix new file mode 100644 index 0000000..dcd5646 --- /dev/null +++ b/lib/path.nix @@ -0,0 +1,79 @@ +{ lib }: +let + inherit (lib) filter head mapAttrs tail; + + # absolutePathComponentsBetween : PathOrString -> PathOrString -> [String] + # absolutePathComponentsBetween ancestor descendant + # + # Returns the path components that form the path from ancestor to descendant. + # Will not return ".." components, which is a feature. Throws when ancestor + # and descendant arguments aren't in said relation to each other. + # + # Example: + # + # absolutePathComponentsBetween /a /a/b/c == ["b" "c"] + # absolutePathComponentsBetween /a/b/c /a/b/c == [] + absolutePathComponentsBetween = + ancestor: descendant: + let + a' = /. + ancestor; + go = d: + if a' == d + then [] + else if d == /. + then throw "absolutePathComponentsBetween: path ${toString ancestor} is not an ancestor of ${toString descendant}" + else go (dirOf d) ++ [(baseNameOf d)]; + in + go (/. + descendant); + + /* + Memoize a function that takes a path argument. + Example: + analyzeTree = dir: + let g = memoizePathFunction (p: t: expensiveFunction p t) (p: {}) dir; + in presentExpensiveData g; + Type: + memoizePathFunction :: (Path -> Type -> a) -> (Path -> a) -> Path -> (Path -> a) + */ + memoize = + # Function to memoize + f: + # What to return when a path does not exist, as a function of the path + missing: + # Filesystem location below which the returned function is defined. `/.` may be acceptable, but a path closer to the data of interest is better. + root: + + let + makeTree = dir: type: { + value = f dir type; + inherit type; + children = + if type == "directory" + then mapAttrs + (key: type: makeTree (dir + "/${key}") type) + (builtins.readDir dir) + else {}; + }; + + # This is where the memoization happens + tree = makeTree root ( + # We can't query the type of a store path in Nix without readFileType in + # pure mode, so we assume. + "directory" + ); + + lookup = notFound: list: subtree: + if list == [] + then subtree.value + else if subtree.children ? ${head list} + then lookup notFound (tail list) subtree.children.${head list} + else notFound; + in + path: lookup + (missing path) + (absolutePathComponentsBetween root path) + tree; +in +{ + inherit memoize absolutePathComponentsBetween; +} diff --git a/tests/x.bar b/tests/x.bar new file mode 100644 index 0000000..e69de29 diff --git a/tests/x.foo b/tests/x.foo new file mode 100644 index 0000000..e69de29 diff --git a/tests/x.qux b/tests/x.qux new file mode 100644 index 0000000..e69de29