Commit 9e37305a by Philip Kelley

Merge pull request #984 from arrbee/fix-fnmatch-and-ignore

Fix single file ignores
parents ebb86755 52032ae5
...@@ -54,9 +54,12 @@ GIT_EXTERN(int) git_ignore_clear_internal_rules( ...@@ -54,9 +54,12 @@ GIT_EXTERN(int) git_ignore_clear_internal_rules(
/** /**
* Test if the ignore rules apply to a given path. * Test if the ignore rules apply to a given path.
* *
* This function simply checks the ignore rules to see if they would apply * This function checks the ignore rules to see if they would apply to the
* to the given file. This indicates if the file would be ignored regardless * given file. This indicates if the file would be ignored regardless of
* of whether the file is already in the index or commited to the repository. * whether the file is already in the index or commited to the repository.
*
* One way to think of this is if you were to do "git add ." on the
* directory containing the file, would it be added or not?
* *
* @param ignored boolean returning 0 if the file is not ignored, 1 if it is * @param ignored boolean returning 0 if the file is not ignored, 1 if it is
* @param repo a repository object * @param repo a repository object
......
...@@ -146,10 +146,12 @@ GIT_EXTERN(int) git_status_file( ...@@ -146,10 +146,12 @@ GIT_EXTERN(int) git_status_file(
/** /**
* Test if the ignore rules apply to a given file. * Test if the ignore rules apply to a given file.
* *
* This function simply checks the ignore rules to see if they would apply * This function checks the ignore rules to see if they would apply to the
* to the given file. Unlike git_status_file(), this indicates if the file * given file. This indicates if the file would be ignored regardless of
* would be ignored regardless of whether the file is already in the index * whether the file is already in the index or commited to the repository.
* or in the repository. *
* One way to think of this is if you were to do "git add ." on the
* directory containing the file, would it be added or not?
* *
* @param ignored boolean returning 0 if the file is not ignored, 1 if it is * @param ignored boolean returning 0 if the file is not ignored, 1 if it is
* @param repo a repository object * @param repo a repository object
......
...@@ -71,10 +71,10 @@ typedef struct { ...@@ -71,10 +71,10 @@ typedef struct {
} git_attr_file; } git_attr_file;
typedef struct { typedef struct {
git_buf full; git_buf full;
const char *path; char *path;
const char *basename; char *basename;
int is_dir; int is_dir;
} git_attr_path; } git_attr_path;
typedef enum { typedef enum {
......
...@@ -24,13 +24,16 @@ ...@@ -24,13 +24,16 @@
static int rangematch(const char *, char, int, char **); static int rangematch(const char *, char, int, char **);
int static int
p_fnmatch(const char *pattern, const char *string, int flags) p_fnmatchx(const char *pattern, const char *string, int flags, size_t recurs)
{ {
const char *stringstart; const char *stringstart;
char *newp; char *newp;
char c, test; char c, test;
if (recurs-- == 0)
return FNM_NORES;
for (stringstart = string;;) for (stringstart = string;;)
switch (c = *pattern++) { switch (c = *pattern++) {
case EOS: case EOS:
...@@ -75,8 +78,11 @@ p_fnmatch(const char *pattern, const char *string, int flags) ...@@ -75,8 +78,11 @@ p_fnmatch(const char *pattern, const char *string, int flags)
/* General case, use recursion. */ /* General case, use recursion. */
while ((test = *string) != EOS) { while ((test = *string) != EOS) {
if (!p_fnmatch(pattern, string, flags & ~FNM_PERIOD)) int e;
return (0);
e = p_fnmatchx(pattern, string, flags & ~FNM_PERIOD, recurs);
if (e != FNM_NOMATCH)
return e;
if (test == '/' && (flags & FNM_PATHNAME)) if (test == '/' && (flags & FNM_PATHNAME))
break; break;
++string; ++string;
...@@ -178,3 +184,9 @@ rangematch(const char *pattern, char test, int flags, char **newp) ...@@ -178,3 +184,9 @@ rangematch(const char *pattern, char test, int flags, char **newp)
return (ok == negate ? RANGE_NOMATCH : RANGE_MATCH); return (ok == negate ? RANGE_NOMATCH : RANGE_MATCH);
} }
int
p_fnmatch(const char *pattern, const char *string, int flags)
{
return p_fnmatchx(pattern, string, flags, 64);
}
...@@ -11,12 +11,13 @@ ...@@ -11,12 +11,13 @@
#define FNM_NOMATCH 1 /* Match failed. */ #define FNM_NOMATCH 1 /* Match failed. */
#define FNM_NOSYS 2 /* Function not supported (unused). */ #define FNM_NOSYS 2 /* Function not supported (unused). */
#define FNM_NORES 3 /* Out of resources */
#define FNM_NOESCAPE 0x01 /* Disable backslash escaping. */ #define FNM_NOESCAPE 0x01 /* Disable backslash escaping. */
#define FNM_PATHNAME 0x02 /* Slash must be matched by slash. */ #define FNM_PATHNAME 0x02 /* Slash must be matched by slash. */
#define FNM_PERIOD 0x04 /* Period must be matched by period. */ #define FNM_PERIOD 0x04 /* Period must be matched by period. */
#define FNM_LEADING_DIR 0x08 /* Ignore /<tail> after Imatch. */ #define FNM_LEADING_DIR 0x08 /* Ignore /<tail> after Imatch. */
#define FNM_CASEFOLD 0x10 /* Case insensitive search. */ #define FNM_CASEFOLD 0x10 /* Case insensitive search. */
#define FNM_IGNORECASE FNM_CASEFOLD #define FNM_IGNORECASE FNM_CASEFOLD
#define FNM_FILE_NAME FNM_PATHNAME #define FNM_FILE_NAME FNM_PATHNAME
......
...@@ -277,15 +277,76 @@ int git_ignore_clear_internal_rules( ...@@ -277,15 +277,76 @@ int git_ignore_clear_internal_rules(
int git_ignore_path_is_ignored( int git_ignore_path_is_ignored(
int *ignored, int *ignored,
git_repository *repo, git_repository *repo,
const char *path) const char *pathname)
{ {
int error; int error;
const char *workdir;
git_attr_path path;
char *tail, *end;
bool full_is_dir;
git_ignores ignores; git_ignores ignores;
unsigned int i;
git_attr_file *file;
if (git_ignore__for_path(repo, path, &ignores) < 0) assert(ignored && pathname);
return -1;
workdir = repo ? git_repository_workdir(repo) : NULL;
if ((error = git_attr_path__init(&path, pathname, workdir)) < 0)
return error;
tail = path.path;
end = &path.full.ptr[path.full.size];
full_is_dir = path.is_dir;
error = git_ignore__lookup(&ignores, path, ignored); while (1) {
/* advance to next component of path */
path.basename = tail;
while (tail < end && *tail != '/') tail++;
*tail = '\0';
path.full.size = (tail - path.full.ptr);
path.is_dir = (tail == end) ? full_is_dir : true;
/* update ignores for new path fragment */
if (path.basename == path.path)
error = git_ignore__for_path(repo, path.path, &ignores);
else
error = git_ignore__push_dir(&ignores, path.basename);
if (error < 0)
break;
/* first process builtins - success means path was found */
if (ignore_lookup_in_rules(
&ignores.ign_internal->rules, &path, ignored))
goto cleanup;
/* next process files in the path */
git_vector_foreach(&ignores.ign_path, i, file) {
if (ignore_lookup_in_rules(&file->rules, &path, ignored))
goto cleanup;
}
/* last process global ignores */
git_vector_foreach(&ignores.ign_global, i, file) {
if (ignore_lookup_in_rules(&file->rules, &path, ignored))
goto cleanup;
}
/* if we found no rules before reaching the end, we're done */
if (tail == end)
break;
/* reinstate divider in path */
*tail = '/';
while (*tail == '/') tail++;
}
*ignored = 0;
cleanup:
git_attr_path__free(&path);
git_ignore__free(&ignores); git_ignore__free(&ignores);
return error; return error;
} }
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
#include "common.h" #include "common.h"
#include <fcntl.h> #include <fcntl.h>
#include <time.h> #include <time.h>
#include "fnmatch.h"
#ifndef S_IFGITLINK #ifndef S_IFGITLINK
#define S_IFGITLINK 0160000 #define S_IFGITLINK 0160000
......
...@@ -86,6 +86,10 @@ int git_status_foreach_ext( ...@@ -86,6 +86,10 @@ int git_status_foreach_ext(
assert(show <= GIT_STATUS_SHOW_INDEX_THEN_WORKDIR); assert(show <= GIT_STATUS_SHOW_INDEX_THEN_WORKDIR);
if (show != GIT_STATUS_SHOW_INDEX_ONLY &&
(err = git_repository__ensure_not_bare(repo, "status")) < 0)
return err;
if ((err = git_repository_head_tree(&head, repo)) < 0) if ((err = git_repository_head_tree(&head, repo)) < 0)
return err; return err;
...@@ -245,9 +249,22 @@ int git_status_file( ...@@ -245,9 +249,22 @@ int git_status_file(
error = GIT_EAMBIGUOUS; error = GIT_EAMBIGUOUS;
if (!error && !sfi.count) { if (!error && !sfi.count) {
giterr_set(GITERR_INVALID, git_buf full = GIT_BUF_INIT;
"Attempt to get status of nonexistent file '%s'", path);
error = GIT_ENOTFOUND; /* if the file actually exists and we still did not get a callback
* for it, then it must be contained inside an ignored directory, so
* mark it as such instead of generating an error.
*/
if (!git_buf_joinpath(&full, git_repository_workdir(repo), path) &&
git_path_exists(full.ptr))
sfi.status = GIT_STATUS_IGNORED;
else {
giterr_set(GITERR_INVALID,
"Attempt to get status of nonexistent file '%s'", path);
error = GIT_ENOTFOUND;
}
git_buf_free(&full);
} }
*status_flags = sfi.status; *status_flags = sfi.status;
......
...@@ -7,13 +7,6 @@ ...@@ -7,13 +7,6 @@
#ifndef INCLUDE_posix__w32_h__ #ifndef INCLUDE_posix__w32_h__
#define INCLUDE_posix__w32_h__ #define INCLUDE_posix__w32_h__
#if !defined(__sun) && !defined(__amigaos4__)
# include <fnmatch.h>
# define p_fnmatch(p, s, f) fnmatch(p, s, f)
#else
# include "compat/fnmatch.h"
#endif
#include <stdio.h> #include <stdio.h>
#define p_lstat(p,b) lstat(p,b) #define p_lstat(p,b) lstat(p,b)
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
#define INCLUDE_posix__w32_h__ #define INCLUDE_posix__w32_h__
#include "common.h" #include "common.h"
#include "compat/fnmatch.h"
#include "utf-conv.h" #include "utf-conv.h"
GIT_INLINE(int) p_link(const char *old, const char *new) GIT_INLINE(int) p_link(const char *old, const char *new)
......
...@@ -451,13 +451,13 @@ static void workdir_iterator_test( ...@@ -451,13 +451,13 @@ static void workdir_iterator_test(
git_iterator_free(i); git_iterator_free(i);
cl_assert(count == expected_count); cl_assert_equal_i(expected_count,count);
cl_assert(count_all == expected_count + expected_ignores); cl_assert_equal_i(expected_count + expected_ignores, count_all);
} }
void test_diff_iterator__workdir_0(void) void test_diff_iterator__workdir_0(void)
{ {
workdir_iterator_test("attr", NULL, NULL, 25, 2, NULL, "ign"); workdir_iterator_test("attr", NULL, NULL, 27, 1, NULL, "ign");
} }
static const char *status_paths[] = { static const char *status_paths[] = {
......
...@@ -22,21 +22,25 @@ void test_status_ignore__0(void) ...@@ -22,21 +22,25 @@ void test_status_ignore__0(void)
const char *path; const char *path;
int expected; int expected;
} test_cases[] = { } test_cases[] = {
/* patterns "sub" and "ign" from .gitignore */ /* pattern "ign" from .gitignore */
{ "file", 0 }, { "file", 0 },
{ "ign", 1 }, { "ign", 1 },
{ "sub", 1 }, { "sub", 0 },
{ "sub/file", 0 }, { "sub/file", 0 },
{ "sub/ign", 1 }, { "sub/ign", 1 },
{ "sub/sub", 1 }, { "sub/ign/file", 1 },
{ "sub/ign/sub", 1 },
{ "sub/ign/sub/file", 1 },
{ "sub/sub", 0 },
{ "sub/sub/file", 0 }, { "sub/sub/file", 0 },
{ "sub/sub/ign", 1 }, { "sub/sub/ign", 1 },
{ "sub/sub/sub", 1 }, { "sub/sub/sub", 0 },
/* pattern "dir/" from .gitignore */ /* pattern "dir/" from .gitignore */
{ "dir", 1 }, { "dir", 1 },
{ "dir/", 1 }, { "dir/", 1 },
{ "sub/dir", 1 }, { "sub/dir", 1 },
{ "sub/dir/", 1 }, { "sub/dir/", 1 },
{ "sub/dir/file", 1 }, /* contained in ignored parent */
{ "sub/sub/dir", 0 }, /* dir is not actually a dir, but a file */ { "sub/sub/dir", 0 }, /* dir is not actually a dir, but a file */
{ NULL, 0 } { NULL, 0 }
}, *one_test; }, *one_test;
...@@ -172,6 +176,61 @@ void test_status_ignore__ignore_pattern_ignorecase(void) ...@@ -172,6 +176,61 @@ void test_status_ignore__ignore_pattern_ignorecase(void)
cl_assert(flags == ignore_case ? GIT_STATUS_IGNORED : GIT_STATUS_WT_NEW); cl_assert(flags == ignore_case ? GIT_STATUS_IGNORED : GIT_STATUS_WT_NEW);
} }
void test_status_ignore__subdirectories(void)
{
status_entry_single st;
int ignored;
git_status_options opts;
g_repo = cl_git_sandbox_init("empty_standard_repo");
cl_git_mkfile(
"empty_standard_repo/ignore_me", "I'm going to be ignored!");
cl_git_rewritefile("empty_standard_repo/.gitignore", "ignore_me\n");
memset(&st, 0, sizeof(st));
cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st));
cl_assert_equal_i(2, st.count);
cl_assert(st.status == GIT_STATUS_IGNORED);
cl_git_pass(git_status_file(&st.status, g_repo, "ignore_me"));
cl_assert(st.status == GIT_STATUS_IGNORED);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "ignore_me"));
cl_assert(ignored);
/* So, interestingly, as per the comment in diff_from_iterators() the
* following file is ignored, but in a way so that it does not show up
* in status even if INCLUDE_IGNORED is used. This actually matches
* core git's behavior - if you follow these steps and try running "git
* status -uall --ignored" then the following file and directory will
* not show up in the output at all.
*/
cl_git_pass(
git_futils_mkdir_r("empty_standard_repo/test/ignore_me", NULL, 0775));
cl_git_mkfile(
"empty_standard_repo/test/ignore_me/file", "I'm going to be ignored!");
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
GIT_STATUS_OPT_INCLUDE_UNTRACKED |
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
memset(&st, 0, sizeof(st));
cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st));
cl_assert_equal_i(2, st.count);
cl_git_pass(git_status_file(&st.status, g_repo, "test/ignore_me/file"));
cl_assert(st.status == GIT_STATUS_IGNORED);
cl_git_pass(
git_status_should_ignore(&ignored, g_repo, "test/ignore_me/file"));
cl_assert(ignored);
}
void test_status_ignore__adding_internal_ignores(void) void test_status_ignore__adding_internal_ignores(void)
{ {
int ignored; int ignored;
...@@ -234,3 +293,49 @@ void test_status_ignore__add_internal_as_first_thing(void) ...@@ -234,3 +293,49 @@ void test_status_ignore__add_internal_as_first_thing(void)
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "two.bar")); cl_git_pass(git_status_should_ignore(&ignored, g_repo, "two.bar"));
cl_assert(!ignored); cl_assert(!ignored);
} }
void test_status_ignore__internal_ignores_inside_deep_paths(void)
{
int ignored;
const char *add_me = "Debug\nthis/is/deep\npatterned*/dir\n";
g_repo = cl_git_sandbox_init("empty_standard_repo");
cl_git_pass(git_ignore_add_rule(g_repo, add_me));
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "Debug"));
cl_assert(ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "and/Debug"));
cl_assert(ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "really/Debug/this/file"));
cl_assert(ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "Debug/what/I/say"));
cl_assert(ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "and/NoDebug"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "NoDebug/this"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "please/NoDebug/this"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "this/is/deep"));
cl_assert(ignored);
/* pattern containing slash gets FNM_PATHNAME so all slashes must match */
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "and/this/is/deep"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "this/is/deep/too"));
cl_assert(ignored);
/* pattern containing slash gets FNM_PATHNAME so all slashes must match */
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "but/this/is/deep/and/ignored"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "this/is/not/deep"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "is/this/not/as/deep"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "this/is/deepish"));
cl_assert(!ignored);
cl_git_pass(git_status_should_ignore(&ignored, g_repo, "xthis/is/deep"));
cl_assert(!ignored);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment