Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
1# https://github.com/siers/nix-gitignore/
2
3{ lib, runCommand }:
4
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.
9
10let
11 inherit (builtins) filterSource;
12
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 ;
32
33 inherit (lib.strings) match split typeOf;
34
35 debug = a: trace a a;
36 last = l: elemAt l ((length l) - 1);
37in
38rec {
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 );
64
65 # string -> [[regex bool]]
66 gitignoreToPatterns =
67 gitignore:
68 let
69 # ignore -> bool
70 isComment = i: (match "^(#.*|$)" i) != null;
71
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 ];
82
83 # regex -> regex
84 handleHashesBangs = replaceStrings [ "\\#" "\\!" ] [ "#" "!" ];
85
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 );
122
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 );
132
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);
142
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;
150
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));
163
164 gitignoreFilter = ign: root: filterPattern (gitignoreToPatterns ign) root;
165
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;
174
175 gitignoreFilterPure =
176 predicate: patterns: root: name: type:
177 gitignoreFilter (gitignoreCompileIgnore patterns root) root name type && predicate name type;
178
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}
193
194 find -type f -exec sh -c '
195 rel="$(realpath --relative-to=. "$(dirname "$1")")/"
196 if [ "$rel" = "./" ]; then rel=""; fi
197
198 awk -v prefix="$rel" -v root="$1" -v top="$(test -z "$rel" && echo 1)" "
199 BEGIN { print \"# \"root }
200
201 /^!?[^\\/]+\/?$/ {
202 match(\$0, /^!?/, negation)
203 sub(/^!?/, \"\")
204
205 if (top) { middle = \"\" } else { middle = \"**/\" }
206
207 print negation[0] prefix middle \$0
208 }
209
210 /^!?(\\/|.*\\/.+$)/ {
211 match(\$0, /^!?/, negation)
212 sub(/^!?/, \"\")
213
214 if (!top) sub(/^\//, \"\")
215
216 print negation[0] prefix \$0
217 }
218
219 END { print \"\" }
220 " "$1"
221 ' sh {} \; > $out
222 ''
223 );
224
225 withGitignoreFile = patterns: root: toList patterns ++ [ ".git" ] ++ [ (root + "/.gitignore") ];
226
227 withRecursiveGitignoreFile =
228 patterns: root: toList patterns ++ [ ".git" ] ++ [ (compileRecursiveGitignore root) ];
229
230 # filterSource derivatives
231
232 gitignoreFilterSourcePure =
233 predicate: patterns: root:
234 filterSource (gitignoreFilterPure predicate patterns root) root;
235
236 gitignoreFilterSource =
237 predicate: patterns: root:
238 gitignoreFilterSourcePure predicate (withGitignoreFile patterns root) root;
239
240 gitignoreFilterRecursiveSource =
241 predicate: patterns: root:
242 gitignoreFilterSourcePure predicate (withRecursiveGitignoreFile patterns root) root;
243
244 # "Predicate"-less alternatives
245
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;
256
257 gitignoreRecursiveSource = gitignoreFilterSourcePure (_: _: true);
258}