diff --git a/default.nix b/default.nix index 452a454..3dbc874 100644 --- a/default.nix +++ b/default.nix @@ -4,118 +4,10 @@ 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)) - ]; + lib = (import "${(import ./inputs.nix {}).nixpkgs}/lib").extend (self: super: { + proot = import ./lib/gitignore-filter.nix { lib = self; }; + inherit (self.proot) parseGitignore runGitignoreFilter toGitignoreMatcher; + }); currentFilePath = (builtins.unsafeGetAttrPos "any" { any = "any"; }).file; storePathLength = builtins.stringLength (builtins.toString builtins.storeDir); @@ -124,11 +16,11 @@ let selfInStore = builtins.filterSource (path: type: let selfPath = builtins.dirOf currentFilePath; - gitIgnoreFilters = parseGitignore selfPath path; + gitIgnoreFilters = lib.parseGitignore selfPath path; result = type != "unknown" && type != "symlink" && builtins.baseNameOf path != ".git" - && runGitignoreFilter gitIgnoreFilters path type; + && lib.runGitignoreFilter gitIgnoreFilters path type; in result ) ./.; in diff --git a/lib/gitignore-filter.nix b/lib/gitignore-filter.nix new file mode 100644 index 0000000..42c8740 --- /dev/null +++ b/lib/gitignore-filter.nix @@ -0,0 +1,112 @@ +{ 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 (lib.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)) + ]; +}