Subject: Re: [PATCH 04/18] t1401-symbolic-ref: avoid direct filesystem access

From: SZEDER Gábor <[email-masked]>
Date: Fri, 23 Apr 2021 07:12:55 +0200

On Thu, Apr 22, 2021 at 03:01:01PM +0200, Han-Wen Nienhuys wrote:
> On Thu, Apr 22, 2021 at 6:59 AM SZEDER Gábor <[email-masked]> wrote:
> > > See the comment above the changed line: we don't want auto-detection
> > > to clobber the surrounding git repo.
> >
> > Indeed, but then this is not a faithful conversion of the original.
> > That 'echo' will write sane content to HEAD no matter what state the
> > repository is in.  That 'symbolic-ref' command, however, won't,
> > because 'git --git-dir .git' turns off only repository discovery, but
> > not repository verification, and in case of a corrupt '.git/HEAD' it
> > will bail out.
> >
> >   $ cd test
> >   $ git init
> >   Initialized empty Git repository in /home/szeder/src/git/test/.git/
> >   $ git commit --allow-empty -m initial
> >   [master (root-commit) ec0df0b] initial
> >   $ echo "foo bar baz" >.git/HEAD
> >   $ git --git-dir .git symbolic-ref HEAD refs/heads/master
> >   fatal: not a git repository: '.git'
> 
> But then it's working as intended, no? It will not corrupt the
> surrounding repository.

No, it definitely does not.

If one of the test cases fails because 'git symbolic-ref' were to
write bogus content to HEAD, then that new 'git symbolic-ref'
invocation in reset_to_sane() will not corrupt the surrounding
repository, but, crucially, it won't restore the test repository's
HEAD to a sane state either, and git commands invoked in subsequent
tests won't recognize the trash dir as their git repository, and will
operate on the surrounding repo instead:

  ~/src/git/t (master)$ vim t/t1401-symbolic-ref.sh
  ~/src/git/t (master *)$ git diff
  diff --git a/t/t1401-symbolic-ref.sh b/t/t1401-symbolic-ref.sh
  index a4ebb0b65f..8f8d93bf6a 100755
  --- a/t/t1401-symbolic-ref.sh
  +++ b/t/t1401-symbolic-ref.sh
  @@ -23,7 +23,11 @@ test_expect_success 'symbolic-ref reads HEAD' '
   '
   
   test_expect_success 'symbolic-ref refuses non-ref for HEAD' '
  -       test_must_fail git symbolic-ref HEAD foo
  +       #test_must_fail git symbolic-ref HEAD foo &&
  +       # Lets pretend that the above "git symbolic-ref" did write that
  +       # bogus content to HEAD:
  +       echo foo >.git/HEAD &&
  +       false
   '
   reset_to_sane
   
  ~/src/git/t (master *)$ ./t1401-symbolic-ref.sh 
  ok 1 - symbolic-ref writes HEAD
  ok 2 - symbolic-ref reads HEAD
  not ok 3 - symbolic-ref refuses non-ref for HEAD
  #	
  #		#test_must_fail git symbolic-ref HEAD foo &&
  #		# Lets pretend that the above "git symbolic-ref" did write that
  #		# bogus content to HEAD:
  #		echo foo >.git/HEAD &&
  #		false
  #	
  ok 4 - symbolic-ref refuses bare sha1
  ok 5 - HEAD cannot be removed
  ok 6 - symbolic-ref can be deleted
  ok 7 - symbolic-ref can delete dangling symref
  ok 8 - symbolic-ref fails to delete missing FOO
  ok 9 - symbolic-ref fails to delete real ref
  ok 10 - create large ref name
  ok 11 - symbolic-ref can point to large ref name
  ok 12 - we can parse long symbolic ref
  ok 13 - symbolic-ref reports failure in exit code
  ok 14 - symbolic-ref writes reflog entry
  ok 15 - symbolic-ref does not create ref d/f conflicts
  ok 16 - symbolic-ref can overwrite pointer to invalid name
  ok 17 - symbolic-ref can resolve d/f name (EISDIR)
  ok 18 - symbolic-ref can resolve d/f name (ENOTDIR)
  # failed 1 among 18 test(s)
  1..18
  ~/src/git/t (master *)$

OK, only one test failed, and the surrounding repo is not affected.

No lets switch to your 'git symbolic-ref command in reset_to_sane():

  ~/src/git/t (master *)$ vim t/t1401-symbolic-ref.sh
  ~/src/git/t (master *)$ git diff
  diff --git a/t/t1401-symbolic-ref.sh b/t/t1401-symbolic-ref.sh
  index a4ebb0b65f..6ef221d1bb 100755
  --- a/t/t1401-symbolic-ref.sh
  +++ b/t/t1401-symbolic-ref.sh
  @@ -7,7 +7,8 @@ test_description='basic symbolic-ref tests'
   # the git repo, meaning that further tests will operate on
   # the surrounding git repo instead of the trash directory.
   reset_to_sane() {
  -       echo ref: refs/heads/foo >.git/HEAD
  +       #echo ref: refs/heads/foo >.git/HEAD
  +       git --git-dir .git symbolic-ref HEAD refs/heads/foo
   }
   
   test_expect_success 'symbolic-ref writes HEAD' '
  @@ -23,7 +24,11 @@ test_expect_success 'symbolic-ref reads HEAD' '
   '
   
   test_expect_success 'symbolic-ref refuses non-ref for HEAD' '
  -       test_must_fail git symbolic-ref HEAD foo
  +       #test_must_fail git symbolic-ref HEAD foo &&
  +       # Lets pretend that the above "git symbolic-ref" did write that
  +       # bogus content to HEAD:
  +       echo foo >.git/HEAD &&
  +       false
   '
   reset_to_sane
 
  ~/src/git/t (master *)$ ./t1401-symbolic-ref.sh 
  ok 1 - symbolic-ref writes HEAD
  ok 2 - symbolic-ref reads HEAD
  not ok 3 - symbolic-ref refuses non-ref for HEAD
  #	
  #		#test_must_fail git symbolic-ref HEAD foo &&
  #		# Lets pretend that the above "git symbolic-ref" did write that
  #		# bogus content to HEAD:
  #		echo foo >.git/HEAD &&
  #		false
  #	
  fatal: not a git repository: '.git'
  not ok 4 - symbolic-ref refuses bare sha1
  #	
  #		echo content >file && git add file && git commit -m one &&
  #		test_must_fail git symbolic-ref HEAD $(git rev-parse HEAD)
  #	
  fatal: not a git repository: '.git'
  ok 5 - HEAD cannot be removed
  fatal: not a git repository: '.git'
  not ok 6 - symbolic-ref can be deleted
  #	
  #		git symbolic-ref NOTHEAD refs/heads/foo &&
  #		git symbolic-ref -d NOTHEAD &&
  #		test_path_is_file .git/refs/heads/foo &&
  #		test_path_is_missing .git/NOTHEAD
  #	
  fatal: not a git repository: '.git'
  ok 7 - symbolic-ref can delete dangling symref
  fatal: not a git repository: '.git'
  ok 8 - symbolic-ref fails to delete missing FOO
  fatal: not a git repository: '.git'
  not ok 9 - symbolic-ref fails to delete real ref
  #	
  #		echo "fatal: Cannot delete refs/heads/foo, not a symbolic ref" >expect &&
  #		test_must_fail git symbolic-ref -d refs/heads/foo >actual 2>&1 &&
  #		git rev-parse --verify refs/heads/foo &&
  #		test_cmp expect actual
  #	
  fatal: not a git repository: '.git'
  ok 10 - create large ref name
  ok 11 - symbolic-ref can point to large ref name
  ok 12 - we can parse long symbolic ref
  not ok 13 - symbolic-ref reports failure in exit code
  #	
  #		test_when_finished "rm -f .git/HEAD.lock" &&
  #		>.git/HEAD.lock &&
  #		test_must_fail git symbolic-ref HEAD refs/heads/whatever
  #	
  not ok 14 - symbolic-ref writes reflog entry
  #	
  #		git checkout -b log1 &&
  #		test_commit one &&
  #		git checkout -b log2  &&
  #		test_commit two &&
  #		git checkout --orphan orphan &&
  #		git symbolic-ref -m create HEAD refs/heads/log1 &&
  #		git symbolic-ref -m update HEAD refs/heads/log2 &&
  #		cat >expect <<-\EOF &&
  #		update
  #		create
  #		EOF
  #		git log --format=%gs -g -2 >actual &&
  #		test_cmp expect actual
  #	
  not ok 15 - symbolic-ref does not create ref d/f conflicts
  #	
  #		git checkout -b df &&
  #		test_commit df &&
  #		test_must_fail git symbolic-ref refs/heads/df/conflict refs/heads/df &&
  #		git pack-refs --all --prune &&
  #		test_must_fail git symbolic-ref refs/heads/df/conflict refs/heads/df
  #	
  not ok 16 - symbolic-ref can overwrite pointer to invalid name
  #	
  #		test_when_finished reset_to_sane &&
  #		head=$(git rev-parse HEAD) &&
  #		git symbolic-ref HEAD refs/heads/outer &&
  #		test_when_finished "git update-ref -d refs/heads/outer/inner" &&
  #		git update-ref refs/heads/outer/inner $head &&
  #		git symbolic-ref HEAD refs/heads/unrelated
  #	
  not ok 17 - symbolic-ref can resolve d/f name (EISDIR)
  #	
  #		test_when_finished reset_to_sane &&
  #		head=$(git rev-parse HEAD) &&
  #		git symbolic-ref HEAD refs/heads/outer/inner &&
  #		test_when_finished "git update-ref -d refs/heads/outer" &&
  #		git update-ref refs/heads/outer $head &&
  #		echo refs/heads/outer/inner >expect &&
  #		git symbolic-ref HEAD >actual &&
  #		test_cmp expect actual
  #	
  not ok 18 - symbolic-ref can resolve d/f name (ENOTDIR)
  #	
  #		test_when_finished reset_to_sane &&
  #		head=$(git rev-parse HEAD) &&
  #		git symbolic-ref HEAD refs/heads/outer &&
  #		test_when_finished "git update-ref -d refs/heads/outer/inner" &&
  #		git update-ref refs/heads/outer/inner $head &&
  #		echo refs/heads/outer >expect &&
  #		git symbolic-ref HEAD >actual &&
  #		test_cmp expect actual
  #	
  # failed 10 among 18 test(s)
  1..18

Uh-oh, a lot more tests failes, and, much worse!, my git repo is now
on a different and orphaned branch:

  ~/src/git/t (df *+)$ git status 
  On branch df
  
  No commits yet
  
  Changes to be committed:
  [...]

> I see it as the test writer's job to clean up to the extent that
> git-symbolic-ref can reset to a sane state.

No, it's the job of whoever updates the cleanup routine to make sure
that the updated cleanup routine still works just as well as it did in
the past.

> We could reset back to a known state in a more drastic manner
> (extracting .git from a tar archive), but that could interfere with
> the test functions if they're not isolated from each other.