Commit 3143d852d788e42c45a61252acb935a698efed2f

Stefan Sperling 2020-06-25T06:53:54

fix ignores when a path is passed to 'got status' Problem reported by semarie, who also provided initial regression test code. ok semarie

diff --git a/lib/fileindex.c b/lib/fileindex.c
index f2f5a3c..6566855 100644
--- a/lib/fileindex.c
+++ b/lib/fileindex.c
@@ -1003,6 +1003,12 @@ diff_fileindex_dir(struct got_fileindex *fileindex,
 	size_t path_len = strlen(path);
 	struct got_pathlist_entry *dle;
 
+	if (cb->diff_traverse) {
+		err = cb->diff_traverse(cb_arg, path, dirfd);
+		if (err)
+			return err;
+	}
+
 	dle = TAILQ_FIRST(dirlist);
 	while ((*ie && got_path_is_child((*ie)->path, path, path_len)) || dle) {
 		if (dle && *ie) {
diff --git a/lib/got_lib_fileindex.h b/lib/got_lib_fileindex.h
index 88f07ec..aff771b 100644
--- a/lib/got_lib_fileindex.h
+++ b/lib/got_lib_fileindex.h
@@ -144,10 +144,13 @@ typedef const struct got_error *(*got_fileindex_diff_dir_old_cb)(void *,
     struct got_fileindex_entry *, const char *);
 typedef const struct got_error *(*got_fileindex_diff_dir_new_cb)(void *,
     struct dirent *, const char *, int);
+typedef const struct got_error *(*got_fileindex_diff_dir_traverse)(void *,
+    const char *, int);
 struct got_fileindex_diff_dir_cb {
 	got_fileindex_diff_dir_old_new_cb diff_old_new;
 	got_fileindex_diff_dir_old_cb diff_old;
 	got_fileindex_diff_dir_new_cb diff_new;
+	got_fileindex_diff_dir_traverse diff_traverse;
 };
 const struct got_error *got_fileindex_diff_dir(struct got_fileindex *, int,
     const char *, const char *, struct got_repository *,
diff --git a/lib/worktree.c b/lib/worktree.c
index a0cbc48..82a6b4b 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -2381,6 +2381,7 @@ struct diff_dir_cb_arg {
     /* A pathlist containing per-directory pathlists of ignore patterns. */
     struct got_pathlist_head ignores;
     int report_unchanged;
+    int no_ignores;
 };
 
 static const struct got_error *
@@ -2684,23 +2685,8 @@ status_new(void *arg, struct dirent *de, const char *parent_path, int dirfd)
 		path = de->d_name;
 	}
 
-	if (de->d_type == DT_DIR) {
-		int subdirfd = openat(dirfd, de->d_name,
-		    O_RDONLY | O_NOFOLLOW | O_DIRECTORY);
-		if (subdirfd == -1) {
-			if (errno != ENOENT && errno != EACCES)
-				err = got_error_from_errno2("openat", path);
-		} else {
-			err = add_ignores(&a->ignores, a->worktree->root_path,
-			    path, subdirfd, ".cvsignore");
-			if (err == NULL)
-				err = add_ignores(&a->ignores,
-				    a->worktree->root_path, path,
-				    subdirfd, ".gitignore");
-			if (close(subdirfd) == -1 && err == NULL)
-				err = got_error_from_errno2("close", path);
-		}
-	} else if (got_path_is_child(path, a->status_path, a->status_path_len)
+	if (de->d_type != DT_DIR &&
+	    got_path_is_child(path, a->status_path, a->status_path_len)
 	    && !match_ignores(&a->ignores, path))
 		err = (*a->status_cb)(a->status_arg, GOT_STATUS_UNVERSIONED,
 		    GOT_STATUS_NO_CHANGE, path, NULL, NULL, NULL, -1, NULL);
@@ -2710,6 +2696,26 @@ status_new(void *arg, struct dirent *de, const char *parent_path, int dirfd)
 }
 
 static const struct got_error *
+status_traverse(void *arg, const char *path, int dirfd)
+{
+	const struct got_error *err = NULL;
+	struct diff_dir_cb_arg *a = arg;
+
+	if (a->no_ignores)
+		return NULL;
+
+	err = add_ignores(&a->ignores, a->worktree->root_path,
+	    path, dirfd, ".cvsignore");
+	if (err)
+		return err;
+
+	err = add_ignores(&a->ignores, a->worktree->root_path, path,
+	    dirfd, ".gitignore");
+
+	return err;
+}
+
+static const struct got_error *
 report_single_file_status(const char *path, const char *ondisk_path,
 struct got_fileindex *fileindex, got_worktree_status_cb status_cb,
 void *status_arg, struct got_repository *repo, int report_unchanged)
@@ -2738,6 +2744,50 @@ void *status_arg, struct got_repository *repo, int report_unchanged)
 }
 
 static const struct got_error *
+add_ignores_from_parent_paths(struct got_pathlist_head *ignores,
+    const char *root_path, const char *path)
+{
+	const struct got_error *err;
+	char *parent_path, *next_parent_path;
+
+	err = add_ignores(ignores, root_path, "", -1,
+	    ".cvsignore");
+	if (err)
+		return err;
+
+	err = add_ignores(ignores, root_path, "", -1,
+	    ".gitignore");
+	if (err)
+		return err;
+
+	err = got_path_dirname(&parent_path, path);
+	if (err) {
+		if (err->code == GOT_ERR_BAD_PATH)
+			return NULL; /* cannot traverse parent */
+		return err;
+	}
+	for (;;) {
+		err = add_ignores(ignores, root_path, parent_path, -1,
+		    ".cvsignore");
+		if (err)
+			break;
+		err = add_ignores(ignores, root_path, parent_path, -1,
+		    ".gitignore");
+		if (err)
+			break;
+		err = got_path_dirname(&next_parent_path, parent_path);
+		if (err) {
+			if (err->code != GOT_ERR_BAD_PATH)
+				return err;
+			err = NULL; /* traversed everything */
+			break;
+		}
+	}
+
+	return err;
+}
+
+static const struct got_error *
 worktree_status(struct got_worktree *worktree, const char *path,
     struct got_fileindex *fileindex, struct got_repository *repo,
     got_worktree_status_cb status_cb, void *status_arg,
@@ -2750,6 +2800,8 @@ worktree_status(struct got_worktree *worktree, const char *path,
 	struct diff_dir_cb_arg arg;
 	char *ondisk_path = NULL;
 
+	TAILQ_INIT(&arg.ignores);
+
 	if (asprintf(&ondisk_path, "%s%s%s",
 	    worktree->root_path, path[0] ? "/" : "", path) == -1)
 		return got_error_from_errno("asprintf");
@@ -2766,6 +2818,7 @@ worktree_status(struct got_worktree *worktree, const char *path,
 		fdiff_cb.diff_old_new = status_old_new;
 		fdiff_cb.diff_old = status_old;
 		fdiff_cb.diff_new = status_new;
+		fdiff_cb.diff_traverse = status_traverse;
 		arg.fileindex = fileindex;
 		arg.worktree = worktree;
 		arg.status_path = path;
@@ -2776,21 +2829,18 @@ worktree_status(struct got_worktree *worktree, const char *path,
 		arg.cancel_cb = cancel_cb;
 		arg.cancel_arg = cancel_arg;
 		arg.report_unchanged = report_unchanged;
-		TAILQ_INIT(&arg.ignores);
+		arg.no_ignores = no_ignores;
 		if (!no_ignores) {
-			err = add_ignores(&arg.ignores, worktree->root_path,
-			    path, fd, ".cvsignore");
-			if (err == NULL)
-				err = add_ignores(&arg.ignores,
-				    worktree->root_path, path, fd,
-				    ".gitignore");
+			err = add_ignores_from_parent_paths(&arg.ignores,
+			    worktree->root_path, path);
+			if (err)
+				goto done;
 		}
-		if (err == NULL)
-			err = got_fileindex_diff_dir(fileindex, fd,
-			    worktree->root_path, path, repo, &fdiff_cb, &arg);
-		free_ignores(&arg.ignores);
+		err = got_fileindex_diff_dir(fileindex, fd,
+		    worktree->root_path, path, repo, &fdiff_cb, &arg);
 	}
-
+done:
+	free_ignores(&arg.ignores);
 	if (fd != -1 && close(fd) != 0 && err == NULL)
 		err = got_error_from_errno("close");
 	free(ondisk_path);
diff --git a/regress/cmdline/status.sh b/regress/cmdline/status.sh
index 5abbce9..bf27874 100755
--- a/regress/cmdline/status.sh
+++ b/regress/cmdline/status.sh
@@ -496,10 +496,13 @@ function test_status_cvsignore {
 
 	echo "unversioned file" > $testroot/wt/foo
 	echo "unversioned file" > $testroot/wt/foop
+	echo "unversioned file" > $testroot/wt/epsilon/foo
 	echo "unversioned file" > $testroot/wt/epsilon/bar
 	echo "unversioned file" > $testroot/wt/epsilon/boo
 	echo "unversioned file" > $testroot/wt/epsilon/moo
-	echo "foo" > $testroot/wt/.cvsignore
+	mkdir -p $testroot/wt/epsilon/new/
+	echo "unversioned file" > $testroot/wt/epsilon/new/foo
+	echo "**/foo" > $testroot/wt/.cvsignore
 	echo "bar" > $testroot/wt/epsilon/.cvsignore
 	echo "moo" >> $testroot/wt/epsilon/.cvsignore
 
@@ -517,6 +520,29 @@ function test_status_cvsignore {
 		return 1
 	fi
 
+	echo '?  epsilon/.cvsignore' > $testroot/stdout.expected
+	echo '?  epsilon/boo' >> $testroot/stdout.expected
+	(cd $testroot/wt && got status epsilon > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+		
+	echo -n '' > $testroot/stdout.expected
+	(cd $testroot/wt && got status epsilon/new > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
 	echo '?  .cvsignore' > $testroot/stdout.expected
 	echo '?  epsilon/.cvsignore' >> $testroot/stdout.expected
 	echo '?  epsilon/boo' >> $testroot/stdout.expected