Subject: Re: [PATCH 0/1] quote: quote space

From: Jeff King <[email-masked]>
Date: Thu, 28 Mar 2024 07:40:38 -0400

On Thu, Mar 28, 2024 at 06:32:54AM -0400, Jeff King wrote:

> We package up the failed test output and trash directories for each run.
> You can find the one for this case here:
> 
>   https://github.com/git/git/actions/runs/8458842054/artifacts/1364695605
> 
> It is sometimes misleading because we don't run with "-i", so subsequent
> tests may stomp on things. But in this case the failing test is the last
> one. Unfortunately, I don't think it shows us much, because the state we
> tried to diff is removed by the test itself (both the funny dir and the
> index after we tried to add it).
> 
> So I don't know if we failed to even create "funny /" in the first
> place, if adding it to the index failed, or if the diff somehow failed.

I ran it again using https://github.com/mxschmitt/action-tmate to get an
interactive shell.

It looks like making the directory works fine:

  # mkdir "funny "
  # ls -ld f*
  drwxr-xr-x 1 runneradmin None 0 Mar 28 11:01 'funny '

Likewise making the file:

  # >"funny /empty"
  # ls -l f*/*
  -rw-r--r-- 1 runneradmin None 0 Mar 28 11:02 'funny /empty'

Adding it _seems_ to work, but nothing is put into the index:

  # git add funny\ /empty
  # git ls-files -s
  100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       empty

At first I thought we had somehow created an entry with the wrong
filename, but that is just the existing "empty" entry from the earlier
tests. If you "rm .git/index" and try again, you get an empty index.

Running it under a debugger, it looks like treat_leading_path() realizes
it needs to look at "funny /", which it then feeds to is_directory().
That calls stat(), which returns -1. Digging there it looks like we feed
the expected name to GetFileAttributesExW(), but it returns an error
(123?) which we don't match in the switch statement, and we declare it
ENOENT.

So I suspect this isn't a bug in Git so much as we are running afoul of
OS limitations. And that is corroborated by these:

  https://superuser.com/questions/1733673/how-to-determine-if-a-file-with-a-trailing-space-exists

  https://stackoverflow.com/questions/48439697/trailing-whitespace-in-filename

There's some Win32 API magic you can do by prepending "\\?\", but I
couldn't get it to do anything useful.  Curiously, asking Git to
traverse itself yields another failure mode:

  # git add "funny "
  error: open("funny /empty"): No such file or directory
  error: unable to index file 'funny /empty'
  fatal: adding files failed

I'm way over my head here in terms of Windows quirks, so I'll stop
digging and assume it's not worth trying to make this work.

The patch you showed earlier functions as a workaround. But I think we
could also skip the filesystem entirely, since what we care about is
parsing the patch itself. Something like this:

diff --git a/t/t4126-apply-empty.sh b/t/t4126-apply-empty.sh
index eaf0c5304a..003b117362 100755
--- a/t/t4126-apply-empty.sh
+++ b/t/t4126-apply-empty.sh
@@ -67,25 +67,23 @@ test_expect_success 'apply --index create' '
 '
 
 test_expect_success 'apply with no-contents and a funny pathname' '
-	mkdir "funny " &&
-	>"funny /empty" &&
-	git add "funny /empty" &&
-	git diff HEAD "funny /" >sample.patch &&
-	git diff -R HEAD "funny /" >elpmas.patch &&
-	git reset --hard &&
-	rm -fr "funny " &&
+	blob=$(git rev-parse HEAD:empty) &&
+	git update-index --add --cacheinfo 100644,$blob,"funny /empty" &&
+	git diff --cached HEAD -- "funny /" >sample.patch &&
+	git diff --cached -R HEAD -- "funny /" >elpmas.patch &&
+	git reset &&
 
-	git apply --stat --check --apply sample.patch &&
-	test_must_be_empty "funny /empty" &&
+	git apply --cached --stat --check --apply sample.patch &&
+	git rev-parse --verify ":funny /empty" &&
 
-	git apply --stat --check --apply elpmas.patch &&
-	test_path_is_missing "funny /empty" &&
+	git apply --cached --stat --check --apply elpmas.patch &&
+	test_must_fail git rev-parse --verify ":funny /empty" &&
 
-	git apply -R --stat --check --apply elpmas.patch &&
-	test_must_be_empty "funny /empty" &&
+	git apply --cached -R --stat --check --apply elpmas.patch &&
+	git rev-parse --verify ":funny /empty" &&
 
-	git apply -R --stat --check --apply sample.patch &&
-	test_path_is_missing "funny /empty"
+	git apply --cached -R --stat --check --apply sample.patch &&
+	test_must_fail git rev-parse --verify ":funny /empty"
 '
 
 test_done

-Peff