"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Renato Aguiar <renato@renatoaguiar.net>
Subject:
Re: fix .{cvs,git}ignore handling
To:
Mikhail Pchelin <misha@freebsd.org>
Cc:
gameoftrees@openbsd.org
Date:
Thu, 13 Aug 2026 12:09:55 -0700

Download raw body.

Thread
On Thu, Aug 13 2026, Stefan Sperling wrote:

> On Sat, Aug 08, 2026 at 06:02:01PM +0300, Mikhail Pchelin wrote:
>> If both .cvsignore and .gitignore files exist in a directory, only
>> .cvsignore will be respected and ignores from the .gitignore will be
>> dropped, inlined patch fixes that, both files are respected now, the
>> test case is included.
>
> The intention was to merge patterns from both files into a single
> ignores list. Your regression test is good, but the fix isn't quite
> right. Instead of using two lists, I think read_ignores() needs to be
> changed such that it will append to the existing list if a list already
> exists for the given path. Do you agree?
>  

I've been working on a fix for this as well, and that was the direction
I was going. I don't think it is ready for review/commit yet, but since
you are also working on the same thing I though of sharing what I have
so far:

diff refs/remotes/origin/main refs/heads/fix-multiple-ignore-files
commit - ae25db3f82d89c28505e5fc1f58b8ba693282f5b
commit + 2b2810911a91a7239769f9ae40f67be81bfebe79
blob - 8232df1c967a331ed394daaa474c7547e2c93fda
blob + 973b0b7690b7d8a2af6816535babe785ebf5653a
--- include/got_path.h
+++ include/got_path.h
@@ -74,6 +74,15 @@ RB_HEAD(got_pathlist_head, got_pathlist_entry);
 RB_PROTOTYPE(got_pathlist_head, got_pathlist_entry, entry, got_pathlist_cmp);
 
 /*
+ * Find element in list of paths. Return 1 if element is found, 0 otherwise.
+ * When first argument is not NULL, set it to a pointer to the found element, or
+ * to a NULL pointer in case the element is not found in the list.
+ */
+int
+got_pathlist_find(struct got_pathlist_entry **,
+		  struct got_pathlist_head *, const char *);
+
+/*
  * Insert a path into the list of paths in a predictable order.
  * The caller should already have initialized the list head. This list stores
  * the pointer to the path as-is, i.e. the path is not copied internally and
blob - b43f45fcda4659959a1775f2a328234862001f60
blob + bfb8711b78840ab74356661b42faceec5e262b22
--- lib/path.c
+++ lib/path.c
@@ -245,6 +245,26 @@ got_pathlist_insert(struct got_pathlist_entry **insert
 	return NULL;
 }
 
+int
+got_pathlist_find(struct got_pathlist_entry **elem,
+    struct got_pathlist_head *pathlist, const char *path)
+{
+	struct got_pathlist_entry pe_query;
+	struct got_pathlist_entry *pe;
+
+	pe_query.path = path;
+	pe_query.path_len = strlen(path);
+	pe = RB_FIND(got_pathlist_head, pathlist, &pe_query);
+
+	if (elem)
+		*elem = pe;
+
+	if (pe == NULL)
+		return 0;
+
+	return 1;
+}
+
 void
 got_pathlist_free(struct got_pathlist_head *pathlist, int freemask)
 {
blob - fca04b3b62b7ecd7035e827c334fa35ad977b7a3
blob + d172b0b0a2eadf9c78b2fc631fca49b292ffdfc2
--- lib/worktree.c
+++ lib/worktree.c
@@ -3795,16 +3795,35 @@ read_ignores(struct got_pathlist_head *ignores, const 
 {
 	const struct got_error *err = NULL;
 	struct got_pathlist_entry *pe = NULL;
-	struct got_pathlist_head *ignorelist;
+	struct got_pathlist_head *ignorelist = NULL;
 	char *line = NULL, *pattern, *dirpath = NULL;
 	size_t linesize = 0;
 	ssize_t linelen;
 
-	ignorelist = calloc(1, sizeof(*ignorelist));
-	if (ignorelist == NULL)
-		return got_error_from_errno("calloc");
-	RB_INIT(ignorelist);
+	got_pathlist_find(&pe, ignores, path);
 
+	if (pe == NULL) {
+		ignorelist = calloc(1, sizeof(*ignorelist));
+		if (ignorelist == NULL) {
+			err = got_error_from_errno("calloc");
+			goto done;
+		}
+		RB_INIT(ignorelist);
+		
+		dirpath = strdup(path);
+		if (dirpath == NULL) {
+			err = got_error_from_errno("strdup");
+			goto done;
+		}
+		
+		err = got_pathlist_insert(&pe, ignores, dirpath, ignorelist);
+		if (err || pe == NULL)
+			goto done;
+
+		dirpath = NULL;
+		ignorelist = NULL;
+	}
+
 	while ((linelen = getline(&line, &linesize, f)) != -1) {
 		if (linelen > 0 && line[linelen - 1] == '\n')
 			line[linelen - 1] = '\0';
@@ -3826,7 +3845,7 @@ read_ignores(struct got_pathlist_head *ignores, const 
 			err = got_error_from_errno("asprintf");
 			goto done;
 		}
-		err = got_pathlist_insert(NULL, ignorelist, pattern, NULL);
+		err = got_pathlist_insert(NULL, pe->data, pattern, NULL);
 		if (err)
 			goto done;
 	}
@@ -3835,16 +3854,12 @@ read_ignores(struct got_pathlist_head *ignores, const 
 		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) {
+	if (line != NULL)
+		free(line);
+	if (dirpath != NULL)
 		free(dirpath);
+	if (ignorelist != NULL) {
 		got_pathlist_free(ignorelist, GOT_PATHLIST_FREE_PATH);
 		free(ignorelist);
 	}
@@ -3981,9 +3996,9 @@ add_ignores(struct got_pathlist_head *ignores, const c
 	}
 
 	if (ignoresfile && fclose(ignoresfile) == EOF && err == NULL)
-		err = got_error_from_errno2("fclose", path);
+		err = got_error_from_errno2("fclose", ignorespath);
 	if (fd != -1 && close(fd) == -1 && err == NULL)
-		err = got_error_from_errno2("close", path);
+		err = got_error_from_errno2("close", ignorespath);
 	free(ignorespath);
 	return err;
 }
blob - 0874b88152ef2725e3b0e312b01cda5d76c37b91
blob + f2e12edf0a7afe18e462832a82b33f8bb46615f4
--- regress/cmdline/status.sh
+++ regress/cmdline/status.sh
@@ -717,6 +717,36 @@ EOF
 	test_done "$testroot" "$ret"
 }
 
+test_status_gitignore_and_cvsignore() {
+	local testroot=`test_init status_gitignore_and_cvsignore`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "cvs would ignore this" > $testroot/wt/baz
+	mkdir -p $testroot/wt/foo
+	echo "git would ignore this" > $testroot/wt/foo/bar
+	mkdir -p $testroot/wt/boo/foo
+	echo "git would ignore this" > $testroot/wt/boo/foo/bar
+	echo "baz" > $testroot/wt/.cvsignore
+	echo "**/foo" > $testroot/wt/.gitignore
+
+	echo '?  .cvsignore' > $testroot/stdout.expected
+	echo '?  .gitignore' >> $testroot/stdout.expected
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_status_gitignore_leading_slashes() {
 	local testroot=`test_init status_gitignore_leading_slashes`
 
@@ -1243,6 +1273,7 @@ run_test test_status_empty_dir_unversioned_file
 run_test test_status_many_paths
 run_test test_status_cvsignore
 run_test test_status_gitignore
+run_test test_status_gitignore_and_cvsignore
 run_test test_status_gitignore_leading_slashes
 run_test test_status_gitignore_trailing_slashes
 run_test test_status_gitignore_comments