{outputs; lib/gitignore-filer}: factor out gitignore filtering
This commit is contained in:
parent
285eed58c4
commit
a8da791356
2 changed files with 118 additions and 114 deletions
120
default.nix
120
default.nix
|
@ -4,118 +4,10 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
# Ideally this file should be selfcontained, but I like the utilities in nixpkgs lib
|
# Ideally this file should be selfcontained, but I like the utilities in nixpkgs lib
|
||||||
lib = (import "${(import ./inputs.nix {}).nixpkgs}/lib");
|
lib = (import "${(import ./inputs.nix {}).nixpkgs}/lib").extend (self: super: {
|
||||||
|
proot = import ./lib/gitignore-filter.nix { lib = self; };
|
||||||
# function that takes gitignore file pattern and returns filter function
|
inherit (self.proot) parseGitignore runGitignoreFilter toGitignoreMatcher;
|
||||||
# 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 .. <directory-up> 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;
|
currentFilePath = (builtins.unsafeGetAttrPos "any" { any = "any"; }).file;
|
||||||
storePathLength = builtins.stringLength (builtins.toString builtins.storeDir);
|
storePathLength = builtins.stringLength (builtins.toString builtins.storeDir);
|
||||||
|
@ -124,11 +16,11 @@ let
|
||||||
selfInStore = builtins.filterSource (path: type:
|
selfInStore = builtins.filterSource (path: type:
|
||||||
let
|
let
|
||||||
selfPath = builtins.dirOf currentFilePath;
|
selfPath = builtins.dirOf currentFilePath;
|
||||||
gitIgnoreFilters = parseGitignore selfPath path;
|
gitIgnoreFilters = lib.parseGitignore selfPath path;
|
||||||
result = type != "unknown"
|
result = type != "unknown"
|
||||||
&& type != "symlink"
|
&& type != "symlink"
|
||||||
&& builtins.baseNameOf path != ".git"
|
&& builtins.baseNameOf path != ".git"
|
||||||
&& runGitignoreFilter gitIgnoreFilters path type;
|
&& lib.runGitignoreFilter gitIgnoreFilters path type;
|
||||||
in result
|
in result
|
||||||
) ./.;
|
) ./.;
|
||||||
in
|
in
|
||||||
|
|
112
lib/gitignore-filter.nix
Normal file
112
lib/gitignore-filter.nix
Normal file
|
@ -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 .. <directory-up> 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))
|
||||||
|
];
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue