Commit bd8de4305a32b69e3b4d44c9785663889a5d9eff

Stefan Sperling 2019-10-04T14:51:33

make 'got status' read .gitignore files; support **/ and /**/ in patterns

diff --git a/got/got.1 b/got/got.1
index 263cc8d..87b6cff 100644
--- a/got/got.1
+++ b/got/got.1
@@ -279,20 +279,37 @@ Changes created on top of staged changes are indicated in the first column:
 .El
 .Pp
 For compatibility with
-.Xr cvs 1 ,
+.Xr cvs 1
+and
+.Xr git 1 ,
 .Cm got status
-parses
+reads
+.Xr glob 7
+patterns from
 .Pa .cvsignore
+and
+.Pa .gitignore
 files in each traversed directory and will not display unversioned files
-which match
+which match these patterns.
+As an extension to
 .Xr glob 7
-ignore patterns contained in
-.Pa .cvsignore
-files.
+matching rules,
+.Cm got status
+supports consecutive asterisks,
+.Dq ** ,
+which will match an arbitrary amount of directories.
 Unlike
 .Xr cvs 1 ,
 .Cm got status
 only supports a single ignore pattern per line.
+Unlike
+.Xr git 1 ,
+.Cm got status
+does not support negated ignore patterns prefixed with
+.Dq \&! ,
+and gives no special significance to the location of path component separators,
+.Dq / ,
+in a pattern.
 .It Cm st
 Short alias for
 .Cm status .
diff --git a/lib/worktree.c b/lib/worktree.c
index 729abd3..d7dc122 100644
--- a/lib/worktree.c
+++ b/lib/worktree.c
@@ -2382,6 +2382,15 @@ read_ignores(struct got_pathlist_head *ignores, const char *path, FILE *f)
 	while ((linelen = getline(&line, &linesize, f)) != -1) {
 		if (linelen > 0 && line[linelen - 1] == '\n')
 			line[linelen - 1] = '\0';
+
+		/* Git's ignores may contain comments. */
+		if (line[0] == '#')
+			continue;
+
+		/* Git's negated patterns are not (yet?) supported. */
+		if (line[0] == '!')
+			continue;
+
 		if (asprintf(&pattern, "%s%s%s", path, path[0] ? "/" : "",
 		    line) == -1) {
 			err = got_error_from_errno("asprintf");
@@ -2416,6 +2425,33 @@ match_ignores(struct got_pathlist_head *ignores, const char *path)
 {
 	struct got_pathlist_entry *pe;
 
+	/* Handle patterns which match in all directories. */
+	TAILQ_FOREACH(pe, ignores, entry) {
+		struct got_pathlist_head *ignorelist = pe->data;
+		struct got_pathlist_entry *pi;
+
+		TAILQ_FOREACH(pi, ignorelist, entry) {
+			const char *p, *pattern = pi->path;
+
+			if (strncmp(pattern, "**/", 3) != 0)
+				continue;
+			pattern += 3;
+			p = path;
+			while (*p) {
+				if (fnmatch(pattern, p,
+				    FNM_PATHNAME | FNM_LEADING_DIR)) {
+					/* Retry in next directory. */
+					while (*p && *p != '/')
+						p++;
+					while (*p == '/')
+						p++;
+					continue;
+				}
+				return 1;
+			}
+		}
+	}
+
 	/*
 	 * The ignores pathlist contains ignore lists from children before
 	 * parents, so we can find the most specific ignorelist by walking
@@ -2427,8 +2463,11 @@ match_ignores(struct got_pathlist_head *ignores, const char *path)
 			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))
+				const char *pattern = pi->path;
+				int flags = FNM_LEADING_DIR;
+				if (strstr(pattern, "/**/") == NULL)
+					flags |= FNM_PATHNAME;
+				if (fnmatch(pattern, path, flags))
 					continue;
 				return 1;
 			}
@@ -2441,15 +2480,14 @@ match_ignores(struct got_pathlist_head *ignores, const char *path)
 
 static const struct got_error *
 add_ignores(struct got_pathlist_head *ignores, const char *root_path,
-    const char *path)
+    const char *path, const char *ignores_filename)
 {
 	const struct got_error *err = NULL;
 	char *ignorespath;
 	FILE *ignoresfile = NULL;
 
-	/* TODO: read .gitignores as well... */
-	if (asprintf(&ignorespath, "%s/%s%s.cvsignore", root_path, path,
-	    path[0] ? "/" : "") == -1)
+	if (asprintf(&ignorespath, "%s/%s%s%s", root_path, path,
+	    path[0] ? "/" : "", ignores_filename) == -1)
 		return got_error_from_errno("asprintf");
 
 	ignoresfile = fopen(ignorespath, "r");
@@ -2487,8 +2525,13 @@ status_new(void *arg, struct dirent *de, const char *parent_path)
 		path = de->d_name;
 	}
 
-	if (de->d_type == DT_DIR)
-		err = add_ignores(&a->ignores, a->worktree->root_path, path);
+	if (de->d_type == DT_DIR) {
+		err = add_ignores(&a->ignores, a->worktree->root_path, path,
+		    ".cvsignore");
+		if (err == NULL)
+			err = add_ignores(&a->ignores, a->worktree->root_path,
+			    path, ".gitignore");
+	}
 	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,
@@ -2563,7 +2606,11 @@ worktree_status(struct got_worktree *worktree, const char *path,
 		arg.cancel_cb = cancel_cb;
 		arg.cancel_arg = cancel_arg;
 		TAILQ_INIT(&arg.ignores);
-		err = add_ignores(&arg.ignores, worktree->root_path, path);
+		err = add_ignores(&arg.ignores, worktree->root_path, path,
+		    ".cvsignore");
+		if (err == NULL)
+			err = add_ignores(&arg.ignores, worktree->root_path,
+			    path, ".gitignore");
 		if (err == NULL)
 			err = got_fileindex_diff_dir(fileindex, workdir,
 			    worktree->root_path, path, repo, &fdiff_cb, &arg);
diff --git a/regress/cmdline/status.sh b/regress/cmdline/status.sh
index 5fde224..c03418c 100755
--- a/regress/cmdline/status.sh
+++ b/regress/cmdline/status.sh
@@ -530,6 +530,55 @@ function test_status_cvsignore {
 	test_done "$testroot" "$ret"
 }
 
+function test_status_gitignore {
+	local testroot=`test_init status_gitignore`
+
+	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/barp
+	echo "unversioned file" > $testroot/wt/epsilon/bar
+	echo "unversioned file" > $testroot/wt/epsilon/boo
+	echo "unversioned file" > $testroot/wt/epsilon/moo
+	mkdir -p $testroot/wt/a/b/c/
+	echo "unversioned file" > $testroot/wt/a/b/c/foo
+	echo "unversioned file" > $testroot/wt/a/b/c/zoo
+	echo "foo" > $testroot/wt/.gitignore
+	echo "bar*" >> $testroot/wt/.gitignore
+	echo "epsilon/**" >> $testroot/wt/.gitignore
+	echo "a/**/foo" >> $testroot/wt/.gitignore
+	echo "**/zoo" >> $testroot/wt/.gitignore
+
+	echo '?  .gitignore' > $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
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo '?  .gitignore' > $testroot/stdout.expected
+	echo '?  foop' >> $testroot/stdout.expected
+	(cd $testroot/wt/gamma && 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
@@ -543,3 +592,4 @@ 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
+run_test test_status_gitignore