Commit 6841da0026bfd0be124721da859278eff7353c4f

Stefan Sperling 2019-08-08T23:53:17

add support for .cvsignore files, as found in /usr/ports, to 'got status'

diff --git a/got/got.1 b/got/got.1
index 400ac4c..1c0a14e 100644
--- a/got/got.1
+++ b/got/got.1
@@ -266,6 +266,22 @@ Changes created on top of staged changes are indicated in the first column:
 .It MM Ta file was modified after earlier changes have been staged
 .It MA Ta file was modified after having been staged for addition
 .El
+.Pp
+For compatibility with
+.Xr cvs 1 ,
+.Cm got status
+parses
+.Pa .cvsignore
+files in each traversed directory and will not display unversioned files
+which match
+.Xr glob 7
+ignore patterns contained in
+.Pa .cvsignore
+files.
+Unlike
+.Xr cvs 1 ,
+.Cm got status
+only supports a single ignore pattern per line.
 .It Cm st
 Short alias for
 .Cm status .
diff --git a/lib/worktree.c b/lib/worktree.c
index 2db9fa2..61c8208 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -2235,6 +2235,8 @@ struct diff_dir_cb_arg {
     void *status_arg;
     got_worktree_cancel_cb cancel_cb;
     void *cancel_arg;
+    /* A pathlist containing per-directory pathlists of ignore patterns. */
+    struct got_pathlist_head ignores;
 };
 
 static const struct got_error *
@@ -2332,6 +2334,130 @@ status_old(void *arg, struct got_fileindex_entry *ie, const char *parent_path)
 	    ie->path, &blob_id, NULL, &commit_id);
 }
 
+void
+free_ignorelist(struct got_pathlist_head *ignorelist)
+{
+	struct got_pathlist_entry *pe;
+
+	TAILQ_FOREACH(pe, ignorelist, entry)
+		free((char *)pe->path);
+	got_pathlist_free(ignorelist);
+}
+
+void
+free_ignores(struct got_pathlist_head *ignores)
+{
+	struct got_pathlist_entry *pe;
+
+	TAILQ_FOREACH(pe, ignores, entry) {
+		struct got_pathlist_head *ignorelist = pe->data;
+		free_ignorelist(ignorelist);
+		free((char *)pe->path);
+	}
+	got_pathlist_free(ignores);
+}
+
+static const struct got_error *
+read_ignores(struct got_pathlist_head *ignores, const char *path, FILE *f)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *pe = NULL;
+	struct got_pathlist_head *ignorelist;
+	char *line = NULL, *pattern, *dirpath;
+	size_t linesize = 0;
+	ssize_t linelen;
+
+	ignorelist = calloc(1, sizeof(*ignorelist));
+	if (ignorelist == NULL)
+		return got_error_from_errno("calloc");
+	TAILQ_INIT(ignorelist);
+
+	while ((linelen = getline(&line, &linesize, f)) != -1) {
+		if (linelen > 0 && line[linelen - 1] == '\n')
+			line[linelen - 1] = '\0';
+		if (asprintf(&pattern, "%s%s%s", path, path[0] ? "/" : "",
+		    line) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		err = got_pathlist_insert(NULL, ignorelist, pattern, NULL);
+		if (err)
+			goto done;
+	}
+	if (ferror(f)) {
+		err = got_error_from_errno("getline");
+		goto done;
+	}
+
+	dirpath = strdup(path);
+	if (dirpath == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+	err = got_pathlist_insert(&pe, ignores, dirpath, ignorelist);
+done:
+	free(line);
+	if (err || pe == NULL) {
+		free(dirpath);
+		free_ignorelist(ignorelist);
+	}
+	return err;
+}
+
+int
+match_ignores(struct got_pathlist_head *ignores, const char *path)
+{
+	struct got_pathlist_entry *pe;
+
+	/*
+	 * The ignores pathlist contains ignore lists from children before
+	 * parents, so we can find the most specific ignorelist by walking
+	 * ignores backwards.
+	 */
+	pe = TAILQ_LAST(ignores, got_pathlist_head);
+	while (pe) {
+		if (got_path_is_child(path, pe->path, pe->path_len)) {
+			struct got_pathlist_head *ignorelist = pe->data;
+			struct got_pathlist_entry *pi;
+			TAILQ_FOREACH(pi, ignorelist, entry) {
+				if (fnmatch(pi->path, path,
+				    FNM_PATHNAME | FNM_LEADING_DIR))
+					continue;
+				return 1;
+			}
+		}
+		pe = TAILQ_PREV(pe, got_pathlist_head, entry);
+	}
+
+	return 0;
+}
+
+static const struct got_error *
+add_ignores(struct got_pathlist_head *ignores, const char *path)
+{
+	const struct got_error *err = NULL;
+	char *ignorespath;
+	FILE *ignoresfile = NULL;
+
+	/* TODO: read .gitignores as well... */
+	if (asprintf(&ignorespath, "%s%s.cvsignore", path, path[0] ? "/" : "")
+	    == -1)
+		return got_error_from_errno("asprintf");
+
+	ignoresfile = fopen(ignorespath, "r");
+	if (ignoresfile == NULL) {
+		if (errno != ENOENT)
+			err = got_error_from_errno2("fopen",
+			    ignorespath);
+	} else
+		err = read_ignores(ignores, path, ignoresfile);
+
+	if (ignoresfile && fclose(ignoresfile) == EOF && err == NULL)
+		err = got_error_from_errno2("flose", path);
+	free(ignorespath);
+	return err;
+}
+
 static const struct got_error *
 status_new(void *arg, struct dirent *de, const char *parent_path)
 {
@@ -2342,9 +2468,6 @@ status_new(void *arg, struct dirent *de, const char *parent_path)
 	if (a->cancel_cb && a->cancel_cb(a->cancel_arg))
 		return got_error(GOT_ERR_CANCELLED);
 
-	if (de->d_type == DT_DIR)
-		return NULL;
-
 	/* XXX ignore symlinks for now */
 	if (de->d_type == DT_LNK)
 		return NULL;
@@ -2356,7 +2479,10 @@ status_new(void *arg, struct dirent *de, const char *parent_path)
 		path = de->d_name;
 	}
 
-	if (got_path_is_child(path, a->status_path, a->status_path_len))
+	if (de->d_type == DT_DIR)
+		err = add_ignores(&a->ignores, path);
+	else if (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);
 	if (parent_path[0])
@@ -2426,8 +2552,12 @@ worktree_status(struct got_worktree *worktree, const char *path,
 		arg.status_arg = status_arg;
 		arg.cancel_cb = cancel_cb;
 		arg.cancel_arg = cancel_arg;
-		err = got_fileindex_diff_dir(fileindex, workdir,
-		    worktree->root_path, path, repo, &fdiff_cb, &arg);
+		TAILQ_INIT(&arg.ignores);
+		err = add_ignores(&arg.ignores, "");
+		if (err == NULL)
+			err = got_fileindex_diff_dir(fileindex, workdir,
+			    worktree->root_path, path, repo, &fdiff_cb, &arg);
+		free_ignores(&arg.ignores);
 	}
 
 	if (workdir)
diff --git a/regress/cmdline/status.sh b/regress/cmdline/status.sh
index b77e1d3..e4d71a9 100755
--- a/regress/cmdline/status.sh
+++ b/regress/cmdline/status.sh
@@ -480,6 +480,39 @@ function test_status_many_paths {
 	test_done "$testroot" "$ret"
 }
 
+function test_status_cvsignore {
+	local testroot=`test_init status_cvsignore`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "unversioned file" > $testroot/wt/foo
+	echo "unversioned file" > $testroot/wt/foop
+	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
+	echo "bar" > $testroot/wt/epsilon/.cvsignore
+	echo "moo" >> $testroot/wt/epsilon/.cvsignore
+
+	echo '?  .cvsignore' > $testroot/stdout.expected
+	echo '?  epsilon/.cvsignore' >> $testroot/stdout.expected
+	echo '?  epsilon/boo' >> $testroot/stdout.expected
+	echo '?  foop' >> $testroot/stdout.expected
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_status_basic
 run_test test_status_subdir_no_mods
 run_test test_status_subdir_no_mods2
@@ -492,3 +525,4 @@ run_test test_status_shows_conflict
 run_test test_status_empty_dir
 run_test test_status_empty_dir_unversioned_file
 run_test test_status_many_paths
+run_test test_status_cvsignore