python312Packages.powerfox: init at 1.1.0 (#371207)
[NixPkgs.git] / pkgs / build-support / nix-gitignore / default.nix
blob60a854225e10558dcaadf7e425c2a1cd6b6492b6
1 # https://github.com/siers/nix-gitignore/
3 { lib, runCommand }:
5 # An interesting bit from the gitignore(5):
6 # - A slash followed by two consecutive asterisks then a slash matches
7 # - zero or more directories. For example, "a/**/b" matches "a/b",
8 # - "a/x/b", "a/x/y/b" and so on.
10 let
11   inherit (builtins) filterSource;
13   inherit (lib)
14     concatStringsSep
15     elemAt
16     filter
17     head
18     isList
19     length
20     optionals
21     optionalString
22     pathExists
23     readFile
24     removePrefix
25     replaceStrings
26     stringLength
27     sub
28     substring
29     toList
30     trace
31     ;
33   inherit (lib.strings) match split typeOf;
35   debug = a: trace a a;
36   last = l: elemAt l ((length l) - 1);
38 rec {
39   # [["good/relative/source/file" true] ["bad.tmpfile" false]] -> root -> path
40   filterPattern =
41     patterns: root:
42     (
43       name: _type:
44       let
45         relPath = removePrefix ((toString root) + "/") name;
46         matches = pair: (match (head pair) relPath) != null;
47         matched = map (pair: [
48           (matches pair)
49           (last pair)
50         ]) patterns;
51       in
52       last (
53         last (
54           [
55             [
56               true
57               true
58             ]
59           ]
60           ++ (filter head matched)
61         )
62       )
63     );
65   # string -> [[regex bool]]
66   gitignoreToPatterns =
67     gitignore:
68     let
69       # ignore -> bool
70       isComment = i: (match "^(#.*|$)" i) != null;
72       # ignore -> [ignore bool]
73       computeNegation =
74         l:
75         let
76           split = match "^(!?)(.*)" l;
77         in
78         [
79           (elemAt split 1)
80           (head split == "!")
81         ];
83       # regex -> regex
84       handleHashesBangs = replaceStrings [ "\\#" "\\!" ] [ "#" "!" ];
86       # ignore -> regex
87       substWildcards =
88         let
89           special = "^$.+{}()";
90           escs = "\\*?";
91           splitString =
92             let
93               recurse =
94                 str:
95                 [ (substring 0 1 str) ] ++ (optionals (str != "") (recurse (substring 1 (stringLength (str)) str)));
96             in
97             str: recurse str;
98           chars = s: filter (c: c != "" && !isList c) (splitString s);
99           escape = s: map (c: "\\" + c) (chars s);
100         in
101         replaceStrings
102           (
103             (chars special)
104             ++ (escape escs)
105             ++ [
106               "**/"
107               "**"
108               "*"
109               "?"
110             ]
111           )
112           (
113             (escape special)
114             ++ (escape escs)
115             ++ [
116               "(.*/)?"
117               ".*"
118               "[^/]*"
119               "[^/]"
120             ]
121           );
123       # (regex -> regex) -> regex -> regex
124       mapAroundCharclass =
125         f: r: # rl = regex or list
126         let
127           slightFix = replaceStrings [ "\\]" ] [ "]" ];
128         in
129         concatStringsSep "" (
130           map (rl: if isList rl then slightFix (elemAt rl 0) else f rl) (split "(\\[([^\\\\]|\\\\.)+])" r)
131         );
133       # regex -> regex
134       handleSlashPrefix =
135         l:
136         let
137           split = (match "^(/?)(.*)" l);
138           findSlash = l: optionalString ((match ".+/.+" l) == null) l;
139           hasSlash = mapAroundCharclass findSlash l != l;
140         in
141         (if (elemAt split 0) == "/" || hasSlash then "^" else "(^|.*/)") + (elemAt split 1);
143       # regex -> regex
144       handleSlashSuffix =
145         l:
146         let
147           split = (match "^(.*)/$" l);
148         in
149         if split != null then (elemAt split 0) + "($|/.*)" else l;
151       # (regex -> regex) -> [regex, bool] -> [regex, bool]
152       mapPat = f: l: [
153         (f (head l))
154         (last l)
155       ];
156     in
157     map (
158       l: # `l' for "line"
159       mapPat (
160         l: handleSlashSuffix (handleSlashPrefix (handleHashesBangs (mapAroundCharclass substWildcards l)))
161       ) (computeNegation l)
162     ) (filter (l: !isList l && !isComment l) (split "\n" gitignore));
164   gitignoreFilter = ign: root: filterPattern (gitignoreToPatterns ign) root;
166   # string|[string|file] (→ [string|file] → [string]) -> string
167   gitignoreCompileIgnore =
168     file_str_patterns: root:
169     let
170       onPath = f: a: if typeOf a == "path" then f a else a;
171       str_patterns = map (onPath readFile) (toList file_str_patterns);
172     in
173     concatStringsSep "\n" str_patterns;
175   gitignoreFilterPure =
176     predicate: patterns: root: name: type:
177     gitignoreFilter (gitignoreCompileIgnore patterns root) root name type && predicate name type;
179   # This is a very hacky way of programming this!
180   # A better way would be to reuse existing filtering by making multiple gitignore functions per each root.
181   # Then for each file find the set of roots with gitignores (and functions).
182   # This would make gitignoreFilterSource very different from gitignoreFilterPure.
183   # rootPath → gitignoresConcatenated
184   compileRecursiveGitignore =
185     root:
186     let
187       dirOrIgnore = file: type: baseNameOf file == ".gitignore" || type == "directory";
188       ignores = builtins.filterSource dirOrIgnore root;
189     in
190     readFile (
191       runCommand "${baseNameOf root}-recursive-gitignore" { } ''
192         cd ${ignores}
194         find -type f -exec sh -c '
195           rel="$(realpath --relative-to=. "$(dirname "$1")")/"
196           if [ "$rel" = "./" ]; then rel=""; fi
198           awk -v prefix="$rel" -v root="$1" -v top="$(test -z "$rel" && echo 1)" "
199             BEGIN { print \"# \"root }
201             /^!?[^\\/]+\/?$/ {
202               match(\$0, /^!?/, negation)
203               sub(/^!?/, \"\")
205               if (top) { middle = \"\" } else { middle = \"**/\" }
207               print negation[0] prefix middle \$0
208             }
210             /^!?(\\/|.*\\/.+$)/ {
211               match(\$0, /^!?/, negation)
212               sub(/^!?/, \"\")
214               if (!top) sub(/^\//, \"\")
216               print negation[0] prefix \$0
217             }
219             END { print \"\" }
220           " "$1"
221         ' sh {} \; > $out
222       ''
223     );
225   withGitignoreFile = patterns: root: toList patterns ++ [ ".git" ] ++ [ (root + "/.gitignore") ];
227   withRecursiveGitignoreFile =
228     patterns: root: toList patterns ++ [ ".git" ] ++ [ (compileRecursiveGitignore root) ];
230   # filterSource derivatives
232   gitignoreFilterSourcePure =
233     predicate: patterns: root:
234     filterSource (gitignoreFilterPure predicate patterns root) root;
236   gitignoreFilterSource =
237     predicate: patterns: root:
238     gitignoreFilterSourcePure predicate (withGitignoreFile patterns root) root;
240   gitignoreFilterRecursiveSource =
241     predicate: patterns: root:
242     gitignoreFilterSourcePure predicate (withRecursiveGitignoreFile patterns root) root;
244   # "Predicate"-less alternatives
246   gitignoreSourcePure = gitignoreFilterSourcePure (_: _: true);
247   gitignoreSource =
248     patterns:
249     let
250       type = typeOf patterns;
251     in
252     if (type == "string" && pathExists patterns) || type == "path" then
253       throw "type error in gitignoreSource(patterns -> source -> path), " "use [] or \"\" if there are no additional patterns"
254     else
255       gitignoreFilterSource (_: _: true) patterns;
257   gitignoreRecursiveSource = gitignoreFilterSourcePure (_: _: true);