anvil-editor: init at 0.4
[NixPkgs.git] / pkgs / build-support / nix-gitignore / default.nix
blob849720675c8924dc1de7280220d47c724a1bfdf5
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     ;
34   inherit (lib.strings) match split typeOf;
36   debug = a: trace a a;
37   last = l: elemAt l ((length l) - 1);
38 in rec {
39   # [["good/relative/source/file" true] ["bad.tmpfile" false]] -> root -> path
40   filterPattern = patterns: root:
41     (name: _type:
42       let
43         relPath = removePrefix ((toString root) + "/") name;
44         matches = pair: (match (head pair) relPath) != null;
45         matched = map (pair: [(matches pair) (last pair)]) patterns;
46       in
47         last (last ([[true true]] ++ (filter head matched)))
48     );
50   # string -> [[regex bool]]
51   gitignoreToPatterns = gitignore:
52     let
53       # ignore -> bool
54       isComment = i: (match "^(#.*|$)" i) != null;
56       # ignore -> [ignore bool]
57       computeNegation = l:
58         let split = match "^(!?)(.*)" l;
59         in [(elemAt split 1) (head split == "!")];
61       # regex -> regex
62       handleHashesBangs = replaceStrings ["\\#" "\\!"] ["#" "!"];
64       # ignore -> regex
65       substWildcards =
66         let
67           special = "^$.+{}()";
68           escs = "\\*?";
69           splitString =
70             let recurse = str : [(substring 0 1 str)] ++
71                                  (optionals (str != "") (recurse (substring 1 (stringLength(str)) str) ));
72             in str : recurse str;
73           chars = s: filter (c: c != "" && !isList c) (splitString s);
74           escape = s: map (c: "\\" + c) (chars s);
75         in
76           replaceStrings
77             ((chars special)  ++ (escape escs) ++ ["**/"    "**" "*"     "?"])
78             ((escape special) ++ (escape escs) ++ ["(.*/)?" ".*" "[^/]*" "[^/]"]);
80       # (regex -> regex) -> regex -> regex
81       mapAroundCharclass = f: r: # rl = regex or list
82         let slightFix = replaceStrings ["\\]"] ["]"];
83         in
84           concatStringsSep ""
85           (map (rl: if isList rl then slightFix (elemAt rl 0) else f rl)
86           (split "(\\[([^\\\\]|\\\\.)+])" r));
88       # regex -> regex
89       handleSlashPrefix = l:
90         let
91           split = (match "^(/?)(.*)" l);
92           findSlash = l: optionalString ((match ".+/.+" l) == null) l;
93           hasSlash = mapAroundCharclass findSlash l != l;
94         in
95           (if (elemAt split 0) == "/" || hasSlash
96           then "^"
97           else "(^|.*/)"
98           ) + (elemAt split 1);
100       # regex -> regex
101       handleSlashSuffix = l:
102         let split = (match "^(.*)/$" l);
103         in if split != null then (elemAt split 0) + "($|/.*)" else l;
105       # (regex -> regex) -> [regex, bool] -> [regex, bool]
106       mapPat = f: l: [(f (head l)) (last l)];
107     in
108       map (l: # `l' for "line"
109         mapPat (l: handleSlashSuffix (handleSlashPrefix (handleHashesBangs (mapAroundCharclass substWildcards l))))
110         (computeNegation l))
111       (filter (l: !isList l && !isComment l)
112       (split "\n" gitignore));
114   gitignoreFilter = ign: root: filterPattern (gitignoreToPatterns ign) root;
116   # string|[string|file] (→ [string|file] → [string]) -> string
117   gitignoreCompileIgnore = file_str_patterns: root:
118     let
119       onPath = f: a: if typeOf a == "path" then f a else a;
120       str_patterns = map (onPath readFile) (toList file_str_patterns);
121     in concatStringsSep "\n" str_patterns;
123   gitignoreFilterPure = predicate: patterns: root: name: type:
124     gitignoreFilter (gitignoreCompileIgnore patterns root) root name type
125     && predicate name type;
127   # This is a very hacky way of programming this!
128   # A better way would be to reuse existing filtering by making multiple gitignore functions per each root.
129   # Then for each file find the set of roots with gitignores (and functions).
130   # This would make gitignoreFilterSource very different from gitignoreFilterPure.
131   # rootPath → gitignoresConcatenated
132   compileRecursiveGitignore = root:
133     let
134       dirOrIgnore = file: type: baseNameOf file == ".gitignore" || type == "directory";
135       ignores = builtins.filterSource dirOrIgnore root;
136     in readFile (
137       runCommand "${baseNameOf root}-recursive-gitignore" {} ''
138         cd ${ignores}
140         find -type f -exec sh -c '
141           rel="$(realpath --relative-to=. "$(dirname "$1")")/"
142           if [ "$rel" = "./" ]; then rel=""; fi
144           awk -v prefix="$rel" -v root="$1" -v top="$(test -z "$rel" && echo 1)" "
145             BEGIN { print \"# \"root }
147             /^!?[^\\/]+\/?$/ {
148               match(\$0, /^!?/, negation)
149               sub(/^!?/, \"\")
151               if (top) { middle = \"\" } else { middle = \"**/\" }
153               print negation[0] prefix middle \$0
154             }
156             /^!?(\\/|.*\\/.+$)/ {
157               match(\$0, /^!?/, negation)
158               sub(/^!?/, \"\")
160               if (!top) sub(/^\//, \"\")
162               print negation[0] prefix \$0
163             }
165             END { print \"\" }
166           " "$1"
167         ' sh {} \; > $out
168       '');
170   withGitignoreFile = patterns: root:
171     toList patterns ++ [ ".git" ] ++ [(root + "/.gitignore")];
173   withRecursiveGitignoreFile = patterns: root:
174     toList patterns ++ [ ".git" ] ++ [(compileRecursiveGitignore root)];
176   # filterSource derivatives
178   gitignoreFilterSourcePure = predicate: patterns: root:
179     filterSource (gitignoreFilterPure predicate patterns root) root;
181   gitignoreFilterSource = predicate: patterns: root:
182     gitignoreFilterSourcePure predicate (withGitignoreFile patterns root) root;
184   gitignoreFilterRecursiveSource = predicate: patterns: root:
185     gitignoreFilterSourcePure predicate (withRecursiveGitignoreFile patterns root) root;
187   # "Predicate"-less alternatives
189   gitignoreSourcePure = gitignoreFilterSourcePure (_: _: true);
190   gitignoreSource = patterns: let type = typeOf patterns; in
191     if (type == "string" && pathExists patterns) || type == "path"
192     then throw
193       "type error in gitignoreSource(patterns -> source -> path), "
194       "use [] or \"\" if there are no additional patterns"
195     else gitignoreFilterSource (_: _: true) patterns;
197   gitignoreRecursiveSource = gitignoreFilterSourcePure (_: _: true);