#!/bin/sh # The tree page. The checks against foo+bar cover a file name and a branch # name that both contain a plus, which cgit percent-encodes in a path as # well as in a query, since a bare plus in a served path reads back as a # space through a query string. test_description='Check content on tree page' . ./setup.sh test_expect_success 'generate bar/tree' 'cgit_ok_url "bar/tree" >tmp' test_expect_success 'find file-1' 'grep ">file-1<" tmp' test_expect_success 'find file-50' 'grep ">file-50<" tmp' test_expect_success 'the links column has a name' ' grep "" tmp ' test_expect_success 'generate bar/tree/file-50' 'cgit_ok_url "bar/tree/file-50" >tmp' test_expect_success 'find line 1' ' grep "1" tmp ' test_expect_success 'a one line file gets no second line number' ' cgit_ok_url "bar/tree/file-50" >tmp && test_grep "1" tmp && test_grep ! "2" tmp ' # The table only sets the numbers beside the code, so a screen reader is told # to read it as layout rather than as a table of data. test_expect_success 'the file table is read as layout' ' grep "" tmp ' # Prints, in stylesheet order, the declarations of the properties named # second that the rules for the selector named first make, so the last line # is the one a browser applies wherever the rules stand. A rule names the # selector when one entry of its list is that selector whole, and a rule # inside a media query counts like any other. css_declarations() { awk -v target="$1" -v properties="$2" ' BEGIN { RS = "}" n = split(properties, list, " ") for (i = 1; i <= n; i++) wanted[list[i]] = 1 } { n = split($0, part, "[{]") if (n < 2) next named = 0 m = split(part[n - 1], selector, ",") for (i = 1; i <= m; i++) { s = selector[i] gsub(/^[ \t\n]+|[ \t\n]+$/, "", s) if (s == target) named = 1 } if (!named) next m = split(part[n], declaration, ";") for (i = 1; i <= m; i++) { d = declaration[i] gsub(/^[ \t\n]+|[ \t\n]+$/, "", d) name = substr(d, 1, index(d, ":") - 1) if (name in wanted) print d } } ' "$TEST_OUTPUT_DIRECTORY/../assets/cgit.css" } # The numbers and the code are two columns of text that a browser keeps in # step only while both are set in one font. Only a browser shows the drift, # so this checks the rule that prevents it. test_expect_success 'the code is set in the font of its line numbers' ' css_declarations "div#cgit table.blob code" "font font-family font-size" >decls && tail -n 1 decls >actual && echo "font: inherit" >expect && test_cmp expect actual ' # A browser starts a new line at more characters than git ends one at, and # each would push the rest of the code a line below its numbers. test_expect_success 'set up a file holding the characters a browser breaks at' ' test_create_repo repos/breaks && ( cd repos/breaks && printf "a\\rb\\vc\\fd\\302\\205e\\342\\200\\250f\\342\\200\\251g\\nh\\r\\ni\\r" >breaks.txt && git add breaks.txt && git commit -m breaks ) && cgitrc_repo breaksrc breaks enable-blame=1 ' test_expect_success 'each of them is shown as an escape in the tree' ' cgit_ok breaksrc "url=breaks/tree/breaks.txt" >tmp && grep "a\\\\rb\\\\vc" tmp && grep "c\\\\fd\\\\u0085e" tmp && grep "e\\\\u2028f\\\\u2029g$" tmp ' test_expect_success 'only a carriage return before a line feed is left as it is' ' cgit_ok breaksrc "url=breaks/tree/breaks.txt" >tmp && printf "h\\r\\n" >expect && grep "^h" tmp >actual && test_cmp expect actual && grep "^i\\\\r" tmp ' test_expect_success 'the numbers count only the lines git counts' ' cgit_ok breaksrc "url=breaks/tree/breaks.txt" >tmp && test_grep "3" tmp && test_grep ! "4" tmp ' test_expect_success 'the blame page escapes them the same way' ' cgit_ok breaksrc "url=breaks/blame/breaks.txt" >tmp && grep "a\\\\rb\\\\vc" tmp && grep "c\\\\fd\\\\u0085e" tmp && grep "e\\\\u2028f\\\\u2029g$" tmp && grep "^i\\\\r" tmp ' # The striped background is padded with spaces to the widest line, which is # 31 columns once its six escapes are written out, though it is 18 bytes. test_expect_success 'the blame stripes are as wide as the escaped line' ' cgit_ok breaksrc "url=breaks/blame/breaks.txt" >tmp && grep "^ \\{31\\}" tmp ' test_expect_success 'blame is refused unless enabled' ' cgit_url "bar/blame/file-1" >tmp && test_grep "^Status: 403" tmp && test_grep "
Blame is disabled
" tmp ' # Blame takes a file alone, so on its page the crumbs for the folders above # the file lead to the tree. test_expect_success 'the folder crumbs of a blame page lead to the tree' ' mkrepo repos/nested 1 && ( cd repos/nested && mkdir -p a/b && echo x >a/b/file.txt && git add a && git commit -m nested ) && cgitrc_repo nestedrc nested enable-blame=1 && cgit_ok nestedrc "url=nested/blame/a/b/file.txt" >tmp && grep "path: root/a/b/file.txt" tmp && cgit_ok nestedrc "url=nested/tree/a" >tmp && test_grep "href=./nested/tree/a/b.>b" tmp && cgit_ok nestedrc "url=nested/tree/a/b" >tmp && test_grep "href=./nested/tree/a/b/file.txt.>file.txt" tmp ' # Each hash carries a tooltip naming the author, the committer and the # subject, with the addresses unless enable-plain-email turns them off. test_expect_success 'a blame hash is titled with its commit' ' cgit_ok nestedrc "url=nested/blame/a/b/file.txt" >tmp && test_grep "tmp && test_grep "a.txt && git add a.txt && git commit -m "add a.txt" && git mv a.txt b.txt && test_seq 1 9 >b.txt && echo ten >>b.txt && git commit -am "rename a.txt" ) && cgitrc_repo renamedrc renamed enable-blame=1 && cgit_ok renamedrc "url=renamed/blame/b.txt" >tmp && added=$(git -C repos/renamed rev-parse HEAD~1) && grep "title=.Blame the previous revision. href=./renamed/blame/a.txt?id=$added.>^" tmp && test $(grep -c "Blame the previous revision" tmp) = 1 && cgit_ok renamedrc "url=renamed/blame/a.txt&id=$added" >tmp && test_grep "plain" tmp && test_grep "
" tmp ' test_expect_success 'generate foo+bar/tree' 'cgit_ok_url "foo%2bbar/tree" >tmp' test_expect_success 'verify a+b link' ' grep "/foo%2Bbar/tree/a%2Bb" tmp ' test_expect_success 'generate foo+bar/tree?h=1+2' 'cgit_ok_url "foo%2bbar/tree&h=1%2b2" >tmp' test_expect_success 'verify a+b?h=1+2 link' ' grep "/foo%2Bbar/tree/a%2Bb?h=1%2B2" tmp ' test_expect_success 'set up a repo with a nested submodule' ' mkrepo repos/submodules 1 && ( cd repos/submodules && sub=$(git rev-parse HEAD) && git update-index --add --cacheinfo 160000,$sub,vendor/sub && git commit -m gitlink ) && cgitrc_repo submodrc submodules enable-blame=1 ' test_expect_success 'generate submodules/tree/vendor' ' cgit_ok submodrc "url=submodules/tree/vendor" >tmp ' test_expect_success 'a submodule row shows the entry name alone' ' grep "class=.ls-mod.>sub" tmp && test_grep ! ">vendor/sub" tmp ' test_expect_success 'the submodule hash is titled with the full id' ' sub=$(git -C repos/submodules rev-parse HEAD~1) && grep "class=.ls-mod-hash. title=.$sub.>" tmp ' test_expect_success 'a submodule row shows no size' ' cgit_ok submodrc "url=submodules/tree/vendor" >tmp && grep "ls-mod" tmp >row && test_grep "class=.ls-size.>-" row ' test_expect_success 'a submodule row offers log but not blame or plain' ' cgit_ok submodrc "url=submodules/tree/vendor" >tmp && grep "ls-mod" tmp >row && test_grep ">log" row && test_grep ! ">blame" row && test_grep ! ">plain" row ' test_expect_success 'a file row still offers blame' ' cgit_ok submodrc "url=submodules/tree/" >tmp && grep ">blame" tmp ' # A submodule's commit lives in another repository, so its path points there # the way its row in the listing does. test_expect_success 'a submodule path is answered with the submodule' ' sub=$(git -C repos/submodules rev-parse HEAD~1) && cgit_rc submodrc "url=submodules/tree/vendor/sub" >tmp && grep "^Status: 404" tmp && grep "
This path is a submodule: sub @ " tmp ' # A link target inside the tree becomes a link, while an absolute one names a # file on whatever machine checks the tree out, not a path in the tree, and # so does one that climbs above the root. test_expect_success 'only a link target inside the tree is linked' ' test_create_repo repos/links && ( cd repos/links && echo hi >target.txt && git add target.txt && rel=$(printf target.txt | git hash-object -w --stdin) && abs=$(printf /etc/passwd | git hash-object -w --stdin) && up=$(printf ../outside | git hash-object -w --stdin) && git update-index --add --cacheinfo 120000,$rel,rel && git update-index --add --cacheinfo 120000,$abs,abs && git update-index --add --cacheinfo 120000,$up,up && git commit -m links ) && cgitrc_repo linksrc links && cgit_ok linksrc "url=links/tree/" >tmp && grep " -> target.txt" tmp && grep " -> /etc/passwd" tmp && test_grep ! "href=./links/tree/etc/passwd" tmp && test_grep " -> \.\./outside" tmp ' # Blame takes a regular file alone, so a symlink is not offered it. The # links repository is the one the test above built. test_expect_success 'a symlink is not offered blame' ' cgitrc_repo linksblamerc links enable-blame=1 && cgit_ok linksblamerc "url=links/tree/" >tmp && grep "href=./links/blame/target.txt.>blame" tmp && test_grep ! "href=./links/blame/rel.>blame" tmp && cgit_ok linksblamerc "url=links/tree/rel" >tmp && grep "
" tmp && test_grep ! ">blame" tmp ' # A row follows a chain of single directories for a set number of levels and # stops on the last one it links, with no separator left hanging after it. test_expect_success 'a long directory chain ends on a link' ' test_create_repo repos/deep && ( cd repos/deep && p=d1 && for i in $(test_seq 2 17) do p=$p/d$i || return 1 done && mkdir -p $p && echo deep >$p/file && git add . && git commit -m deep ) && cgitrc_repo deeprc deep && cgit_ok deeprc "url=deep/tree/" >tmp && grep ">d15" tmp && test_grep ! " / " tmp ' # Git holds one object per id, as the type it was first looked up as, so a # folder the commit also names as its parent cannot be read as a tree and the # chain stops on it. The rest of the row still belongs to the first folder. test_expect_success 'a broken chain keeps the row links on the first folder' ' test_create_repo repos/cut && ( cd repos/cut && mkdir -p a/b && echo x >a/b/file && git add a && git commit -m base && odd=$(printf "tree %s\\nparent %s\\nauthor A 1700000000 +0000\\ncommitter A 1700000000 +0000\\n\\nodd\\n" \ $(git rev-parse HEAD^{tree}) $(git rev-parse HEAD:a/b) | git hash-object -t commit -w --stdin --literally) && git update-ref refs/heads/odd $odd ) && cgitrc_repo cutrc cut && cgit_rc cutrc "url=cut/tree/&h=odd" >tmp 2>err && test_grep "^Status: 200" tmp && test_grep "object $(git -C repos/cut rev-parse HEAD:a/b) is a commit, not a tree" err && grep ">b" tmp && grep "href=./cut/log/a?h=odd.>log" tmp && test_grep "" tmp ' test_expect_success 'a binary file in the tree shows as a hex dump' ' test_create_repo repos/bin && ( cd repos/bin && printf "\\000\\001\\002bin" >blob.bin && git add blob.bin && git commit -m binary ) && cgitrc_repo binrc bin && cgit_ok binrc "url=bin/tree/blob.bin" >tmp && grep "
" tmp && grep "" tmp && test_grep ! "" tmp && grep "
Only the first 64 KB of this file are shown. View the whole file.
" tmp ' # The blame page refuses a file git counts as binary and one past # max-blob-size, so a listing offers blame for neither. The two binaries are # the ones the tests above committed. test_expect_success 'a listing offers blame only where it works' ' ( cd repos/bin && echo hello >small.txt && test_seq 1 2000 >large.txt && git add small.txt large.txt && git commit -m "text files" ) && cgitrc_repo binblamerc bin enable-blame=1 max-blob-size=4 && cgit_ok binblamerc "url=bin/tree/" >tmp && test_grep "href=./bin/tree/large.txt.>large.txt" tmp && test_grep "href=./bin/tree/blob.bin.>blob.bin" tmp && test_grep "href=./bin/tree/big.bin.>big.bin" tmp && test_grep "href=./bin/blame/small.txt.>blame" tmp && test_grep ! "href=./bin/blame/large.txt.>blame" tmp && test_grep ! "href=./bin/blame/blob.bin.>blame" tmp && test_grep ! "href=./bin/blame/big.bin.>blame" tmp ' # The bin and nested repositories are the ones the tests above built. test_expect_success 'blame says why it cannot blame a path' ' cgit_ok binblamerc "url=bin/blame/blob.bin" >tmp && test_grep "
Blob is binary
" tmp && cgit_rc nestedrc "url=nested/blame/a" >tmp && test_grep "^Status: 404" tmp && test_grep "
Blame is not available for a directory
" tmp && cgit_rc nestedrc "url=nested/blame/a/no-such-file" >tmp && test_grep "^Status: 404" tmp && test_grep "
Not found
" tmp ' test_expect_success 'the file view offers blame only for a text file' ' cgit_ok binblamerc "url=bin/tree/small.txt" >tmp && test_grep "(plain) (blame)" tmp && cgit_ok binblamerc "url=bin/tree/blob.bin" >tmp && test_grep "(plain)" tmp && test_grep ! ">blame" tmp ' # The listing sniffs only the window git judges binary by, while blame and # the file view look for a NUL anywhere, so a file whose NUL lies past that # window is offered a blame link that ends on the binary notice. Reading the # whole of every blob would cost more than that link does. test_expect_success 'a NUL past the first 8000 bytes still gets a blame link' ' test_create_repo repos/late && ( cd repos/late && test_seq 1 2000 >late.txt && printf "\\000end\\n" >>late.txt && git add late.txt && git commit -m "late NUL" ) && cgitrc_repo laterc late enable-blame=1 && cgit_ok laterc "url=late/tree/" >tmp && test_grep "href=./late/blame/late.txt.>blame" tmp && cgit_ok laterc "url=late/blame/late.txt" >tmp && test_grep "
Blob is binary
" tmp && cgit_ok laterc "url=late/tree/late.txt" >tmp && test_grep "
0000 00 01 02 62 69 6e" tmp && test_grep ! "class=.truncated." tmp ' # The dump scrolls sideways inside its own table, which a keyboard reaches # by focusing the table, so the table takes the focus ring links have. test_expect_success 'a focused hex dump shows the focus ring' ' ring="outline outline-style outline-width outline-color" && css_declarations "div#cgit a:focus-visible" "$ring" >decls && tail -n 1 decls >expect && echo "outline: solid 2px var(--link)" >ring && test_cmp ring expect && css_declarations "div#cgit table.bin-blob:focus-visible" "$ring" >decls && tail -n 1 decls >actual && test_cmp expect actual ' # Each byte of the dump costs several bytes of markup, so a large binary is # dumped up to its first 64 KB and links to the whole file. It joins the bin # repository the test above built. test_expect_success 'a large binary is dumped only in part' ' ( cd repos/bin && printf "\\000" >big.bin && test_seq 1 20000 >>big.bin && git add big.bin && git commit -m "large binary" ) && cgit_ok binrc "url=bin/tree/big.bin" >tmp && grep "ffe010000
" tmp && test_grep ! ">blame" tmp ' # Grouping directories ahead of files is behaviour this fork adds, so nothing # upstream covers it. The directory sorts after the file by name, so only the # grouping can put it first. test_expect_success 'tree groups directories before files' ' test_create_repo repos/grouped && mkdir repos/grouped/zsub && test_commit -C repos/grouped --no-tag inner zsub/inner && test_commit -C repos/grouped --no-tag top afile && cgitrc_repo groupedrc grouped enable-tree-group-dirs=1 && cgit_ok groupedrc "url=grouped/tree/" >tmp && dirline=$(grep -n "tree/zsub" tmp | head -n 1 | cut -d: -f1) && fileline=$(grep -n "tree/afile" tmp | head -n 1 | cut -d: -f1) && test -n "$dirline" && test -n "$fileline" && test "$dirline" -lt "$fileline" ' test_done