Commit Diff


commit - c478f6d828a8e3d24ac5e4c70fcdd878aa334c7d
commit + 6841da0026bfd0be124721da859278eff7353c4f
blob - 400ac4c7f36af7a8a5a083c376506aebea7aabb7
blob + 1c0a14e01967511a718e3509c005cdb13ae0faab
--- got/got.1
+++ got/got.1
@@ -266,6 +266,22 @@ Changes created on top of staged changes are indicated
 .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 .
blob - 2db9fa27872d5e99c6cdd186e1a7baf24a6d9dc0
blob + 61c82088a8a2401eae516229118fdc49eac21432
--- lib/worktree.c
+++ 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 *
@@ -2330,6 +2332,130 @@ status_old(void *arg, struct got_fileindex_entry *ie, 
 		status = GOT_STATUS_DELETE;
 	return (*a->status_cb)(a->status_arg, status, get_staged_status(ie),
 	    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 *
@@ -2342,9 +2468,6 @@ status_new(void *arg, struct dirent *de, const char *p
 	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 *p
 		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 c
 		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)
blob - b77e1d3aed4da758cdd72648e410e4a105ebac25
blob + e4d71a97459fd79e1edfb492c0b395452e70e44a
--- regress/cmdline/status.sh
+++ regress/cmdline/status.sh
@@ -471,7 +471,40 @@ function test_status_many_paths {
 
 	(cd $testroot/wt && got status newdir alpha epsilon foo new beta . \
 		> $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"
+}
+
+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
@@ -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