# if evaluating outside of the store, copy the current directory to the store and import it # filtering out .gitignore files and .git directories # if evaluating inside the store, import the outputs.nix file let # Ideally this file should be selfcontained, but I like the utilities in nixpkgs lib lib = (import "${(import ./inputs.nix {}).nixpkgs}/lib"); # function that takes gitignore file pattern and returns filter function # true - include file # false - exclude file # null - no match # string -> string -> [(string -> string -> (bool | null))] toGitignoreMatcher = gitignorePath: pattern: lib.pipe pattern [ (v: { pattern = v; invalid = false; }) # trim whitespaces not preceded by backslash (v: v // { pattern = let stringLength = builtins.stringLength v.pattern; leftPaddingLength = builtins.stringLength (lib.trimWith { start = true; end = false; } v.pattern) - stringLength; rightPaddingLength = builtins.stringLength (lib.trimWith { start = false; end = true; } v.pattern) - stringLength; isLastCharBackslash = if stringLength == 0 then false else builtins.substring (stringLength - rightPaddingLength - 1) 1 v.pattern == "\\"; trimmedString = builtins.substring leftPaddingLength (stringLength - leftPaddingLength - rightPaddingLength) v.pattern; in if isLastCharBackslash && rightPaddingLength > 0 then trimmedString + " " else trimmedString; }) # ignore empty lines (v: if v.pattern != "" then v else v // { invalid = true; }) # ignore comments (v: if !v.invalid && builtins.substring 0 1 v.pattern != "#" then v else v // { invalid = true; }) # mark negated patterns (v: if !v.invalid && builtins.substring 0 1 v.pattern == "!" then v // { negated = true; pattern = builtins.substring 1 (builtins.stringLength v) v; } else v // { negated = false; } ) # ignore escapes (v: if v.invalid then v else v // { pattern = builtins.replaceStrings ["\\"] [""] v.pattern; }) # convert parsed pattern to matchers ({ pattern, negated, invalid }: { __functor = _: path: type: let relative = builtins.match "^/.+[^/]$" pattern == []; directory = builtins.match "/$" pattern == []; regexPattern = lib.pipe pattern [ (v: if relative then "${gitignorePath}/${v}" else v) (builtins.split "/") (builtins.filter (v: v != [])) (builtins.map (builtins.split "(\\*\\*|\\*)")) (builtins.concatMap (v: # v: (string | [string])[] if v == [ "" ] then [] # TODO: check and add support for .. if git supports else if v == [ "." ] then [] else [( builtins.foldl' (acc: vp: # vp: string | [string] if builtins.isString vp then acc + lib.escapeRegex vp else if vp == [ "**" ] then acc + ".*" else if vp == [ "*" ] then acc + "[^/]*" else throw "unreachable" ) "" v )] )) (builtins.concatStringsSep "/" ) (v: if relative then v else ".*/${v}") ]; matches = (!directory || type == "directory") && (builtins.match regexPattern path == []); in if invalid then null else if matches then negated else null; # for debug purposes inherit pattern negated; # for filtering purposes inherit invalid; }) ]; # TODO: optimize this so if match is found in a given gitignore, # no further checks in gitignores in parent directories are performed parseGitignore = gitRepositoryPath: filePath: lib.pipe filePath [ (builtins.dirOf) (builtins.split "/" ) (builtins.filter (v: v != [] && v != "")) # ["a" "b" "c"] -> ["/" "/a/" "/a/b/" "/a/b/c/"] ( builtins.foldl' (acc: v: acc ++ [( (builtins.elemAt acc (builtins.length acc - 1)) + "${v}/" )] ) ["/"] ) (builtins.map (v: "${v}.gitignore")) # Filter out paths that are not part of git repository and don't exist (builtins.filter (v: lib.hasPrefix gitRepositoryPath v && builtins.pathExists v)) (builtins.map (v: { path = v; # Split gitignore files into lines contents = lib.pipe v [ builtins.readFile (builtins.split "\n") # builtins.split uses lists for matches (builtins.filter (v: v != [])) ]; })) # Convert gitignore patterns to matchers (builtins.map (v: builtins.map (toGitignoreMatcher v.path) v.contents) ) lib.flatten (lib.filter (v: !v.invalid)) ]; runGitignoreFilter = filters: path: type: lib.pipe filters [ (builtins.map (v: v path type)) (builtins.filter (v: v != null)) # If any filter didn't match anything, include the file (v: if v == [] then [ true ] else v) (v: builtins.elemAt v (builtins.length v - 1)) ]; currentFilePath = (builtins.unsafeGetAttrPos "any" { any = "any"; }).file; storePathLength = builtins.stringLength (builtins.toString builtins.storeDir); evaluatingInStore = (builtins.substring 0 storePathLength currentFilePath) == builtins.storeDir; selfInStore = builtins.filterSource (path: type: let selfPath = builtins.dirOf currentFilePath; gitIgnoreFilters = parseGitignore selfPath path; result = type != "unknown" && type != "symlink" && builtins.baseNameOf path != ".git" && runGitignoreFilter gitIgnoreFilters path type; in result ) ./.; in if !(evaluatingInStore) then { ... }@args: import selfInStore ({ selfPath = selfInStore; } // args ) else { ... }@args: import ./outputs.nix ({ selfPath = selfInStore; } // args)