]>
Commit | Line | Data |
---|---|---|
f004c4a8 | 1 | #include "git2/ignore.h" |
157cef10 | 2 | #include "common.h" |
df743c7d | 3 | #include "ignore.h" |
823c0e9c | 4 | #include "attrcache.h" |
df743c7d | 5 | #include "path.h" |
ec40b7f9 | 6 | #include "config.h" |
df743c7d RB |
7 | |
8 | #define GIT_IGNORE_INTERNAL "[internal]exclude" | |
df743c7d | 9 | |
02df42dd RB |
10 | #define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n" |
11 | ||
f917481e | 12 | static int parse_ignore_file( |
823c0e9c | 13 | git_repository *repo, git_attr_file *attrs, const char *data) |
df743c7d | 14 | { |
b709e951 | 15 | int error = 0; |
eac76c23 | 16 | int ignore_case = false; |
7d490872 RB |
17 | const char *scan = data, *context = NULL; |
18 | git_attr_fnmatch *match = NULL; | |
ec40b7f9 | 19 | |
823c0e9c | 20 | if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0) |
7d490872 | 21 | giterr_clear(); |
df743c7d | 22 | |
7d490872 | 23 | /* if subdir file path, convert context for file paths */ |
823c0e9c RB |
24 | if (attrs->entry && |
25 | git_path_root(attrs->entry->path) < 0 && | |
26 | !git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE)) | |
27 | context = attrs->entry->path; | |
df743c7d | 28 | |
e6e8530a | 29 | if (git_mutex_lock(&attrs->lock) < 0) { |
823c0e9c | 30 | giterr_set(GITERR_OS, "Failed to lock ignore file"); |
e6e8530a RB |
31 | return -1; |
32 | } | |
33 | ||
0d0fa7c3 RB |
34 | while (!error && *scan) { |
35 | if (!match) { | |
36 | match = git__calloc(1, sizeof(*match)); | |
37 | GITERR_CHECK_ALLOC(match); | |
df743c7d RB |
38 | } |
39 | ||
4ba64794 | 40 | match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG; |
9939e602 | 41 | |
4ba64794 | 42 | if (!(error = git_attr_fnmatch__parse( |
7d490872 | 43 | match, &attrs->pool, context, &scan))) |
19fa2bc1 | 44 | { |
edb456c3 PK |
45 | match->flags |= GIT_ATTR_FNMATCH_IGNORE; |
46 | ||
47 | if (ignore_case) | |
48 | match->flags |= GIT_ATTR_FNMATCH_ICASE; | |
49 | ||
df743c7d | 50 | scan = git__next_line(scan); |
7d490872 | 51 | error = git_vector_insert(&attrs->rules, match); |
df743c7d RB |
52 | } |
53 | ||
0d0fa7c3 | 54 | if (error != 0) { |
df743c7d RB |
55 | git__free(match->pattern); |
56 | match->pattern = NULL; | |
57 | ||
58 | if (error == GIT_ENOTFOUND) | |
0d0fa7c3 | 59 | error = 0; |
df743c7d RB |
60 | } else { |
61 | match = NULL; /* vector now "owns" the match */ | |
62 | } | |
63 | } | |
64 | ||
e6e8530a | 65 | git_mutex_unlock(&attrs->lock); |
1dbcc9fc | 66 | git__free(match); |
df743c7d | 67 | |
df743c7d RB |
68 | return error; |
69 | } | |
70 | ||
7d490872 RB |
71 | static int push_ignore_file( |
72 | git_ignores *ignores, | |
73 | git_vector *which_list, | |
74 | const char *base, | |
75 | const char *filename) | |
76 | { | |
77 | int error = 0; | |
78 | git_attr_file *file = NULL; | |
79 | ||
2e9d813b | 80 | error = git_attr_cache__get( |
823c0e9c RB |
81 | &file, ignores->repo, GIT_ATTR_FILE__FROM_FILE, |
82 | base, filename, parse_ignore_file); | |
2e9d813b RB |
83 | if (error < 0) |
84 | return error; | |
85 | ||
86 | if (file != NULL) { | |
87 | if ((error = git_vector_insert(which_list, file)) < 0) | |
88 | git_attr_file__free(file); | |
89 | } | |
7d490872 RB |
90 | |
91 | return error; | |
92 | } | |
df743c7d | 93 | |
25e0b157 | 94 | static int push_one_ignore(void *payload, git_buf *path) |
0cfcff5d | 95 | { |
25e0b157 | 96 | git_ignores *ign = payload; |
8f7bc646 | 97 | ign->depth++; |
7d490872 | 98 | return push_ignore_file(ign, &ign->ign_path, path->ptr, GIT_IGNORE_FILE); |
0cfcff5d RB |
99 | } |
100 | ||
7d490872 | 101 | static int get_internal_ignores(git_attr_file **out, git_repository *repo) |
02df42dd RB |
102 | { |
103 | int error; | |
104 | ||
7d490872 RB |
105 | if ((error = git_attr_cache__init(repo)) < 0) |
106 | return error; | |
107 | ||
7d490872 | 108 | error = git_attr_cache__get( |
823c0e9c | 109 | out, repo, GIT_ATTR_FILE__IN_MEMORY, NULL, GIT_IGNORE_INTERNAL, NULL); |
02df42dd | 110 | |
7d490872 RB |
111 | /* if internal rules list is empty, insert default rules */ |
112 | if (!error && !(*out)->rules.length) | |
823c0e9c | 113 | error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES); |
02df42dd RB |
114 | |
115 | return error; | |
116 | } | |
117 | ||
f917481e RB |
118 | int git_ignore__for_path( |
119 | git_repository *repo, | |
120 | const char *path, | |
121 | git_ignores *ignores) | |
df743c7d | 122 | { |
0d0fa7c3 | 123 | int error = 0; |
df743c7d | 124 | const char *workdir = git_repository_workdir(repo); |
adc9bdb3 RB |
125 | |
126 | assert(ignores); | |
df743c7d | 127 | |
2e9d813b | 128 | memset(ignores, 0, sizeof(*ignores)); |
b6c93aef | 129 | ignores->repo = repo; |
b6c93aef | 130 | |
eac76c23 RB |
131 | /* Read the ignore_case flag */ |
132 | if ((error = git_repository__cvar( | |
133 | &ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0) | |
ec40b7f9 PK |
134 | goto cleanup; |
135 | ||
2e9d813b | 136 | if ((error = git_attr_cache__init(repo)) < 0) |
df743c7d RB |
137 | goto cleanup; |
138 | ||
f917481e RB |
139 | /* given a unrooted path in a non-bare repo, resolve it */ |
140 | if (workdir && git_path_root(path) < 0) | |
141 | error = git_path_find_dir(&ignores->dir, path, workdir); | |
142 | else | |
143 | error = git_buf_sets(&ignores->dir, path); | |
144 | if (error < 0) | |
df743c7d RB |
145 | goto cleanup; |
146 | ||
b6c93aef | 147 | /* set up internals */ |
7d490872 | 148 | if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0) |
df743c7d RB |
149 | goto cleanup; |
150 | ||
151 | /* load .gitignore up the path */ | |
f917481e RB |
152 | if (workdir != NULL) { |
153 | error = git_path_walk_up( | |
25e0b157 | 154 | &ignores->dir, workdir, push_one_ignore, ignores); |
f917481e RB |
155 | if (error < 0) |
156 | goto cleanup; | |
157 | } | |
df743c7d RB |
158 | |
159 | /* load .git/info/exclude */ | |
7d490872 RB |
160 | error = push_ignore_file( |
161 | ignores, &ignores->ign_global, | |
95dfb031 | 162 | git_repository_path(repo), GIT_IGNORE_FILE_INREPO); |
0d0fa7c3 | 163 | if (error < 0) |
df743c7d RB |
164 | goto cleanup; |
165 | ||
166 | /* load core.excludesfile */ | |
95dfb031 | 167 | if (git_repository_attr_cache(repo)->cfg_excl_file != NULL) |
7d490872 RB |
168 | error = push_ignore_file( |
169 | ignores, &ignores->ign_global, NULL, | |
95dfb031 | 170 | git_repository_attr_cache(repo)->cfg_excl_file); |
df743c7d RB |
171 | |
172 | cleanup: | |
0d0fa7c3 | 173 | if (error < 0) |
b6c93aef | 174 | git_ignore__free(ignores); |
95dfb031 | 175 | |
b6c93aef RB |
176 | return error; |
177 | } | |
178 | ||
179 | int git_ignore__push_dir(git_ignores *ign, const char *dir) | |
180 | { | |
0d0fa7c3 RB |
181 | if (git_buf_joinpath(&ign->dir, ign->dir.ptr, dir) < 0) |
182 | return -1; | |
ba8b8c04 | 183 | |
8f7bc646 RB |
184 | ign->depth++; |
185 | ||
ba8b8c04 | 186 | return push_ignore_file( |
7d490872 | 187 | ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE); |
df743c7d RB |
188 | } |
189 | ||
b6c93aef RB |
190 | int git_ignore__pop_dir(git_ignores *ign) |
191 | { | |
192 | if (ign->ign_path.length > 0) { | |
193 | git_attr_file *file = git_vector_last(&ign->ign_path); | |
823c0e9c | 194 | const char *start = file->entry->path, *end; |
3bc3ed80 | 195 | |
7d490872 RB |
196 | /* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/") |
197 | * - file->path looks something like "a/b/.gitignore | |
3bc3ed80 | 198 | * |
7d490872 RB |
199 | * We are popping the last directory off ign->dir. We also want |
200 | * to remove the file from the vector if the popped directory | |
201 | * matches the ignore path. We need to test if the "a/b" part of | |
3bc3ed80 RB |
202 | * the file key matches the path we are about to pop. |
203 | */ | |
204 | ||
7d490872 RB |
205 | if ((end = strrchr(start, '/')) != NULL) { |
206 | size_t dirlen = (end - start) + 1; | |
3bc3ed80 | 207 | |
7d490872 RB |
208 | if (ign->dir.size >= dirlen && |
209 | !memcmp(ign->dir.ptr + ign->dir.size - dirlen, start, dirlen)) | |
210 | { | |
211 | git_vector_pop(&ign->ign_path); | |
212 | git_attr_file__free(file); | |
213 | } | |
cef170ab | 214 | } |
8f7bc646 | 215 | } |
ba8b8c04 | 216 | |
8f7bc646 | 217 | if (--ign->depth > 0) { |
b6c93aef | 218 | git_buf_rtruncate_at_char(&ign->dir, '/'); |
8f7bc646 | 219 | git_path_to_dir(&ign->dir); |
b6c93aef | 220 | } |
8f7bc646 | 221 | |
0d0fa7c3 | 222 | return 0; |
b6c93aef RB |
223 | } |
224 | ||
adc9bdb3 | 225 | void git_ignore__free(git_ignores *ignores) |
df743c7d | 226 | { |
b8777615 RB |
227 | unsigned int i; |
228 | git_attr_file *file; | |
229 | ||
7d490872 | 230 | git_attr_file__free(ignores->ign_internal); |
b8777615 RB |
231 | |
232 | git_vector_foreach(&ignores->ign_path, i, file) { | |
233 | git_attr_file__free(file); | |
234 | ignores->ign_path.contents[i] = NULL; | |
235 | } | |
b6c93aef | 236 | git_vector_free(&ignores->ign_path); |
b8777615 RB |
237 | |
238 | git_vector_foreach(&ignores->ign_global, i, file) { | |
239 | git_attr_file__free(file); | |
240 | ignores->ign_global.contents[i] = NULL; | |
241 | } | |
b6c93aef | 242 | git_vector_free(&ignores->ign_global); |
b8777615 | 243 | |
b6c93aef RB |
244 | git_buf_free(&ignores->dir); |
245 | } | |
246 | ||
0d0fa7c3 | 247 | static bool ignore_lookup_in_rules( |
e6e8530a | 248 | git_attr_file *file, git_attr_path *path, int *ignored) |
b6c93aef | 249 | { |
b8457baa | 250 | size_t j; |
b6c93aef RB |
251 | git_attr_fnmatch *match; |
252 | ||
e6e8530a | 253 | git_vector_rforeach(&file->rules, j, match) { |
ab43ad2f | 254 | if (git_attr_fnmatch__match(match, path)) { |
b6c93aef | 255 | *ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0); |
0d0fa7c3 | 256 | return true; |
b6c93aef RB |
257 | } |
258 | } | |
259 | ||
0d0fa7c3 | 260 | return false; |
df743c7d RB |
261 | } |
262 | ||
f917481e RB |
263 | int git_ignore__lookup( |
264 | git_ignores *ignores, const char *pathname, int *ignored) | |
df743c7d | 265 | { |
b6c93aef | 266 | unsigned int i; |
df743c7d RB |
267 | git_attr_file *file; |
268 | git_attr_path path; | |
df743c7d | 269 | |
0d0fa7c3 RB |
270 | if (git_attr_path__init( |
271 | &path, pathname, git_repository_workdir(ignores->repo)) < 0) | |
272 | return -1; | |
df743c7d | 273 | |
0d0fa7c3 | 274 | /* first process builtins - success means path was found */ |
e6e8530a | 275 | if (ignore_lookup_in_rules(ignores->ign_internal, &path, ignored)) |
d58336dd | 276 | goto cleanup; |
df743c7d | 277 | |
b6c93aef RB |
278 | /* next process files in the path */ |
279 | git_vector_foreach(&ignores->ign_path, i, file) { | |
e6e8530a | 280 | if (ignore_lookup_in_rules(file, &path, ignored)) |
d58336dd | 281 | goto cleanup; |
df743c7d | 282 | } |
df743c7d | 283 | |
b6c93aef RB |
284 | /* last process global ignores */ |
285 | git_vector_foreach(&ignores->ign_global, i, file) { | |
e6e8530a | 286 | if (ignore_lookup_in_rules(file, &path, ignored)) |
d58336dd | 287 | goto cleanup; |
b6c93aef RB |
288 | } |
289 | ||
290 | *ignored = 0; | |
d58336dd RB |
291 | |
292 | cleanup: | |
293 | git_attr_path__free(&path); | |
0d0fa7c3 | 294 | return 0; |
df743c7d | 295 | } |
f004c4a8 | 296 | |
823c0e9c | 297 | int git_ignore_add_rule(git_repository *repo, const char *rules) |
f004c4a8 RB |
298 | { |
299 | int error; | |
7d490872 | 300 | git_attr_file *ign_internal = NULL; |
f004c4a8 | 301 | |
823c0e9c RB |
302 | if ((error = get_internal_ignores(&ign_internal, repo)) < 0) |
303 | return error; | |
304 | ||
305 | error = parse_ignore_file(repo, ign_internal, rules); | |
306 | git_attr_file__free(ign_internal); | |
f004c4a8 RB |
307 | |
308 | return error; | |
309 | } | |
310 | ||
823c0e9c | 311 | int git_ignore_clear_internal_rules(git_repository *repo) |
f004c4a8 RB |
312 | { |
313 | int error; | |
314 | git_attr_file *ign_internal; | |
315 | ||
823c0e9c RB |
316 | if ((error = get_internal_ignores(&ign_internal, repo)) < 0) |
317 | return error; | |
7d490872 | 318 | |
823c0e9c RB |
319 | if (!(error = git_attr_file__clear_rules(ign_internal, true))) |
320 | error = parse_ignore_file( | |
321 | repo, ign_internal, GIT_IGNORE_DEFAULT_RULES); | |
02df42dd | 322 | |
823c0e9c | 323 | git_attr_file__free(ign_internal); |
f004c4a8 RB |
324 | return error; |
325 | } | |
2fb4e9b3 RB |
326 | |
327 | int git_ignore_path_is_ignored( | |
328 | int *ignored, | |
329 | git_repository *repo, | |
52032ae5 | 330 | const char *pathname) |
2fb4e9b3 RB |
331 | { |
332 | int error; | |
52032ae5 RB |
333 | const char *workdir; |
334 | git_attr_path path; | |
335 | char *tail, *end; | |
336 | bool full_is_dir; | |
2fb4e9b3 | 337 | git_ignores ignores; |
52032ae5 RB |
338 | unsigned int i; |
339 | git_attr_file *file; | |
2fb4e9b3 | 340 | |
52032ae5 RB |
341 | assert(ignored && pathname); |
342 | ||
343 | workdir = repo ? git_repository_workdir(repo) : NULL; | |
344 | ||
345 | if ((error = git_attr_path__init(&path, pathname, workdir)) < 0) | |
346 | return error; | |
347 | ||
348 | tail = path.path; | |
349 | end = &path.full.ptr[path.full.size]; | |
350 | full_is_dir = path.is_dir; | |
2fb4e9b3 | 351 | |
52032ae5 RB |
352 | while (1) { |
353 | /* advance to next component of path */ | |
354 | path.basename = tail; | |
355 | ||
356 | while (tail < end && *tail != '/') tail++; | |
357 | *tail = '\0'; | |
358 | ||
359 | path.full.size = (tail - path.full.ptr); | |
360 | path.is_dir = (tail == end) ? full_is_dir : true; | |
361 | ||
ba8b8c04 RB |
362 | /* initialize ignores the first time through */ |
363 | if (path.basename == path.path && | |
364 | (error = git_ignore__for_path(repo, path.path, &ignores)) < 0) | |
52032ae5 RB |
365 | break; |
366 | ||
367 | /* first process builtins - success means path was found */ | |
e6e8530a | 368 | if (ignore_lookup_in_rules(ignores.ign_internal, &path, ignored)) |
52032ae5 RB |
369 | goto cleanup; |
370 | ||
371 | /* next process files in the path */ | |
372 | git_vector_foreach(&ignores.ign_path, i, file) { | |
e6e8530a | 373 | if (ignore_lookup_in_rules(file, &path, ignored)) |
52032ae5 RB |
374 | goto cleanup; |
375 | } | |
376 | ||
377 | /* last process global ignores */ | |
378 | git_vector_foreach(&ignores.ign_global, i, file) { | |
e6e8530a | 379 | if (ignore_lookup_in_rules(file, &path, ignored)) |
52032ae5 RB |
380 | goto cleanup; |
381 | } | |
382 | ||
383 | /* if we found no rules before reaching the end, we're done */ | |
384 | if (tail == end) | |
385 | break; | |
386 | ||
ba8b8c04 RB |
387 | /* now add this directory to list of ignores */ |
388 | if ((error = git_ignore__push_dir(&ignores, path.path)) < 0) | |
389 | break; | |
390 | ||
52032ae5 RB |
391 | /* reinstate divider in path */ |
392 | *tail = '/'; | |
393 | while (*tail == '/') tail++; | |
394 | } | |
395 | ||
396 | *ignored = 0; | |
397 | ||
398 | cleanup: | |
399 | git_attr_path__free(&path); | |
2fb4e9b3 RB |
400 | git_ignore__free(&ignores); |
401 | return error; | |
402 | } | |
403 | ||
85b8b18b RB |
404 | |
405 | int git_ignore__check_pathspec_for_exact_ignores( | |
406 | git_repository *repo, | |
407 | git_vector *vspec, | |
408 | bool no_fnmatch) | |
409 | { | |
410 | int error = 0; | |
411 | size_t i; | |
412 | git_attr_fnmatch *match; | |
413 | int ignored; | |
414 | git_buf path = GIT_BUF_INIT; | |
415 | const char *wd, *filename; | |
416 | git_index *idx; | |
417 | ||
418 | if ((error = git_repository__ensure_not_bare( | |
419 | repo, "validate pathspec")) < 0 || | |
420 | (error = git_repository_index(&idx, repo)) < 0) | |
421 | return error; | |
422 | ||
423 | wd = git_repository_workdir(repo); | |
424 | ||
425 | git_vector_foreach(vspec, i, match) { | |
426 | /* skip wildcard matches (if they are being used) */ | |
427 | if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 && | |
428 | !no_fnmatch) | |
429 | continue; | |
430 | ||
431 | filename = match->pattern; | |
432 | ||
433 | /* if file is already in the index, it's fine */ | |
434 | if (git_index_get_bypath(idx, filename, 0) != NULL) | |
435 | continue; | |
436 | ||
437 | if ((error = git_buf_joinpath(&path, wd, filename)) < 0) | |
438 | break; | |
439 | ||
440 | /* is there a file on disk that matches this exactly? */ | |
441 | if (!git_path_isfile(path.ptr)) | |
442 | continue; | |
443 | ||
444 | /* is that file ignored? */ | |
445 | if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0) | |
446 | break; | |
447 | ||
448 | if (ignored) { | |
449 | giterr_set(GITERR_INVALID, "pathspec contains ignored file '%s'", | |
450 | filename); | |
451 | error = GIT_EINVALIDSPEC; | |
452 | break; | |
453 | } | |
454 | } | |
455 | ||
456 | git_index_free(idx); | |
457 | git_buf_free(&path); | |
458 | ||
459 | return error; | |
460 | } | |
461 |