-
Notifications
You must be signed in to change notification settings - Fork 20
/
find-files.nix
326 lines (275 loc) · 11.8 KB
/
find-files.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
{ lib ? import <nixpkgs/lib> }:
let
parse-ini = import ./parse-git-config.nix { inherit lib; };
parse-gitignore = import ./rules.nix { inherit lib; };
path = import ./lib/path.nix { inherit lib; };
in
rec {
inherit (builtins) dirOf baseNameOf abort split hasAttr readFile readDir pathExists;
inherit (lib.lists) filter length head tail concatMap take;
inherit (lib.attrsets) filterAttrs mapAttrs attrNames;
inherit (lib.strings) hasPrefix removePrefix splitString toLower;
inherit (lib) strings flip any;
inherit lib;
inherit parse-ini;
# TODO: 'filesystem.nix'
# - readLines function with CRLF support
# TODO: check assumption that a relative core.excludesFile is relative to HOME
# TODO: write test for trailing slash (matches dir only)
gitignoreFilter = basePath:
gitignoreFilterWith { inherit basePath; };
gitignoreFilterWith = { basePath, extraRules ? null, extraRulesWithContextDir ? [] }:
assert extraRules == null || builtins.typeOf extraRules == "string";
let
extraRules2 = extraRulesWithContextDir ++
lib.optional (extraRules != null) { contextDir = basePath; rules = extraRules; };
patternsBelowP = findPatternsTree extraRules2 basePath;
basePathStr = toString basePath;
rawFilter =
path: type: let
localDirPath = removePrefix basePathStr (toString (dirOf path));
localDirPathElements = splitString "/" localDirPath;
patternResult = parse-gitignore.runFilterPattern (getPatterns patternsBelowP localDirPathElements)."/patterns" path type;
contents = readDir path;
nonempty = localDirPathElements == [] || any (nodeName: memoFilter (path + "/${nodeName}") != false)
(attrNames contents);
in patternResult && (type == "directory" -> nonempty);
memoFilter =
path.memoize rawFilter
(p: throw "Could not find path ${toString p} in memo table. Did path or filtering semantics change?")
basePath;
in path: type: memoFilter path;
getPatterns =
patternTree: pathElems:
if length pathElems == 0
then patternTree
else let hd = head pathElems; in
if hd == "" || hd == "."
then getPatterns patternTree (tail pathElems)
else
if hasAttr hd patternTree
then getPatterns patternTree."${hd}" (tail pathElems)
else
# Files are not in the tree, so we return the
# most patterns we could find here.
patternTree;
#####
# Constructing a tree of patterns per non-ignored subdirectory, recursively
#
/* Given a dir, return a tree of patterns mirroring the directory structure,
where the patterns on the nodes towards the leaves become more specific.
It's a tree where the nodes are attribute sets and the keys are directory basenames.
The patterns are mixed into the attrsets using the special key "/patterns".
Leaves are simply {}
*/
findPatternsTree = extraRules: dir:
let
listOfStartingPatterns = map ({contextDir, rules ? readFile file, file ? throw "gitignore.nix: A `file` or `rules` attribute is required in extraRulesWithContextDir items.", ...}:
parse-gitignore.gitignoreFilter rules contextDir
) (findAncestryGitignores dir ++ extraRules);
startingPatterns = builtins.foldl'
parse-gitignore.mergePattern
(defaultPatterns dir) # not the unit of merge but a set of defaults
listOfStartingPatterns;
in
findDescendantPatternsTree startingPatterns dir;
# We do an eager-looking descent ourselves, in order to memoize the patterns.
# In fact it is lazy, so some directories' patterns will not need to be
# evaluated if not requested. This works out nicely when the user adds a
# filter *before* the gitignore filter.
#
# This function assumes that the gitignore files that are specified *in*
# dir, in the *ancestry* of dir or globally are already included in
# currentPatterns.
findDescendantPatternsTree = currentPatterns: dir:
let nodes = readDir dir;
dirs = filterAttrs (name: type:
type == nodeTypes.directory &&
(parse-gitignore.runFilterPattern currentPatterns (dir + "/${name}") type)
) nodes;
in mapAttrs (name: _t:
let subdir = dir + "/${name}";
ignore = subdir + "/.gitignore";
newPatterns = map (file:
parse-gitignore.mergePattern
currentPatterns # Performance: this is where you could potentially filter out patterns irrelevant to subdir
(parse-gitignore.gitignoreFilter (readFile file) subdir)
) (guardFile ignore);
subdirPatterns = headOr currentPatterns newPatterns;
in
findDescendantPatternsTree subdirPatterns subdir
) dirs // { "/patterns" = currentPatterns; };
defaultPatterns = root: parse-gitignore.gitignoreFilter ".git" root; # no trailing slash, because of worktree references
#####
# Finding the gitignore files in the current directory, towards the root and
# in the user config.
#
findAncestryGitignores = path:
let
up = inspectDirAndUp path;
inherit (up) localIgnores gitDir worktreeRoot;
globalIgnores =
if builtins?currentSystem
# impure mode: we should account for the user's gitignores as their tooling
# can put impure files in the project
then map (file: { contextDir = worktreeRoot; inherit file; }) maybeGlobalIgnoresFile
# pure mode: we hope that all ignores are also in the project .gitignore
else [];
# TODO: can local config override global core.excludesFile?
# localConfigItems = parse-ini.parseIniFile (gitDir + "/config");
in
globalIgnores ++ localIgnores;
#####
# Functions for getting "context" from directory ancestry, repo
#
/* path -> { localIgnores : list {contextDir, file}
, gitDir : path }
Precondition: dir exists and is a directory
*/
inspectDirAndUp = dirPath: let
go = p: acc:
let
parentDir = dirOf p;
dirInfo = inspectDir p;
isHighest = dirInfo.isWorkTreeRoot || p == /. || p == "/";
dirs = [dirInfo] ++ acc;
getIgnores = di: if di.hasGitignore
then [{ contextDir = di.dirPath; file = di.dirPath + "/.gitignore"; }]
else [];
in
if isHighest || isForbiddenDir (toString parentDir)
then
{
localIgnores = concatMap getIgnores dirs;
worktreeRoot = p;
inherit (dirInfo) gitDir;
}
else
go parentDir dirs
;
in go dirPath [];
# isForbiddenDir: string -> bool
#
# Some directories should never be traversed when looking for .git
# - for performance
# - to help lorri and possibly other tools that monitor which paths are read
# during evaluation
isForbiddenDir = p:
p == builtins.storeDir || p == "/";
inspectDir = dirPath:
let
d = readDir dirPath;
dotGitType = d.".git" or null;
isWorkTreeRoot = pathExists (dirPath + "/.git");
gitDir = if dotGitType == nodeTypes.directory then dirPath + "/.git"
else if dotGitType == nodeTypes.regular then readDotGitFile (dirPath + "/.git")
else if dotGitType == nodeTypes.symlink then throw "gitignore.nix: ${toString dirPath}/.git is a symlink. This is not supported (yet?)."
else if dotGitType == null then null
else throw "gitignore.nix: ${toString dirPath}/.git is of unknown node type ${dotGitType}";
# directory should probably be ignored here, but to figure out the node type, we
# currently don't have a builtin to do it directly and readDir is expensive,
# particularly for a tool like lorri.
hasGitignore = pathExists (dirPath + "/.gitignore");
in { inherit isWorkTreeRoot hasGitignore gitDir dirPath; };
/* .git file path -> GIT_DIR
Used for establishing $GIT_DIR when the worktree is an external worktree,
when .git is a file.
*/
readDotGitFile = filepath:
let contents = readFile filepath;
lines = lib.strings.splitString "\n" contents;
gitdirLines = map (strings.removePrefix "gitdir: ") (filter (lib.strings.hasPrefix "gitdir: ") lines);
errNoGitDirLine = abort ("Could not find a gitdir line in " + filepath);
in /. + headOr errNoGitDirLine gitdirLines
;
/* default -> list -> head or default
*/
headOr = default: l:
if length l == 0 then default else head l;
#####
# Finding git config
#
home = if lib.inPureEvalMode or false then _: /nonexistent else import ./home.nix;
maybeXdgGitConfigFile =
for
(guardNonEmptyString (builtins.getEnv "XDG_CONFIG_HOME"))
(xdgConfigHome:
guardFile (/. + xdgConfigHome + "/git/config")
);
maybeGlobalConfig = take 1 (guardFile (home /.gitconfig)
++ maybeXdgGitConfigFile
++ guardFile (home /.config/git/config));
globalConfigItems = for maybeGlobalConfig (globalConfigFile:
parse-ini.parseIniFile globalConfigFile
);
globalConfiguredExcludesFile = take 1 (
for
globalConfigItems
({section, key, value}:
for
(guard (toLower section == "core" && toLower key == "excludesfile"))
(_:
resolveFile (home /.) value
)
)
);
xdgExcludesFile = for
(guardNonEmptyString (builtins.getEnv "XDG_CONFIG_HOME"))
(xdgConfigHome:
guardFile (/. + xdgConfigHome + "/git/ignore")
);
maybeGlobalIgnoresFile = take 1
( globalConfiguredExcludesFile
++ xdgExcludesFile
++ guardFile (home /.config/git/ignore));
/* Given baseDir, which generalizes the idea of working directory,
resolve a file path relative to that directory.
It will return at most 1 path; 0 if no such file could be found.
Absolute paths and home-relative (~) paths ignore the baseDir, unless
the
*/
resolveFile = baseDir: path: take 1
( if hasPrefix "/" path then guardFile (/. + path) else
(if hasPrefix "~" path then guardFile (home /. + removePrefix "~" path) else [])
++ guardFile (baseDir + "/" + path)
)
;
#####
# List as a search and backtracking tool
#
nullableToList = x: if x == null then [] else [x];
for = l: f: concatMap f l;
guard = b: if b then [{}] else [];
/*
Check whether a path exists; if it does, return it as a singleton list.
Currently it checks whether a path exists, but we'd like to check the
node type, so we don't try to readFile for example a directory if we don't
have to.
It can be done with readDir but this causes lorri to watch everything,
which is really bad when reading for example ~/.gitconfig.
*/
# TODO: get something like builtins.pathType or builtins.stat into Nix
guardFile = p: if pathExists p then [p] else [];
guardNonEmptyString = s: if s == "" then [s] else [];
guardNonNull = a: if a != null then a else [];
#####
# Working with readDir output
#
nodeTypes.directory = "directory";
nodeTypes.regular = "regular";
nodeTypes.symlink = "symlink";
# TODO: Assumes that it's a file when it's a symlink
nodeTypes.isFile = p: p == nodeTypes.regular || p == nodeTypes.symlink;
#####
# Generic file system functions
#
/* path -> nullable nodeType
* Without throwing (unrecoverable) errors
*/
safeGetNodeType = path:
if toString path == "/" then nodeTypes.directory
else if pathExists path
then let parentDir = readDir (dirOf path);
in parentDir."${baseNameOf path}" or null
else null;
}