#!/bin/sh # The snapshot page hands a revision back as an archive. Every format is held # byte for byte to the archive git itself writes, once decompressed where cgit # pipes through a compressor, and each compressed format needs the same tool # on the server and here, so one missing tool skips that format on both # grounds at once. The rest covers how a snapshot is named and found again from # its name, with a repository of its own for each set of refs, the signatures # kept in notes, and the requests that are refused. test_description='Verify snapshot' . ./setup.sh check_snapshot_headers() { sed "/^$/q" tmp >headers && grep "^Status: 200 OK$" headers && grep "^Content-Type: $2$" headers && grep "^Content-Disposition: inline; filename=\"master.$1\"$" headers } check_snapshot_content() { ls master/ >output && test_line_count = 5 output && grep "^5$" master/file-5 && test_line_count = 1 master/file-5 } # Lists the members of the archive in the response held in tmp. snapshot_members() { strip_headers snapshot.tar && tar -tf snapshot.tar >members } # The format checks below compare against these, which git's own writer makes # from the commit the snapshots are taken of. test_expect_success 'set up the archives git writes' ' git -C repos/foo archive --format=tar --prefix=master/ master >expect.tar && git -C repos/foo archive --format=zip --prefix=master/ master >expect.zip ' test_expect_success 'a tar snapshot is the archive git writes' ' cgit_ok_url "foo/snapshot/master.tar" >tmp && check_snapshot_headers tar application/x-tar && strip_headers master.tar && test_cmp_bin expect.tar master.tar && rm -rf master && tar -xf master.tar && check_snapshot_content ' # Registers the check for one compressed tar format. test_tar_snapshot() { ext=$1 type=$2 tool=$3 prereq=$4 test_lazy_prereq $prereq "command -v $tool" test_expect_success $prereq "a $ext snapshot unpacks to the tar git writes" " cgit_ok_url 'foo/snapshot/master.$ext' >tmp && check_snapshot_headers $ext $type && strip_headers master.$ext && '$tool' -t 'master.$ext' && '$tool' -dc 'master.$ext' >master.tar && test_cmp_bin expect.tar master.tar " } test_tar_snapshot tar.gz application/gzip gzip GZIP test_tar_snapshot tar.bz2 application/x-bzip2 bzip2 BZIP2 test_tar_snapshot tar.lz application/x-lzip lzip LZIP test_tar_snapshot tar.xz application/x-xz xz XZ test_tar_snapshot tar.zst application/zstd zstd ZSTD test_expect_success 'a zip snapshot is the archive git writes' ' cgit_ok_url "foo/snapshot/master.zip" >tmp && check_snapshot_headers zip application/zip && strip_headers master.zip && test_cmp_bin expect.zip master.zip ' test_expect_success UNZIP 'a zip snapshot unzips' ' cgit_ok_url "foo/snapshot/master.zip" >tmp && strip_headers master.zip && rm -rf master && "$GIT_UNZIP" master.zip && check_snapshot_content ' # The tags sit on the older commit, so a name that led to master instead # would bring file-2 with it. On a case-insensitive filesystem a ref lookup # for one case finds a loose file of the other. v1.0 stays loose on purpose, # because deciding by lookups called every fresh tag ambiguous and kept its v # until git happened to pack the refs. The rest are packed, so that a lookup # for v2.0 cannot find V2.0. Reftable keeps no file per ref, so v1.0 is # looked for as a file only under the files backend. test_expect_success 'set up a repo with tags of every shape' ' mkrepo repos/vtags 2 && git -C repos/vtags update-ref refs/tags/V2.0 HEAD~1 && git -C repos/vtags update-ref refs/tags/vtags-3.0 HEAD~1 && git -C repos/vtags update-ref refs/tags/release/2.0 HEAD~1 && git -C repos/vtags update-ref refs/heads/topic/x HEAD~1 && git -C repos/vtags pack-refs --all && git -C repos/vtags update-ref refs/tags/v1.0 HEAD~1 && { ! test_have_prereq REFFILES || test_path_is_file repos/vtags/.git/refs/tags/v1.0 } && cgitrc_repo vtagsrc vtags snapshots=tar ' # A tag named v1.0 sheds its v in the snapshot name unless dropping it would # merge two tags, and the name still leads back to the tag. test_expect_success 'a loose v tag sheds its v and the name leads back to it' ' cgit_ok vtagsrc "url=vtags/refs/" >tmp && grep "vtags-1.0.tar" tmp && ! grep "vtags-v1.0.tar" tmp && cgit_ok vtagsrc "url=vtags/snapshot/vtags-1.0.tar" >tmp && snapshot_members && grep "^vtags-1.0/file-1$" members && ! grep "file-2" members ' test_expect_success 'an underscore after the prefix leads back to the tag too' ' cgit_ok vtagsrc "url=vtags/snapshot/vtags_1.0.tar" >tmp && snapshot_members && grep "^vtags_1.0/file-1$" members && ! grep "file-2" members ' test_expect_success 'a V tag sheds its V and the name leads back to it' ' cgit_ok vtagsrc "url=vtags/refs/" >tmp && grep "vtags-2.0.tar" tmp && cgit_ok vtagsrc "url=vtags/snapshot/vtags-2.0.tar" >tmp && snapshot_members && grep "^vtags-2.0/file-1$" members && ! grep "file-2" members ' test_expect_success 'a tag that starts with the prefix is not prefixed again' ' cgit_ok vtagsrc "url=vtags/refs/" >tmp && grep "vtags-3.0.tar" tmp && ! grep "vtags-vtags-3.0" tmp && cgit_ok vtagsrc "url=vtags/snapshot/vtags-3.0.tar" >tmp && snapshot_members && grep "^vtags-3.0/file-1$" members && ! grep "file-2" members ' test_expect_success 'a snapshot-prefix names the snapshots in its place' ' cgitrc_repo prefixrc vtags snapshots=tar repo.snapshot-prefix=proj && cgit_ok prefixrc "url=vtags/refs/" >tmp && grep "proj-1.0.tar" tmp && cgit_ok prefixrc "url=vtags/snapshot/proj-1.0.tar" >tmp && snapshot_members && grep "^proj-1.0/file-1$" members && ! grep "file-2" members ' test_expect_success 'snapshots=all offers every format' ' cgitrc_repo allrc vtags snapshots=all && cgit_ok allrc "url=vtags/refs/" >tmp && for suffix in tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip do grep ">vtags-1.0.$suffix" tmp || return 1 done ' # A snapshot name is never worked back to a ref holding a slash, so such a # ref is offered under a dashed name and passed along with it, even from a # page whose own head is that ref. The dashed name is no ref, so the archive # unpacks into the repository's own prefix. test_expect_success 'a ref with a slash is linked under a dashed name' ' cgit_ok vtagsrc "url=vtags/refs/" >tmp && grep "vtags-release-2.0.tar" tmp && cgit_ok vtagsrc "url=vtags/commit/&h=topic/x" >tmp && grep "vtags-topic-x.tar" tmp ' test_expect_success 'the dashed name serves the ref under the repository prefix' ' cgit_ok vtagsrc "url=vtags/snapshot/vtags-release-2.0.tar&id=release/2.0" >tmp && snapshot_members && commit=$(git -C repos/vtags get-tar-commit-id tmp && grep "^Status: 400" tmp && grep "
Unsupported snapshot format: vtags-1.0.zip
" tmp && cgit_rc vtagsrc "url=vtags/snapshot/vtags-1.0.tar.xz" >tmp && grep "^Status: 400" tmp && grep "
Unsupported snapshot format: vtags-1.0.tar.xz
" tmp ' test_expect_success 'a request naming no snapshot is refused' ' cgit_rc vtagsrc "url=vtags/snapshot/" >tmp && grep "^Status: 400" tmp && grep "
No snapshot name specified
" tmp ' test_expect_success 'an id naming no commit is refused' ' cgit_rc vtagsrc "url=vtags/snapshot/vtags.tar&id=nosuchref" >tmp && grep "^Status: 404" tmp && grep "
Bad object id: nosuchref
" tmp ' test_expect_success 'set up a repo with a tag holding a slash' ' mkrepo repos/names 2 && git -C repos/names tag rel/1 && cgitrc_repo namesrc names snapshots=tar ' # The stem becomes the leading directory of every member of the archive, so # a name holding a slash is refused even where it spells a ref. Such a ref # is linked under a dashed name instead. test_expect_success 'a snapshot name holding a slash is refused' ' for name in rel/1 :/../../.. do cgit_rc namesrc "url=names/snapshot/$name.tar" >tmp && grep "^Status: 404" tmp && grep "
Not found
" tmp || return 1 done && cgit_ok namesrc "url=names/snapshot/names-master.tar" >tmp ' # Only a ref is worked back from a file name, since the wider revision syntax # can walk the whole history for a match. The name holds no slash, so it # reaches that check rather than the one above. test_expect_success 'a snapshot name in revision syntax is refused' ' cgit_rc namesrc "url=names/snapshot/master~1.tar" >tmp && grep "^Status: 404" tmp && grep "
Not found
" tmp && cgit_ok namesrc "url=names/snapshot/master.tar" >tmp ' # Two tags differing only in the case of their v keep it on both names. Two # loose files differing in case alone cannot coexist on a case-insensitive # filesystem, so the first is packed before the second goes in, and both are # packed after, so that each name is looked up exactly. test_expect_success 'set up a repo whose tags differ only in case' ' mkrepo repos/vcase 2 && git -C repos/vcase update-ref refs/tags/v1.0 HEAD && git -C repos/vcase pack-refs --all && git -C repos/vcase update-ref refs/tags/V1.0 HEAD~1 && git -C repos/vcase pack-refs --all && cgitrc_repo vcaserc vcase snapshots=tar ' test_expect_success 'a real case collision keeps the v on both tags' ' cgit_ok vcaserc "url=vcase/refs/" >tmp && grep ">vcase-v1.0.tar" tmp && grep ">vcase-V1.0.tar" tmp && ! grep ">vcase-1.0.tar" tmp ' test_expect_success 'each of those names leads back to its own tag' ' cgit_ok vcaserc "url=vcase/snapshot/vcase-v1.0.tar" >tmp && snapshot_members && grep "^vcase-v1.0/file-2$" members && cgit_ok vcaserc "url=vcase/snapshot/vcase-V1.0.tar" >tmp && snapshot_members && grep "^vcase-V1.0/file-1$" members && ! grep "file-2" members ' # A name that is all suffix names no revision, though the lookup would settle # on the branch called v that the full name finds. test_expect_success 'set up a repo with a branch named v' ' mkrepo repos/stem 1 && git -C repos/stem branch v HEAD && cgitrc_repo stemrc stem snapshots=tar ' test_expect_success 'a snapshot name with nothing before its suffix is refused' ' cgit_rc stemrc "url=stem/snapshot/.tar" >tmp && grep "^Status: 404" tmp && grep "
Not found
" tmp && cgit_ok stemrc "url=stem/snapshot/stem-v.tar" >tmp && snapshot_members && grep "^stem-v/file-1$" members ' # An empty snapshot-prefix gives the members no leading directory rather than # a leading slash. test_expect_success 'an empty snapshot-prefix puts no slash ahead of the members' ' cgitrc_repo noprefixrc stem snapshots=tar repo.snapshot-prefix= && cgit_ok noprefixrc "url=stem/snapshot/stem.tar&id=master" >tmp && snapshot_members && grep "^file-1$" members && ! grep "^/" members ' # A snapshot is named after the last component of the url once its .git # suffix is gone, so a url of .git alone leaves the ref as the whole name. test_expect_success 'set up repositories whose urls end in .git' ' mkrepo repos/dotgit 1 && git -C repos/dotgit tag v1.0 && { echo "virtual-root=/" && echo "cache-size=0" && echo "snapshots=tar" && echo "repo.url=.git" && echo "repo.path=$PWD/repos/dotgit/.git" && echo "repo.url=sub/.git" && echo "repo.path=$PWD/repos/dotgit/.git" } >dotgitrc ' test_expect_success 'a url of .git names a snapshot after the ref alone' ' cgit_ok dotgitrc "url=.git/refs/" >tmp && grep ">v1.0.tar<" tmp && cgit_ok dotgitrc "url=.git/snapshot/v1.0.tar" >tmp && snapshot_members && grep "^v1.0/file-1$" members ' test_expect_success 'a url ending in /.git names a snapshot after its directory' ' cgit_ok dotgitrc "url=sub/.git/refs/" >tmp && grep ">sub-1.0.tar<" tmp && cgit_ok dotgitrc "url=sub/.git/snapshot/sub-1.0.tar" >tmp && snapshot_members && grep "^sub-1.0/file-1$" members ' test -n "$CGIT_VALGRIND" || test_set_prereq NO_VALGRIND # A server may run cgit with no PATH at all, and the compressor is then looked # for on the default path execvp searches. Under valgrind the cgit found here # is a wrapper script, which needs a PATH of its own. test_expect_success GZIP,NO_VALGRIND 'a compressed snapshot is made without a PATH' ' cgitrc_copy nopathrc && cgit_bin=$(command -v cgit) && ( unset PATH && CGIT_CONFIG="$PWD/nopathrc" QUERY_STRING="url=foo/snapshot/master.tar.gz" "$cgit_bin" >tmp 2>err ) && test_must_be_empty err && grep "^Status: 200" tmp && strip_headers nopath.tar.gz && gzip -dc nopath.tar.gz >nopath.tar && test_cmp_bin expect.tar nopath.tar ' # A link offers the snapshot of a tag, and the name on it has to lead back to # that tag even when a branch is spelled like the tag's stem. The branch holds # the same tree, so only the commit the archive records tells them apart. test_expect_success 'a snapshot name leads to the tag ahead of a branch' ' mkrepo repos/shadow 1 && git -C repos/shadow tag v1.0 && shadow=$(git -C repos/shadow commit-tree -m shadow "HEAD^{tree}") && git -C repos/shadow update-ref refs/heads/1.0 "$shadow" && cgitrc_repo shadowrc shadow snapshots=tar && cgit_ok shadowrc "url=shadow/snapshot/shadow-1.0.tar" >tmp && strip_headers tag.tar && commit=$(git -C repos/shadow get-tar-commit-id sig && sig=$(git hash-object -w sig) && git notes --ref signatures/tar.gz add -C $sig one && echo plain >plain && plain=$(git hash-object -w plain) && git notes --ref signatures/tar add -C $plain three && big=$(test_seq 1 2000 | git hash-object -w --stdin) && git notes --ref signatures/tar.gz add -C $big big ) && cgitrc_repo signedrc signed snapshots=tar.gz ' test_expect_success 'a snapshot and its signature are linked as one unit' ' cgit_ok signedrc "url=signed/refs/" >tmp && grep "signed-one.tar.gz (sig)" tmp ' test_expect_success 'a signature is served as the note holds it' ' cgit_ok signedrc "url=signed/snapshot/signed-one.tar.gz.asc" >tmp && sed "/^$/q" tmp >headers && grep "^Content-Type: application/pgp-signature$" headers && grep "^Content-Disposition: inline; filename=\"signed-one.tar.gz.asc\"$" headers && grep "^X-Content-Type-Options: nosniff$" headers && grep "^Content-Security-Policy: default-src .none.$" headers && strip_headers actual && echo signature >expect && test_cmp expect actual ' test_expect_success 'a ref with no signature has none to offer' ' cgit_ok signedrc "url=signed/refs/" >tmp && grep "signed-two.tar.gz" tmp && cgit_rc signedrc "url=signed/snapshot/signed-two.tar.gz.asc" >tmp && grep "^Status: 404" tmp && grep "
No signature for signed-two.tar.gz
" tmp ' test_expect_success 'a compressed tar offers the plain tar signature' ' cgit_ok signedrc "url=signed/refs/" >tmp && grep "signed-three.tar.gz (sig)" tmp && cgit_ok signedrc "url=signed/snapshot/signed-three.tar.asc" >tmp && strip_headers actual && echo plain >expect && test_cmp expect actual ' test_expect_success 'a signature past max-blob-size is refused' ' cgitrc_repo sigrc signed snapshots=tar.gz max-blob-size=1 && cgit_rc sigrc "url=signed/snapshot/signed-big.tar.gz.asc" >tmp && grep "^Status: 413" tmp && cgit_ok sigrc "url=signed/snapshot/signed-one.tar.gz.asc" >tmp ' # A tag named in UTF-8 gives a snapshot name past ASCII, which is sent as it # is in filename and percent-encoded in filename*. test_expect_success 'a snapshot name in UTF-8 is also sent as filename*' ' mkrepo repos/accent 1 && tag=$(printf "caf\303\251") && git -C repos/accent tag "$tag" && cgitrc_repo accentrc accent snapshots=tar && cgit_ok accentrc "url=accent/refs/tags" >tmp && test_grep "accent-$tag.tar" tmp && cgit_ok accentrc "url=accent/snapshot/accent-caf%C3%A9.tar" >tmp && sed "/^$/q" tmp >headers && test_grep "^Content-Disposition: inline; filename=.accent-$tag.tar.; filename\*=UTF-8..accent-caf%C3%A9.tar$" headers ' test_done