detached 39ca311

feat(web): expand directories in place in the Files view

commit
39ca311756f5cde2a82d8cd77938f885034a3d50
author
brice · · 2026-10-11 18:29 UTC
parent
bfab294
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
43 files · +534 -331
M cmd/ermite/testdata/web/blob-binary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-code.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-empty.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-image.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-markdown.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-svg.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-text.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-too-large.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-trojan.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-big.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-binary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-merge.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-modify-split.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-modify.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-rename.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-root.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-trojan.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-page2.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-path.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-root.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-small.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/empty.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/home.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/not-found-page.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/not-found.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/refs.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/summary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/tree-nested.html +3 -7
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -106,7 +108,7 @@
106108 <th class="c-when">date</th>
107109 </tr>
108110 </thead>
109-<tbody id="tree-rows" data-rows>
111+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
110112 <tr data-cursor>
111113 <td class="cur" aria-hidden="true">
112114 </td>
@@ -341,12 +343,6 @@
341343 <span class="k">tag</span>
342344 </a>
343345 </li>
344-<li data-k="file src/lib/util.go">
345-<a href="/files/blob/main/src/lib/util.go" tabindex="-1">
346-<span class="lbl">src/lib/util.go</span>
347-<span class="k">file</span>
348-</a>
349-</li>
350346 </ul>
351347 <p class="panel-foot">
352348 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -106,7 +108,7 @@
106 <th class="c-when">date</th>108 <th class="c-when">date</th>
107 </tr>109 </tr>
108 </thead>110 </thead>
109-<tbody id="tree-rows" data-rows>111+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
110 <tr data-cursor>112 <tr data-cursor>
111 <td class="cur" aria-hidden="true">113 <td class="cur" aria-hidden="true">
112 </td>114 </td>
@@ -341,12 +343,6 @@
341 <span class="k">tag</span>343 <span class="k">tag</span>
342 </a>344 </a>
343 </li>345 </li>
344-<li data-k="file src/lib/util.go">
345-<a href="/files/blob/main/src/lib/util.go" tabindex="-1">
346-<span class="lbl">src/lib/util.go</span>
347-<span class="k">file</span>
348-</a>
349-</li>
350 </ul>346 </ul>
351 <p class="panel-foot">347 <p class="panel-foot">
352 <span>348 <span>
M cmd/ermite/testdata/web/tree-root.html +7 -83
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -100,13 +102,13 @@
100102 <th class="c-when">date</th>
101103 </tr>
102104 </thead>
103-<tbody id="tree-rows" data-rows>
105+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
104106 <tr data-cursor>
105107 <td class="cur" aria-hidden="true">
106108 </td>
107-<td class="c-kind" aria-hidden="true">▾</td>
109+<td class="c-kind" aria-hidden="true">▸</td>
108110 <td class="c-name">
109-<a href="/files/tree/main/docs">docs/</a>
111+<a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a>
110112 </td>
111113 <td class="c-size num dim">-</td>
112114 <td class="c-sub dim">
@@ -120,9 +122,9 @@
120122 <tr>
121123 <td class="cur" aria-hidden="true">
122124 </td>
123-<td class="c-kind" aria-hidden="true">▾</td>
125+<td class="c-kind" aria-hidden="true">▸</td>
124126 <td class="c-name">
125-<a href="/files/tree/main/src">src/</a>
127+<a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a>
126128 </td>
127129 <td class="c-size num dim">-</td>
128130 <td class="c-sub dim">
@@ -568,84 +570,6 @@
568570 <span class="k">tag</span>
569571 </a>
570572 </li>
571-<li data-k="file readme.md">
572-<a href="/files/blob/main/README.md" tabindex="-1">
573-<span class="lbl">README.md</span>
574-<span class="k">file</span>
575-</a>
576-</li>
577-<li data-k="file big.txt">
578-<a href="/files/blob/main/big.txt" tabindex="-1">
579-<span class="lbl">big.txt</span>
580-<span class="k">file</span>
581-</a>
582-</li>
583-<li data-k="file data.bin">
584-<a href="/files/blob/main/data.bin" tabindex="-1">
585-<span class="lbl">data.bin</span>
586-<span class="k">file</span>
587-</a>
588-</li>
589-<li data-k="file empty.txt">
590-<a href="/files/blob/main/empty.txt" tabindex="-1">
591-<span class="lbl">empty.txt</span>
592-<span class="k">file</span>
593-</a>
594-</li>
595-<li data-k="file evil.go">
596-<a href="/files/blob/main/evil.go" tabindex="-1">
597-<span class="lbl">evil.go</span>
598-<span class="k">file</span>
599-</a>
600-</li>
601-<li data-k="file fake.jpg">
602-<a href="/files/blob/main/fake.jpg" tabindex="-1">
603-<span class="lbl">fake.jpg</span>
604-<span class="k">file</span>
605-</a>
606-</li>
607-<li data-k="file fake.png">
608-<a href="/files/blob/main/fake.png" tabindex="-1">
609-<span class="lbl">fake.png</span>
610-<span class="k">file</span>
611-</a>
612-</li>
613-<li data-k="file lines.txt">
614-<a href="/files/blob/main/lines.txt" tabindex="-1">
615-<span class="lbl">lines.txt</span>
616-<span class="k">file</span>
617-</a>
618-</li>
619-<li data-k="file logo.svg">
620-<a href="/files/blob/main/logo.svg" tabindex="-1">
621-<span class="lbl">logo.svg</span>
622-<span class="k">file</span>
623-</a>
624-</li>
625-<li data-k="file main.go">
626-<a href="/files/blob/main/main.go" tabindex="-1">
627-<span class="lbl">main.go</span>
628-<span class="k">file</span>
629-</a>
630-</li>
631-<li data-k="file notes.txt">
632-<a href="/files/blob/main/notes.txt" tabindex="-1">
633-<span class="lbl">notes.txt</span>
634-<span class="k">file</span>
635-</a>
636-</li>
637-<li data-k="file page.html">
638-<a href="/files/blob/main/page.html" tabindex="-1">
639-<span class="lbl">page.html</span>
640-<span class="k">file</span>
641-</a>
642-</li>
643-<li data-k="file pic.png">
644-<a href="/files/blob/main/pic.png" tabindex="-1">
645-<span class="lbl">pic.png</span>
646-<span class="k">file</span>
647-</a>
648-</li>
649573 </ul>
650574 <p class="panel-foot">
651575 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -100,13 +102,13 @@
100 <th class="c-when">date</th>102 <th class="c-when">date</th>
101 </tr>103 </tr>
102 </thead>104 </thead>
103-<tbody id="tree-rows" data-rows>105+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
104 <tr data-cursor>106 <tr data-cursor>
105 <td class="cur" aria-hidden="true">107 <td class="cur" aria-hidden="true">
106 </td>108 </td>
107-<td class="c-kind" aria-hidden="true">▾</td>109+<td class="c-kind" aria-hidden="true">▸</td>
108 <td class="c-name">110 <td class="c-name">
109-<a href="/files/tree/main/docs">docs/</a>111+<a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a>
110 </td>112 </td>
111 <td class="c-size num dim">-</td>113 <td class="c-size num dim">-</td>
112 <td class="c-sub dim">114 <td class="c-sub dim">
@@ -120,9 +122,9 @@
120 <tr>122 <tr>
121 <td class="cur" aria-hidden="true">123 <td class="cur" aria-hidden="true">
122 </td>124 </td>
123-<td class="c-kind" aria-hidden="true">▾</td>125+<td class="c-kind" aria-hidden="true">▸</td>
124 <td class="c-name">126 <td class="c-name">
125-<a href="/files/tree/main/src">src/</a>127+<a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a>
126 </td>128 </td>
127 <td class="c-size num dim">-</td>129 <td class="c-size num dim">-</td>
128 <td class="c-sub dim">130 <td class="c-sub dim">
@@ -568,84 +570,6 @@
568 <span class="k">tag</span>570 <span class="k">tag</span>
569 </a>571 </a>
570 </li>572 </li>
571-<li data-k="file readme.md">
572-<a href="/files/blob/main/README.md" tabindex="-1">
573-<span class="lbl">README.md</span>
574-<span class="k">file</span>
575-</a>
576-</li>
577-<li data-k="file big.txt">
578-<a href="/files/blob/main/big.txt" tabindex="-1">
579-<span class="lbl">big.txt</span>
580-<span class="k">file</span>
581-</a>
582-</li>
583-<li data-k="file data.bin">
584-<a href="/files/blob/main/data.bin" tabindex="-1">
585-<span class="lbl">data.bin</span>
586-<span class="k">file</span>
587-</a>
588-</li>
589-<li data-k="file empty.txt">
590-<a href="/files/blob/main/empty.txt" tabindex="-1">
591-<span class="lbl">empty.txt</span>
592-<span class="k">file</span>
593-</a>
594-</li>
595-<li data-k="file evil.go">
596-<a href="/files/blob/main/evil.go" tabindex="-1">
597-<span class="lbl">evil.go</span>
598-<span class="k">file</span>
599-</a>
600-</li>
601-<li data-k="file fake.jpg">
602-<a href="/files/blob/main/fake.jpg" tabindex="-1">
603-<span class="lbl">fake.jpg</span>
604-<span class="k">file</span>
605-</a>
606-</li>
607-<li data-k="file fake.png">
608-<a href="/files/blob/main/fake.png" tabindex="-1">
609-<span class="lbl">fake.png</span>
610-<span class="k">file</span>
611-</a>
612-</li>
613-<li data-k="file lines.txt">
614-<a href="/files/blob/main/lines.txt" tabindex="-1">
615-<span class="lbl">lines.txt</span>
616-<span class="k">file</span>
617-</a>
618-</li>
619-<li data-k="file logo.svg">
620-<a href="/files/blob/main/logo.svg" tabindex="-1">
621-<span class="lbl">logo.svg</span>
622-<span class="k">file</span>
623-</a>
624-</li>
625-<li data-k="file main.go">
626-<a href="/files/blob/main/main.go" tabindex="-1">
627-<span class="lbl">main.go</span>
628-<span class="k">file</span>
629-</a>
630-</li>
631-<li data-k="file notes.txt">
632-<a href="/files/blob/main/notes.txt" tabindex="-1">
633-<span class="lbl">notes.txt</span>
634-<span class="k">file</span>
635-</a>
636-</li>
637-<li data-k="file page.html">
638-<a href="/files/blob/main/page.html" tabindex="-1">
639-<span class="lbl">page.html</span>
640-<span class="k">file</span>
641-</a>
642-</li>
643-<li data-k="file pic.png">
644-<a href="/files/blob/main/pic.png" tabindex="-1">
645-<span class="lbl">pic.png</span>
646-<span class="k">file</span>
647-</a>
648-</li>
649 </ul>573 </ul>
650 <p class="panel-foot">574 <p class="panel-foot">
651 <span>575 <span>
M cmd/ermite/testdata/web/tree-slash-ref.html +3 -7
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103105 <th class="c-when">date</th>
104106 </tr>
105107 </thead>
106-<tbody id="tree-rows" data-rows>
108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107109 <tr data-cursor>
108110 <td class="cur" aria-hidden="true">
109111 </td>
@@ -338,12 +340,6 @@
338340 <span class="k">tag</span>
339341 </a>
340342 </li>
341-<li data-k="file x/y.txt">
342-<a href="/files/blob/feature/x/x/y.txt" tabindex="-1">
343-<span class="lbl">x/y.txt</span>
344-<span class="k">file</span>
345-</a>
346-</li>
347343 </ul>
348344 <p class="panel-foot">
349345 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103 <th class="c-when">date</th>105 <th class="c-when">date</th>
104 </tr>106 </tr>
105 </thead>107 </thead>
106-<tbody id="tree-rows" data-rows>108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107 <tr data-cursor>109 <tr data-cursor>
108 <td class="cur" aria-hidden="true">110 <td class="cur" aria-hidden="true">
109 </td>111 </td>
@@ -338,12 +340,6 @@
338 <span class="k">tag</span>340 <span class="k">tag</span>
339 </a>341 </a>
340 </li>342 </li>
341-<li data-k="file x/y.txt">
342-<a href="/files/blob/feature/x/x/y.txt" tabindex="-1">
343-<span class="lbl">x/y.txt</span>
344-<span class="k">file</span>
345-</a>
346-</li>
347 </ul>343 </ul>
348 <p class="panel-foot">344 <p class="panel-foot">
349 <span>345 <span>
M cmd/ermite/testdata/web/tree-subdir.html +3 -13
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103105 <th class="c-when">date</th>
104106 </tr>
105107 </thead>
106-<tbody id="tree-rows" data-rows>
108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107109 <tr data-cursor>
108110 <td class="cur" aria-hidden="true">
109111 </td>
@@ -363,18 +365,6 @@
363365 <span class="k">tag</span>
364366 </a>
365367 </li>
366-<li data-k="file docs/readme.md">
367-<a href="/files/blob/main/docs/README.md" tabindex="-1">
368-<span class="lbl">docs/README.md</span>
369-<span class="k">file</span>
370-</a>
371-</li>
372-<li data-k="file docs/guide.md">
373-<a href="/files/blob/main/docs/guide.md" tabindex="-1">
374-<span class="lbl">docs/guide.md</span>
375-<span class="k">file</span>
376-</a>
377-</li>
378368 </ul>
379369 <p class="panel-foot">
380370 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103 <th class="c-when">date</th>105 <th class="c-when">date</th>
104 </tr>106 </tr>
105 </thead>107 </thead>
106-<tbody id="tree-rows" data-rows>108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107 <tr data-cursor>109 <tr data-cursor>
108 <td class="cur" aria-hidden="true">110 <td class="cur" aria-hidden="true">
109 </td>111 </td>
@@ -363,18 +365,6 @@
363 <span class="k">tag</span>365 <span class="k">tag</span>
364 </a>366 </a>
365 </li>367 </li>
366-<li data-k="file docs/readme.md">
367-<a href="/files/blob/main/docs/README.md" tabindex="-1">
368-<span class="lbl">docs/README.md</span>
369-<span class="k">file</span>
370-</a>
371-</li>
372-<li data-k="file docs/guide.md">
373-<a href="/files/blob/main/docs/guide.md" tabindex="-1">
374-<span class="lbl">docs/guide.md</span>
375-<span class="k">file</span>
376-</a>
377-</li>
378 </ul>368 </ul>
379 <p class="panel-foot">369 <p class="panel-foot">
380 <span>370 <span>
M cmd/ermite/web_files_test.go +42 -0
@@ -100,6 +100,48 @@ func TestWebFilesGolden(t *testing.T) {
100100 }
101101 }
102102
103+// AC1, AC2, AC7: ?rows answers the entries of a directory as table rows only,
104+// for the Files view to insert under the directory; a private repository is
105+// the 404 of a missing one.
106+func TestWebTreeRows(t *testing.T) {
107+ h := newHarness(t, harnessOpts{})
108+ h.webFilesRepo("files")
109+ h.seed("secret-sauce", domain.VisibilityPrivate, "dir/a.txt")
110+
111+ // The page links its directories to their rows.
112+ _, _, page := h.get("/files/tree/main")
113+ assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`)
114+ assert.Contains(t, page, `<script src="/-/static/js/files.`)
115+ assert.NotContains(t, page, "main.go</span>", "the palette lists no file")
116+
117+ status, hdr, body := h.get("/files/tree/main/docs?rows")
118+ require.Equal(t, 200, status)
119+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
120+ assert.Contains(t, hdr.Get("Content-Type"), "text/html")
121+ assert.NotContains(t, body, "<html")
122+ assert.NotContains(t, body, "<table")
123+ assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry")
124+ assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`)
125+ assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`)
126+ assert.NotContains(t, body, "data-cursor", "the cursor stays on the page")
127+ assertNoInline(t, body)
128+
129+ // A subdirectory row can be opened in turn.
130+ _, _, body = h.get("/files/tree/main/src?rows")
131+ assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`)
132+ assert.Contains(t, body, "▸")
133+ _, _, body = h.get("/files/tree/main/src/lib?rows")
134+ assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`)
135+ assert.NotContains(t, body, "data-expand")
136+
137+ // Not found, and private as anonymous: the 404 page, nothing of the rows.
138+ for _, p := range []string{"/files/tree/main/nope?rows", "/secret-sauce/tree/main/dir?rows", "/secret-sauce/tree/main?rows"} {
139+ status, _, body = h.get(p)
140+ assert.Equal(t, 404, status, p)
141+ assert.NotContains(t, body, "a.txt", p)
142+ }
143+}
144+
103145 // Ref names with a slash are found by their longest prefix, and a commit id
104146 // works where a ref does.
105147 func TestWebFilesRefs(t *testing.T) {
@@ -100,6 +100,48 @@ func TestWebFilesGolden(t *testing.T) {
100 }100 }
101 }101 }
102 102
103+// AC1, AC2, AC7: ?rows answers the entries of a directory as table rows only,
104+// for the Files view to insert under the directory; a private repository is
105+// the 404 of a missing one.
106+func TestWebTreeRows(t *testing.T) {
107+ h := newHarness(t, harnessOpts{})
108+ h.webFilesRepo("files")
109+ h.seed("secret-sauce", domain.VisibilityPrivate, "dir/a.txt")
110+
111+ // The page links its directories to their rows.
112+ _, _, page := h.get("/files/tree/main")
113+ assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`)
114+ assert.Contains(t, page, `<script src="/-/static/js/files.`)
115+ assert.NotContains(t, page, "main.go</span>", "the palette lists no file")
116+
117+ status, hdr, body := h.get("/files/tree/main/docs?rows")
118+ require.Equal(t, 200, status)
119+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
120+ assert.Contains(t, hdr.Get("Content-Type"), "text/html")
121+ assert.NotContains(t, body, "<html")
122+ assert.NotContains(t, body, "<table")
123+ assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry")
124+ assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`)
125+ assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`)
126+ assert.NotContains(t, body, "data-cursor", "the cursor stays on the page")
127+ assertNoInline(t, body)
128+
129+ // A subdirectory row can be opened in turn.
130+ _, _, body = h.get("/files/tree/main/src?rows")
131+ assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`)
132+ assert.Contains(t, body, "▸")
133+ _, _, body = h.get("/files/tree/main/src/lib?rows")
134+ assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`)
135+ assert.NotContains(t, body, "data-expand")
136+
137+ // Not found, and private as anonymous: the 404 page, nothing of the rows.
138+ for _, p := range []string{"/files/tree/main/nope?rows", "/secret-sauce/tree/main/dir?rows", "/secret-sauce/tree/main?rows"} {
139+ status, _, body = h.get(p)
140+ assert.Equal(t, 404, status, p)
141+ assert.NotContains(t, body, "a.txt", p)
142+ }
143+}
144+
103 // Ref names with a slash are found by their longest prefix, and a commit id145 // Ref names with a slash are found by their longest prefix, and a commit id
104 // works where a ref does.146 // works where a ref does.
105 func TestWebFilesRefs(t *testing.T) {147 func TestWebFilesRefs(t *testing.T) {
M internal/adapters/web/ASSETS.md +1 -0
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
99 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
1010 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
1111 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |
12+| `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`) | written here | |
1213 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
1314 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
1415 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
11 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |11 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |
12+| `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`) | written here | |
12 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |13 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
13 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |14 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
14 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |15 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
M internal/adapters/web/assets.go +2 -1
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
2323 // Assets are the URLs of the assets a page loads. Their names carry a hash of
2424 // the content, so they can be cached for ever.
2525 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Icon string
26+ CSS, HTMX, Live, SSE, App, Files, Icon string
2727 // Font is the UI font, preloaded by every page.
2828 Font string
2929 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -52,6 +52,7 @@ var scripts = []struct {
5252 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},
5353 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
5454 {"ermite.js", func(a *Assets, u string) { a.App = u }},
55+ {"files.js", func(a *Assets, u string) { a.Files = u }},
5556 }
5657
5758 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
23 // Assets are the URLs of the assets a page loads. Their names carry a hash of23 // Assets are the URLs of the assets a page loads. Their names carry a hash of
24 // the content, so they can be cached for ever.24 // the content, so they can be cached for ever.
25 type Assets struct {25 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Icon string26+ CSS, HTMX, Live, SSE, App, Files, Icon string
27 // Font is the UI font, preloaded by every page.27 // Font is the UI font, preloaded by every page.
28 Font string28 Font string
29 // Rules is the speculation rules file named by the Speculation-Rules header.29 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -52,6 +52,7 @@ var scripts = []struct {
52 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},52 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},
53 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},53 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
54 {"ermite.js", func(a *Assets, u string) { a.App = u }},54 {"ermite.js", func(a *Assets, u string) { a.App = u }},
55+ {"files.js", func(a *Assets, u string) { a.Files = u }},
55 }56 }
56 57
57 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):58 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
M internal/adapters/web/overlays.templ +1 -1
@@ -84,7 +84,7 @@ templ refItem(r RefItem) {
8484 }
8585
8686 // OverlayCmd is the ":" command line: one filterable list mixing sections,
87-// refs, files of this directory and repos.
87+// refs and repos.
8888 templ OverlayCmd(items []CmdItem) {
8989 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
9090 <div class="panel-head">
@@ -84,7 +84,7 @@ templ refItem(r RefItem) {
84 }84 }
85 85
86 // OverlayCmd is the ":" command line: one filterable list mixing sections,86 // OverlayCmd is the ":" command line: one filterable list mixing sections,
87-// refs, files of this directory and repos.87+// refs and repos.
88 templ OverlayCmd(items []CmdItem) {88 templ OverlayCmd(items []CmdItem) {
89 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">89 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
90 <div class="panel-head">90 <div class="panel-head">
M internal/adapters/web/overlays_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/shell.templ +1 -0
@@ -20,6 +20,7 @@ templ Shell(l Layout, rail templ.Component) {
2020 <script src={ l.Assets.Live } defer></script>
2121 <script src={ l.Assets.SSE } defer></script>
2222 <script src={ l.Assets.App } defer></script>
23+ <script src={ l.Assets.Files } defer></script>
2324 </head>
2425 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">
2526 <a class="skip" href="#main">skip to content</a>
@@ -20,6 +20,7 @@ templ Shell(l Layout, rail templ.Component) {
20 <script src={ l.Assets.Live } defer></script>20 <script src={ l.Assets.Live } defer></script>
21 <script src={ l.Assets.SSE } defer></script>21 <script src={ l.Assets.SSE } defer></script>
22 <script src={ l.Assets.App } defer></script>22 <script src={ l.Assets.App } defer></script>
23+ <script src={ l.Assets.Files } defer></script>
23 </head>24 </head>
24 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">25 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">
25 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/adapters/web/shell_templ.go +138 -125 generated file: collapsed
M internal/adapters/web/static/css/30-list.css +4 -1
@@ -38,7 +38,10 @@
3838 }
3939 .list.tree {
4040 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }
41- & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); }
41+ /* --depth is set by files.js on the rows opened in place */
42+ & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); }
43+ & tr[aria-busy=true] .c-kind { opacity: .4; }
44+ & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); }
4245 & .c-size { width: 90px; text-align: right; }
4346 & .c-when { width: 100px; }
4447 & .c-sub { width: auto; }
@@ -38,7 +38,10 @@
38 }38 }
39 .list.tree {39 .list.tree {
40 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }40 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }
41- & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); }41+ /* --depth is set by files.js on the rows opened in place */
42+ & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); }
43+ & tr[aria-busy=true] .c-kind { opacity: .4; }
44+ & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); }
42 & .c-size { width: 90px; text-align: right; }45 & .c-size { width: 90px; text-align: right; }
43 & .c-when { width: 100px; }46 & .c-when { width: 100px; }
44 & .c-sub { width: auto; }47 & .c-sub { width: auto; }
A internal/adapters/web/static/js/files.js +81 -0
@@ -0,0 +1,81 @@
1+// files.js: the Files view. A directory opens in place: its entries are
2+// fetched once as table rows (the tree page with ?rows), inserted under it
3+// and kept in the DOM, so closing and opening it again needs no request.
4+//
5+// Rows opened in place carry data-depth and the --depth custom property
6+// (set through the CSSOM: the CSP forbids style attributes). Rows of the
7+// page itself have depth 0. Nothing here touches the document at load; the
8+// handlers are declared in the templates (hx-on:click on #tree-rows).
9+window.ermiteTree = (() => {
10+ const depthOf = (tr) => Number(tr.dataset.depth || 0);
11+ const linkOf = (tr) => tr.querySelector('a[data-expand]');
12+ const isOpen = (a) => a.getAttribute('aria-expanded') === 'true';
13+
14+ function setOpen(tr, a, open) {
15+ a.setAttribute('aria-expanded', open ? 'true' : 'false');
16+ const kind = tr.querySelector('.c-kind');
17+ if (kind) kind.textContent = open ? '▾' : '▸';
18+ }
19+
20+ // collapse hides every row under tr.
21+ function collapse(tr, a) {
22+ setOpen(tr, a, false);
23+ const d = depthOf(tr);
24+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) r.hidden = true;
25+ }
26+
27+ // expand shows the rows under tr that no collapsed directory between hides.
28+ function expand(tr, a) {
29+ setOpen(tr, a, true);
30+ const d = depthOf(tr);
31+ let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped
32+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) {
33+ const rd = depthOf(r);
34+ if (rd <= closedAt) closedAt = Infinity;
35+ r.hidden = rd > closedAt;
36+ const la = linkOf(r);
37+ if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd;
38+ }
39+ }
40+
41+ async function load(tr, a) {
42+ tr.setAttribute('aria-busy', 'true');
43+ try {
44+ const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } });
45+ if (!res.ok) throw new Error('HTTP ' + res.status);
46+ const t = document.createElement('template');
47+ t.innerHTML = await res.text();
48+ const d = depthOf(tr) + 1;
49+ const rows = Array.from(t.content.children);
50+ for (const r of rows) {
51+ r.dataset.depth = String(d);
52+ r.style.setProperty('--depth', String(d));
53+ }
54+ tr.after(...rows);
55+ tr.dataset.loaded = '1';
56+ tr.removeAttribute('aria-busy');
57+ expand(tr, a);
58+ } catch (e) {
59+ location.href = a.href;
60+ }
61+ }
62+
63+ function toggle(tr) {
64+ const a = linkOf(tr);
65+ if (!a || tr.getAttribute('aria-busy') === 'true') return;
66+ if (isOpen(a)) collapse(tr, a);
67+ else if (tr.dataset.loaded) expand(tr, a);
68+ else load(tr, a);
69+ }
70+
71+ return {
72+ // click opens or closes the directory under a plain left click; any
73+ // modified or other click keeps the link's own navigation.
74+ click(event) {
75+ const a = event.target.closest('a[data-expand]');
76+ if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
77+ event.preventDefault();
78+ toggle(a.closest('tr'));
79+ },
80+ };
81+})();
@@ -0,0 +1,81 @@
1+// files.js: the Files view. A directory opens in place: its entries are
2+// fetched once as table rows (the tree page with ?rows), inserted under it
3+// and kept in the DOM, so closing and opening it again needs no request.
4+//
5+// Rows opened in place carry data-depth and the --depth custom property
6+// (set through the CSSOM: the CSP forbids style attributes). Rows of the
7+// page itself have depth 0. Nothing here touches the document at load; the
8+// handlers are declared in the templates (hx-on:click on #tree-rows).
9+window.ermiteTree = (() => {
10+ const depthOf = (tr) => Number(tr.dataset.depth || 0);
11+ const linkOf = (tr) => tr.querySelector('a[data-expand]');
12+ const isOpen = (a) => a.getAttribute('aria-expanded') === 'true';
13+
14+ function setOpen(tr, a, open) {
15+ a.setAttribute('aria-expanded', open ? 'true' : 'false');
16+ const kind = tr.querySelector('.c-kind');
17+ if (kind) kind.textContent = open ? '▾' : '▸';
18+ }
19+
20+ // collapse hides every row under tr.
21+ function collapse(tr, a) {
22+ setOpen(tr, a, false);
23+ const d = depthOf(tr);
24+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) r.hidden = true;
25+ }
26+
27+ // expand shows the rows under tr that no collapsed directory between hides.
28+ function expand(tr, a) {
29+ setOpen(tr, a, true);
30+ const d = depthOf(tr);
31+ let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped
32+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) {
33+ const rd = depthOf(r);
34+ if (rd <= closedAt) closedAt = Infinity;
35+ r.hidden = rd > closedAt;
36+ const la = linkOf(r);
37+ if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd;
38+ }
39+ }
40+
41+ async function load(tr, a) {
42+ tr.setAttribute('aria-busy', 'true');
43+ try {
44+ const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } });
45+ if (!res.ok) throw new Error('HTTP ' + res.status);
46+ const t = document.createElement('template');
47+ t.innerHTML = await res.text();
48+ const d = depthOf(tr) + 1;
49+ const rows = Array.from(t.content.children);
50+ for (const r of rows) {
51+ r.dataset.depth = String(d);
52+ r.style.setProperty('--depth', String(d));
53+ }
54+ tr.after(...rows);
55+ tr.dataset.loaded = '1';
56+ tr.removeAttribute('aria-busy');
57+ expand(tr, a);
58+ } catch (e) {
59+ location.href = a.href;
60+ }
61+ }
62+
63+ function toggle(tr) {
64+ const a = linkOf(tr);
65+ if (!a || tr.getAttribute('aria-busy') === 'true') return;
66+ if (isOpen(a)) collapse(tr, a);
67+ else if (tr.dataset.loaded) expand(tr, a);
68+ else load(tr, a);
69+ }
70+
71+ return {
72+ // click opens or closes the directory under a plain left click; any
73+ // modified or other click keeps the link's own navigation.
74+ click(event) {
75+ const a = event.target.closest('a[data-expand]');
76+ if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
77+ event.preventDefault();
78+ toggle(a.closest('tr'));
79+ },
80+ };
81+})();
M internal/adapters/web/tree.go +21 -12
@@ -12,14 +12,14 @@ import (
1212 pb "bnema.dev/ermite-api/gen/ermite"
1313 )
1414
15-// maxFileTargets bounds the files of a directory offered in the ":" list.
16-const maxFileTargets = 200
17-
1815 // TreeRow is one entry of a directory.
1916 type TreeRow struct {
20- Glyph string // ▾ directory, @ symbolic link, + submodule, · file
21- Name string
22- Href string // "" for a submodule
17+ Glyph string // ▸ directory, @ symbolic link, + submodule, · file
18+ Name string
19+ Href string // "" for a submodule
20+ // Expand is the URL of the rows of a directory (Href?rows); "" for any
21+ // other entry.
22+ Expand string
2323 Size string
2424 Subject string
2525 CHref string
@@ -35,6 +35,9 @@ type TreePage struct {
3535 Readme *Readme
3636 // Notes say what the listing leaves out.
3737 Notes []string
38+ // More, for the rows of a directory expanded in place, is the page of the
39+ // directory when it has more entries than are listed; "" otherwise.
40+ More string
3841 }
3942
4043 // tree lists a directory of a ref, with the last commit of each entry, and
@@ -57,14 +60,15 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
5760 entries := res.Msg.GetEntries()
5861 now := s.cfg.Now()
5962
63+ rowsOnly := r.URL.Query().Has("rows")
6064 p := TreePage{}
61- l := s.repoLayout(ctx, d, "tree")
6265 for i, e := range entries {
63- row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}
66+ row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
6467 switch e.GetKind() {
6568 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66- row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"
69+ row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
6770 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71+ row.Expand = row.Href + "?rows"
6872 case pb.EntryKind_ENTRY_KIND_SYMLINK:
6973 row.Glyph = "@"
7074 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
@@ -72,9 +76,6 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
7276 row.Glyph, row.Size = "+", "-"
7377 default:
7478 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75- if len(l.Cmd) < maxFileTargets {
76- l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77- }
7879 }
7980 if c := e.GetLastCommit(); c != nil {
8081 last := commitRow(d.name(), c, now)
@@ -82,6 +83,13 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
8283 }
8384 p.Rows = append(p.Rows, row)
8485 }
86+ if rowsOnly {
87+ // The rows alone, for a directory opened in place (files.js).
88+ if res.Msg.GetTruncated() {
89+ p.More = repoHref(d.name(), "tree", t.name, t.path)
90+ }
91+ return TreeRows(p), http.StatusOK, nil
92+ }
8593 if res.Msg.GetTruncated() {
8694 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
8795 }
@@ -96,6 +104,7 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
96104 p.Readme = rd.readme
97105 }
98106
107+ l := s.repoLayout(ctx, d, "tree")
99108 pinRef(&l, t)
100109 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101110 if t.path == "" {
@@ -12,14 +12,14 @@ import (
12 pb "bnema.dev/ermite-api/gen/ermite"12 pb "bnema.dev/ermite-api/gen/ermite"
13 )13 )
14 14
15-// maxFileTargets bounds the files of a directory offered in the ":" list.
16-const maxFileTargets = 200
17-
18 // TreeRow is one entry of a directory.15 // TreeRow is one entry of a directory.
19 type TreeRow struct {16 type TreeRow struct {
20- Glyph string // ▾ directory, @ symbolic link, + submodule, · file17+ Glyph string // ▸ directory, @ symbolic link, + submodule, · file
21- Name string18+ Name string
22- Href string // "" for a submodule19+ Href string // "" for a submodule
20+ // Expand is the URL of the rows of a directory (Href?rows); "" for any
21+ // other entry.
22+ Expand string
23 Size string23 Size string
24 Subject string24 Subject string
25 CHref string25 CHref string
@@ -35,6 +35,9 @@ type TreePage struct {
35 Readme *Readme35 Readme *Readme
36 // Notes say what the listing leaves out.36 // Notes say what the listing leaves out.
37 Notes []string37 Notes []string
38+ // More, for the rows of a directory expanded in place, is the page of the
39+ // directory when it has more entries than are listed; "" otherwise.
40+ More string
38 }41 }
39 42
40 // tree lists a directory of a ref, with the last commit of each entry, and43 // tree lists a directory of a ref, with the last commit of each entry, and
@@ -57,14 +60,15 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
57 entries := res.Msg.GetEntries()60 entries := res.Msg.GetEntries()
58 now := s.cfg.Now()61 now := s.cfg.Now()
59 62
63+ rowsOnly := r.URL.Query().Has("rows")
60 p := TreePage{}64 p := TreePage{}
61- l := s.repoLayout(ctx, d, "tree")
62 for i, e := range entries {65 for i, e := range entries {
63- row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}66+ row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
64 switch e.GetKind() {67 switch e.GetKind() {
65 case pb.EntryKind_ENTRY_KIND_DIRECTORY:68 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66- row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"69+ row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
67 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())70 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71+ row.Expand = row.Href + "?rows"
68 case pb.EntryKind_ENTRY_KIND_SYMLINK:72 case pb.EntryKind_ENTRY_KIND_SYMLINK:
69 row.Glyph = "@"73 row.Glyph = "@"
70 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
@@ -72,9 +76,6 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
72 row.Glyph, row.Size = "+", "-"76 row.Glyph, row.Size = "+", "-"
73 default:77 default:
74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())78 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75- if len(l.Cmd) < maxFileTargets {
76- l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77- }
78 }79 }
79 if c := e.GetLastCommit(); c != nil {80 if c := e.GetLastCommit(); c != nil {
80 last := commitRow(d.name(), c, now)81 last := commitRow(d.name(), c, now)
@@ -82,6 +83,13 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
82 }83 }
83 p.Rows = append(p.Rows, row)84 p.Rows = append(p.Rows, row)
84 }85 }
86+ if rowsOnly {
87+ // The rows alone, for a directory opened in place (files.js).
88+ if res.Msg.GetTruncated() {
89+ p.More = repoHref(d.name(), "tree", t.name, t.path)
90+ }
91+ return TreeRows(p), http.StatusOK, nil
92+ }
85 if res.Msg.GetTruncated() {93 if res.Msg.GetTruncated() {
86 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")94 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
87 }95 }
@@ -96,6 +104,7 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
96 p.Readme = rd.readme104 p.Readme = rd.readme
97 }105 }
98 106
107+ l := s.repoLayout(ctx, d, "tree")
99 pinRef(&l, t)108 pinRef(&l, t)
100 l.Crumbs = pathCrumbs(d.name(), "tree", t)109 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101 if t.path == "" {110 if t.path == "" {
M internal/adapters/web/tree.templ +24 -7
@@ -21,10 +21,8 @@ templ Tree(l Layout, p TreePage) {
2121 <thead>
2222 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>
2323 </thead>
24- <tbody id="tree-rows" data-rows>
25- for _, r := range p.Rows {
26- @TreeRowView(r)
27- }
24+ <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
25+ @TreeRows(p)
2826 </tbody>
2927 </table>
3028 if len(p.Rows) == 0 {
@@ -39,15 +37,34 @@ templ Tree(l Layout, p TreePage) {
3937 }
4038 }
4139
42-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic
43-// link, + submodule, · file.
40+// TreeRows is the rows of a directory alone: what files.js inserts under a
41+// directory it opens in place. When the directory has more entries than are
42+// listed, the last row links to its page.
43+templ TreeRows(p TreePage) {
44+ for _, r := range p.Rows {
45+ @TreeRowView(r)
46+ }
47+ if p.More != "" {
48+ <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr>
49+ }
50+}
51+
52+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
53+// in place), @ symbolic link, + submodule, · file. A directory link carries
54+// data-expand, the URL of its rows.
4455 templ TreeRowView(r TreeRow) {
4556 <tr data-cursor?={ r.Cursor }>
4657 <td class="cur" aria-hidden="true"></td>
4758 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>
4859 <td class="c-name">
4960 if r.Href != "" {
50- <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
61+ <a
62+ href={ templ.SafeURL(r.Href) }
63+ if r.Expand != "" {
64+ data-expand={ r.Expand }
65+ aria-expanded="false"
66+ }
67+ >{ r.Name }</a>
5168 } else {
5269 { r.Name }
5370 }
@@ -21,10 +21,8 @@ templ Tree(l Layout, p TreePage) {
21 <thead>21 <thead>
22 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>22 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>
23 </thead>23 </thead>
24- <tbody id="tree-rows" data-rows>24+ <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
25- for _, r := range p.Rows {25+ @TreeRows(p)
26- @TreeRowView(r)
27- }
28 </tbody>26 </tbody>
29 </table>27 </table>
30 if len(p.Rows) == 0 {28 if len(p.Rows) == 0 {
@@ -39,15 +37,34 @@ templ Tree(l Layout, p TreePage) {
39 }37 }
40 }38 }
41 39
42-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic40+// TreeRows is the rows of a directory alone: what files.js inserts under a
43-// link, + submodule, · file.41+// directory it opens in place. When the directory has more entries than are
42+// listed, the last row links to its page.
43+templ TreeRows(p TreePage) {
44+ for _, r := range p.Rows {
45+ @TreeRowView(r)
46+ }
47+ if p.More != "" {
48+ <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr>
49+ }
50+}
51+
52+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
53+// in place), @ symbolic link, + submodule, · file. A directory link carries
54+// data-expand, the URL of its rows.
44 templ TreeRowView(r TreeRow) {55 templ TreeRowView(r TreeRow) {
45 <tr data-cursor?={ r.Cursor }>56 <tr data-cursor?={ r.Cursor }>
46 <td class="cur" aria-hidden="true"></td>57 <td class="cur" aria-hidden="true"></td>
47 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>58 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>
48 <td class="c-name">59 <td class="c-name">
49 if r.Href != "" {60 if r.Href != "" {
50- <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>61+ <a
62+ href={ templ.SafeURL(r.Href) }
63+ if r.Expand != "" {
64+ data-expand={ r.Expand }
65+ aria-expanded="false"
66+ }
67+ >{ r.Name }</a>
51 } else {68 } else {
52 { r.Name }69 { r.Name }
53 }70 }
M internal/adapters/web/tree_templ.go +148 -73
@@ -45,15 +45,13 @@ func Tree(l Layout, p TreePage) templ.Component {
4545 }()
4646 }
4747 ctx = templ.InitializeContext(ctx)
48- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"filter\"><label for=\"tree-filter\" class=\"sr\">filter</label> <input id=\"tree-filter\" type=\"search\" data-filter placeholder=\"Filter this directory…\" autocomplete=\"off\" hx-on:input=\"let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')\"></div><table class=\"list tree\" aria-label=\"Entries\"><thead><tr><th class=\"cur\"></th><th class=\"c-kind\"></th><th class=\"c-name\">name</th><th class=\"c-size\">size</th><th class=\"c-sub\">last commit</th><th class=\"c-who\">author</th><th class=\"c-when\">date</th></tr></thead> <tbody id=\"tree-rows\" data-rows>")
48+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"filter\"><label for=\"tree-filter\" class=\"sr\">filter</label> <input id=\"tree-filter\" type=\"search\" data-filter placeholder=\"Filter this directory…\" autocomplete=\"off\" hx-on:input=\"let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')\"></div><table class=\"list tree\" aria-label=\"Entries\"><thead><tr><th class=\"cur\"></th><th class=\"c-kind\"></th><th class=\"c-name\">name</th><th class=\"c-size\">size</th><th class=\"c-sub\">last commit</th><th class=\"c-who\">author</th><th class=\"c-when\">date</th></tr></thead> <tbody id=\"tree-rows\" data-rows hx-on:click=\"ermiteTree.click(event)\">")
4949 if templ_7745c5c3_Err != nil {
5050 return templ_7745c5c3_Err
5151 }
52- for _, r := range p.Rows {
53- templ_7745c5c3_Err = TreeRowView(r).Render(ctx, templ_7745c5c3_Buffer)
54- if templ_7745c5c3_Err != nil {
55- return templ_7745c5c3_Err
56- }
52+ templ_7745c5c3_Err = TreeRows(p).Render(ctx, templ_7745c5c3_Buffer)
53+ if templ_7745c5c3_Err != nil {
54+ return templ_7745c5c3_Err
5755 }
5856 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</tbody></table>")
5957 if templ_7745c5c3_Err != nil {
@@ -73,7 +71,7 @@ func Tree(l Layout, p TreePage) templ.Component {
7371 var templ_7745c5c3_Var3 string
7472 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(n)
7573 if templ_7745c5c3_Err != nil {
76- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 34, Col: 22}
74+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 32, Col: 22}
7775 }
7876 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
7977 if templ_7745c5c3_Err != nil {
@@ -104,9 +102,10 @@ func Tree(l Layout, p TreePage) templ.Component {
104102 })
105103 }
106104
107-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic
108-// link, + submodule, · file.
109-func TreeRowView(r TreeRow) templ.Component {
105+// TreeRows is the rows of a directory alone: what files.js inserts under a
106+// directory it opens in place. When the directory has more entries than are
107+// listed, the last row links to its page.
108+func TreeRows(p TreePage) templ.Component {
110109 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
111110 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
112111 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -127,187 +126,263 @@ func TreeRowView(r TreeRow) templ.Component {
127126 templ_7745c5c3_Var4 = templ.NopComponent
128127 }
129128 ctx = templ.ClearChildren(ctx)
130- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr")
129+ for _, r := range p.Rows {
130+ templ_7745c5c3_Err = TreeRowView(r).Render(ctx, templ_7745c5c3_Buffer)
131+ if templ_7745c5c3_Err != nil {
132+ return templ_7745c5c3_Err
133+ }
134+ }
135+ if p.More != "" {
136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr class=\"tree-more\"><td colspan=\"7\"><a href=\"")
137+ if templ_7745c5c3_Err != nil {
138+ return templ_7745c5c3_Err
139+ }
140+ var templ_7745c5c3_Var5 templ.SafeURL
141+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.More))
142+ if templ_7745c5c3_Err != nil {
143+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 48, Col: 71}
144+ }
145+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
146+ if templ_7745c5c3_Err != nil {
147+ return templ_7745c5c3_Err
148+ }
149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">more entries: open the directory</a></td></tr>")
150+ if templ_7745c5c3_Err != nil {
151+ return templ_7745c5c3_Err
152+ }
153+ }
154+ return nil
155+ })
156+}
157+
158+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
159+// in place), @ symbolic link, + submodule, · file. A directory link carries
160+// data-expand, the URL of its rows.
161+func TreeRowView(r TreeRow) templ.Component {
162+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
163+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
164+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
165+ return templ_7745c5c3_CtxErr
166+ }
167+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
168+ if !templ_7745c5c3_IsBuffer {
169+ defer func() {
170+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
171+ if templ_7745c5c3_Err == nil {
172+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
173+ }
174+ }()
175+ }
176+ ctx = templ.InitializeContext(ctx)
177+ templ_7745c5c3_Var6 := templ.GetChildren(ctx)
178+ if templ_7745c5c3_Var6 == nil {
179+ templ_7745c5c3_Var6 = templ.NopComponent
180+ }
181+ ctx = templ.ClearChildren(ctx)
182+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<tr")
131183 if templ_7745c5c3_Err != nil {
132184 return templ_7745c5c3_Err
133185 }
134186 if r.Cursor {
135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " data-cursor")
187+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " data-cursor")
136188 if templ_7745c5c3_Err != nil {
137189 return templ_7745c5c3_Err
138190 }
139191 }
140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-kind\" aria-hidden=\"true\">")
192+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-kind\" aria-hidden=\"true\">")
141193 if templ_7745c5c3_Err != nil {
142194 return templ_7745c5c3_Err
143195 }
144- var templ_7745c5c3_Var5 string
145- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(r.Glyph)
196+ var templ_7745c5c3_Var7 string
197+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(r.Glyph)
146198 if templ_7745c5c3_Err != nil {
147- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 47, Col: 49}
199+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 58, Col: 49}
148200 }
149- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
201+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
150202 if templ_7745c5c3_Err != nil {
151203 return templ_7745c5c3_Err
152204 }
153- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</td><td class=\"c-name\">")
205+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td class=\"c-name\">")
154206 if templ_7745c5c3_Err != nil {
155207 return templ_7745c5c3_Err
156208 }
157209 if r.Href != "" {
158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"")
210+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<a href=\"")
159211 if templ_7745c5c3_Err != nil {
160212 return templ_7745c5c3_Err
161213 }
162- var templ_7745c5c3_Var6 templ.SafeURL
163- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
214+ var templ_7745c5c3_Var8 templ.SafeURL
215+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
164216 if templ_7745c5c3_Err != nil {
165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 50, Col: 35}
217+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 62, Col: 33}
166218 }
167- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
219+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
168220 if templ_7745c5c3_Err != nil {
169221 return templ_7745c5c3_Err
170222 }
171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
223+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"")
172224 if templ_7745c5c3_Err != nil {
173225 return templ_7745c5c3_Err
174226 }
175- var templ_7745c5c3_Var7 string
176- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
227+ if r.Expand != "" {
228+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " data-expand=\"")
229+ if templ_7745c5c3_Err != nil {
230+ return templ_7745c5c3_Err
231+ }
232+ var templ_7745c5c3_Var9 string
233+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Expand)
234+ if templ_7745c5c3_Err != nil {
235+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 28}
236+ }
237+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
238+ if templ_7745c5c3_Err != nil {
239+ return templ_7745c5c3_Err
240+ }
241+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" aria-expanded=\"false\"")
242+ if templ_7745c5c3_Err != nil {
243+ return templ_7745c5c3_Err
244+ }
245+ }
246+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">")
177247 if templ_7745c5c3_Err != nil {
178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 50, Col: 46}
248+ return templ_7745c5c3_Err
179249 }
180- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
250+ var templ_7745c5c3_Var10 string
251+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
252+ if templ_7745c5c3_Err != nil {
253+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 67, Col: 13}
254+ }
255+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
181256 if templ_7745c5c3_Err != nil {
182257 return templ_7745c5c3_Err
183258 }
184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a>")
259+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a>")
185260 if templ_7745c5c3_Err != nil {
186261 return templ_7745c5c3_Err
187262 }
188263 } else {
189- var templ_7745c5c3_Var8 string
190- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
264+ var templ_7745c5c3_Var11 string
265+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
191266 if templ_7745c5c3_Err != nil {
192- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 52, Col: 12}
267+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 69, Col: 12}
193268 }
194- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
269+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
195270 if templ_7745c5c3_Err != nil {
196271 return templ_7745c5c3_Err
197272 }
198273 }
199- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</td><td class=\"c-size num dim\">")
274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"c-size num dim\">")
200275 if templ_7745c5c3_Err != nil {
201276 return templ_7745c5c3_Err
202277 }
203- var templ_7745c5c3_Var9 string
204- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(r.Size)
278+ var templ_7745c5c3_Var12 string
279+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Size)
205280 if templ_7745c5c3_Err != nil {
206- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 55, Col: 37}
281+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 72, Col: 37}
207282 }
208- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
283+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
209284 if templ_7745c5c3_Err != nil {
210285 return templ_7745c5c3_Err
211286 }
212- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td><td class=\"c-sub dim\">")
287+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"c-sub dim\">")
213288 if templ_7745c5c3_Err != nil {
214289 return templ_7745c5c3_Err
215290 }
216291 if r.CHref != "" {
217- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a href=\"")
292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
218293 if templ_7745c5c3_Err != nil {
219294 return templ_7745c5c3_Err
220295 }
221- var templ_7745c5c3_Var10 templ.SafeURL
222- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
296+ var templ_7745c5c3_Var13 templ.SafeURL
297+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
223298 if templ_7745c5c3_Err != nil {
224- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 58, Col: 36}
299+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 75, Col: 36}
225300 }
226- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
301+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
227302 if templ_7745c5c3_Err != nil {
228303 return templ_7745c5c3_Err
229304 }
230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")
305+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
231306 if templ_7745c5c3_Err != nil {
232307 return templ_7745c5c3_Err
233308 }
234- var templ_7745c5c3_Var11 string
235- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
309+ var templ_7745c5c3_Var14 string
310+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
236311 if templ_7745c5c3_Err != nil {
237- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 58, Col: 50}
312+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 75, Col: 50}
238313 }
239- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
314+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
240315 if templ_7745c5c3_Err != nil {
241316 return templ_7745c5c3_Err
242317 }
243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a>")
318+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</a>")
244319 if templ_7745c5c3_Err != nil {
245320 return templ_7745c5c3_Err
246321 }
247322 }
248- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"c-who dim\">")
323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td class=\"c-who dim\">")
249324 if templ_7745c5c3_Err != nil {
250325 return templ_7745c5c3_Err
251326 }
252- var templ_7745c5c3_Var12 string
253- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Author)
327+ var templ_7745c5c3_Var15 string
328+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.Author)
254329 if templ_7745c5c3_Err != nil {
255- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 61, Col: 34}
330+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 78, Col: 34}
256331 }
257- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
332+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
258333 if templ_7745c5c3_Err != nil {
259334 return templ_7745c5c3_Err
260335 }
261- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"c-when nowrap dim\">")
336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td class=\"c-when nowrap dim\">")
262337 if templ_7745c5c3_Err != nil {
263338 return templ_7745c5c3_Err
264339 }
265340 if r.WhenAt != "" {
266- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<time datetime=\"")
341+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<time datetime=\"")
267342 if templ_7745c5c3_Err != nil {
268343 return templ_7745c5c3_Err
269344 }
270- var templ_7745c5c3_Var13 string
271- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
345+ var templ_7745c5c3_Var16 string
346+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
272347 if templ_7745c5c3_Err != nil {
273- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 29}
348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 81, Col: 29}
274349 }
275- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
350+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
276351 if templ_7745c5c3_Err != nil {
277352 return templ_7745c5c3_Err
278353 }
279- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" title=\"")
354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" title=\"")
280355 if templ_7745c5c3_Err != nil {
281356 return templ_7745c5c3_Err
282357 }
283- var templ_7745c5c3_Var14 string
284- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
358+ var templ_7745c5c3_Var17 string
359+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
285360 if templ_7745c5c3_Err != nil {
286- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 48}
361+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 81, Col: 48}
287362 }
288- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
363+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
289364 if templ_7745c5c3_Err != nil {
290365 return templ_7745c5c3_Err
291366 }
292- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
293368 if templ_7745c5c3_Err != nil {
294369 return templ_7745c5c3_Err
295370 }
296- var templ_7745c5c3_Var15 string
297- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
371+ var templ_7745c5c3_Var18 string
372+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
298373 if templ_7745c5c3_Err != nil {
299- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 59}
374+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 81, Col: 59}
300375 }
301- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
376+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
302377 if templ_7745c5c3_Err != nil {
303378 return templ_7745c5c3_Err
304379 }
305- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</time>")
380+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</time>")
306381 if templ_7745c5c3_Err != nil {
307382 return templ_7745c5c3_Err
308383 }
309384 }
310- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td></tr>")
385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td></tr>")
311386 if templ_7745c5c3_Err != nil {
312387 return templ_7745c5c3_Err
313388 }
@@ -45,15 +45,13 @@ func Tree(l Layout, p TreePage) templ.Component {
45 }()45 }()
46 }46 }
47 ctx = templ.InitializeContext(ctx)47 ctx = templ.InitializeContext(ctx)
48- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"filter\"><label for=\"tree-filter\" class=\"sr\">filter</label> <input id=\"tree-filter\" type=\"search\" data-filter placeholder=\"Filter this directory…\" autocomplete=\"off\" hx-on:input=\"let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')\"></div><table class=\"list tree\" aria-label=\"Entries\"><thead><tr><th class=\"cur\"></th><th class=\"c-kind\"></th><th class=\"c-name\">name</th><th class=\"c-size\">size</th><th class=\"c-sub\">last commit</th><th class=\"c-who\">author</th><th class=\"c-when\">date</th></tr></thead> <tbody id=\"tree-rows\" data-rows>")48+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"filter\"><label for=\"tree-filter\" class=\"sr\">filter</label> <input id=\"tree-filter\" type=\"search\" data-filter placeholder=\"Filter this directory…\" autocomplete=\"off\" hx-on:input=\"let t = this.value.toLowerCase(); q('#tree-rows > tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); take('data-cursor', '#tree-rows > tr:not([hidden])')\"></div><table class=\"list tree\" aria-label=\"Entries\"><thead><tr><th class=\"cur\"></th><th class=\"c-kind\"></th><th class=\"c-name\">name</th><th class=\"c-size\">size</th><th class=\"c-sub\">last commit</th><th class=\"c-who\">author</th><th class=\"c-when\">date</th></tr></thead> <tbody id=\"tree-rows\" data-rows hx-on:click=\"ermiteTree.click(event)\">")
49 if templ_7745c5c3_Err != nil {49 if templ_7745c5c3_Err != nil {
50 return templ_7745c5c3_Err50 return templ_7745c5c3_Err
51 }51 }
52- for _, r := range p.Rows {52+ templ_7745c5c3_Err = TreeRows(p).Render(ctx, templ_7745c5c3_Buffer)
53- templ_7745c5c3_Err = TreeRowView(r).Render(ctx, templ_7745c5c3_Buffer)53+ if templ_7745c5c3_Err != nil {
54- if templ_7745c5c3_Err != nil {54+ return templ_7745c5c3_Err
55- return templ_7745c5c3_Err
56- }
57 }55 }
58 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</tbody></table>")56 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</tbody></table>")
59 if templ_7745c5c3_Err != nil {57 if templ_7745c5c3_Err != nil {
@@ -73,7 +71,7 @@ func Tree(l Layout, p TreePage) templ.Component {
73 var templ_7745c5c3_Var3 string71 var templ_7745c5c3_Var3 string
74 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(n)72 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(n)
75 if templ_7745c5c3_Err != nil {73 if templ_7745c5c3_Err != nil {
76- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 34, Col: 22}74+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 32, Col: 22}
77 }75 }
78 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))76 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
79 if templ_7745c5c3_Err != nil {77 if templ_7745c5c3_Err != nil {
@@ -104,9 +102,10 @@ func Tree(l Layout, p TreePage) templ.Component {
104 })102 })
105 }103 }
106 104
107-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic105+// TreeRows is the rows of a directory alone: what files.js inserts under a
108-// link, + submodule, · file.106+// directory it opens in place. When the directory has more entries than are
109-func TreeRowView(r TreeRow) templ.Component {107+// listed, the last row links to its page.
108+func TreeRows(p TreePage) templ.Component {
110 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {109 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
111 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context110 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
112 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {111 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -127,187 +126,263 @@ func TreeRowView(r TreeRow) templ.Component {
127 templ_7745c5c3_Var4 = templ.NopComponent126 templ_7745c5c3_Var4 = templ.NopComponent
128 }127 }
129 ctx = templ.ClearChildren(ctx)128 ctx = templ.ClearChildren(ctx)
130- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr")129+ for _, r := range p.Rows {
130+ templ_7745c5c3_Err = TreeRowView(r).Render(ctx, templ_7745c5c3_Buffer)
131+ if templ_7745c5c3_Err != nil {
132+ return templ_7745c5c3_Err
133+ }
134+ }
135+ if p.More != "" {
136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr class=\"tree-more\"><td colspan=\"7\"><a href=\"")
137+ if templ_7745c5c3_Err != nil {
138+ return templ_7745c5c3_Err
139+ }
140+ var templ_7745c5c3_Var5 templ.SafeURL
141+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.More))
142+ if templ_7745c5c3_Err != nil {
143+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 48, Col: 71}
144+ }
145+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
146+ if templ_7745c5c3_Err != nil {
147+ return templ_7745c5c3_Err
148+ }
149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">more entries: open the directory</a></td></tr>")
150+ if templ_7745c5c3_Err != nil {
151+ return templ_7745c5c3_Err
152+ }
153+ }
154+ return nil
155+ })
156+}
157+
158+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
159+// in place), @ symbolic link, + submodule, · file. A directory link carries
160+// data-expand, the URL of its rows.
161+func TreeRowView(r TreeRow) templ.Component {
162+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
163+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
164+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
165+ return templ_7745c5c3_CtxErr
166+ }
167+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
168+ if !templ_7745c5c3_IsBuffer {
169+ defer func() {
170+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
171+ if templ_7745c5c3_Err == nil {
172+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
173+ }
174+ }()
175+ }
176+ ctx = templ.InitializeContext(ctx)
177+ templ_7745c5c3_Var6 := templ.GetChildren(ctx)
178+ if templ_7745c5c3_Var6 == nil {
179+ templ_7745c5c3_Var6 = templ.NopComponent
180+ }
181+ ctx = templ.ClearChildren(ctx)
182+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<tr")
131 if templ_7745c5c3_Err != nil {183 if templ_7745c5c3_Err != nil {
132 return templ_7745c5c3_Err184 return templ_7745c5c3_Err
133 }185 }
134 if r.Cursor {186 if r.Cursor {
135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " data-cursor")187+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " data-cursor")
136 if templ_7745c5c3_Err != nil {188 if templ_7745c5c3_Err != nil {
137 return templ_7745c5c3_Err189 return templ_7745c5c3_Err
138 }190 }
139 }191 }
140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-kind\" aria-hidden=\"true\">")192+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-kind\" aria-hidden=\"true\">")
141 if templ_7745c5c3_Err != nil {193 if templ_7745c5c3_Err != nil {
142 return templ_7745c5c3_Err194 return templ_7745c5c3_Err
143 }195 }
144- var templ_7745c5c3_Var5 string196+ var templ_7745c5c3_Var7 string
145- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(r.Glyph)197+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(r.Glyph)
146 if templ_7745c5c3_Err != nil {198 if templ_7745c5c3_Err != nil {
147- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 47, Col: 49}199+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 58, Col: 49}
148 }200 }
149- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))201+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
150 if templ_7745c5c3_Err != nil {202 if templ_7745c5c3_Err != nil {
151 return templ_7745c5c3_Err203 return templ_7745c5c3_Err
152 }204 }
153- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</td><td class=\"c-name\">")205+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td class=\"c-name\">")
154 if templ_7745c5c3_Err != nil {206 if templ_7745c5c3_Err != nil {
155 return templ_7745c5c3_Err207 return templ_7745c5c3_Err
156 }208 }
157 if r.Href != "" {209 if r.Href != "" {
158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"")210+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<a href=\"")
159 if templ_7745c5c3_Err != nil {211 if templ_7745c5c3_Err != nil {
160 return templ_7745c5c3_Err212 return templ_7745c5c3_Err
161 }213 }
162- var templ_7745c5c3_Var6 templ.SafeURL214+ var templ_7745c5c3_Var8 templ.SafeURL
163- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))215+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
164 if templ_7745c5c3_Err != nil {216 if templ_7745c5c3_Err != nil {
165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 50, Col: 35}217+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 62, Col: 33}
166 }218 }
167- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))219+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
168 if templ_7745c5c3_Err != nil {220 if templ_7745c5c3_Err != nil {
169 return templ_7745c5c3_Err221 return templ_7745c5c3_Err
170 }222 }
171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")223+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"")
172 if templ_7745c5c3_Err != nil {224 if templ_7745c5c3_Err != nil {
173 return templ_7745c5c3_Err225 return templ_7745c5c3_Err
174 }226 }
175- var templ_7745c5c3_Var7 string227+ if r.Expand != "" {
176- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)228+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " data-expand=\"")
229+ if templ_7745c5c3_Err != nil {
230+ return templ_7745c5c3_Err
231+ }
232+ var templ_7745c5c3_Var9 string
233+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Expand)
234+ if templ_7745c5c3_Err != nil {
235+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 28}
236+ }
237+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
238+ if templ_7745c5c3_Err != nil {
239+ return templ_7745c5c3_Err
240+ }
241+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" aria-expanded=\"false\"")
242+ if templ_7745c5c3_Err != nil {
243+ return templ_7745c5c3_Err
244+ }
245+ }
246+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">")
177 if templ_7745c5c3_Err != nil {247 if templ_7745c5c3_Err != nil {
178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 50, Col: 46}248+ return templ_7745c5c3_Err
179 }249 }
180- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))250+ var templ_7745c5c3_Var10 string
251+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
252+ if templ_7745c5c3_Err != nil {
253+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 67, Col: 13}
254+ }
255+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
181 if templ_7745c5c3_Err != nil {256 if templ_7745c5c3_Err != nil {
182 return templ_7745c5c3_Err257 return templ_7745c5c3_Err
183 }258 }
184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a>")259+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a>")
185 if templ_7745c5c3_Err != nil {260 if templ_7745c5c3_Err != nil {
186 return templ_7745c5c3_Err261 return templ_7745c5c3_Err
187 }262 }
188 } else {263 } else {
189- var templ_7745c5c3_Var8 string264+ var templ_7745c5c3_Var11 string
190- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)265+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
191 if templ_7745c5c3_Err != nil {266 if templ_7745c5c3_Err != nil {
192- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 52, Col: 12}267+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 69, Col: 12}
193 }268 }
194- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))269+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
195 if templ_7745c5c3_Err != nil {270 if templ_7745c5c3_Err != nil {
196 return templ_7745c5c3_Err271 return templ_7745c5c3_Err
197 }272 }
198 }273 }
199- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</td><td class=\"c-size num dim\">")274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"c-size num dim\">")
200 if templ_7745c5c3_Err != nil {275 if templ_7745c5c3_Err != nil {
201 return templ_7745c5c3_Err276 return templ_7745c5c3_Err
202 }277 }
203- var templ_7745c5c3_Var9 string278+ var templ_7745c5c3_Var12 string
204- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(r.Size)279+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Size)
205 if templ_7745c5c3_Err != nil {280 if templ_7745c5c3_Err != nil {
206- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 55, Col: 37}281+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 72, Col: 37}
207 }282 }
208- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))283+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
209 if templ_7745c5c3_Err != nil {284 if templ_7745c5c3_Err != nil {
210 return templ_7745c5c3_Err285 return templ_7745c5c3_Err
211 }286 }
212- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td><td class=\"c-sub dim\">")287+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"c-sub dim\">")
213 if templ_7745c5c3_Err != nil {288 if templ_7745c5c3_Err != nil {
214 return templ_7745c5c3_Err289 return templ_7745c5c3_Err
215 }290 }
216 if r.CHref != "" {291 if r.CHref != "" {
217- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a href=\"")292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
218 if templ_7745c5c3_Err != nil {293 if templ_7745c5c3_Err != nil {
219 return templ_7745c5c3_Err294 return templ_7745c5c3_Err
220 }295 }
221- var templ_7745c5c3_Var10 templ.SafeURL296+ var templ_7745c5c3_Var13 templ.SafeURL
222- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))297+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
223 if templ_7745c5c3_Err != nil {298 if templ_7745c5c3_Err != nil {
224- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 58, Col: 36}299+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 75, Col: 36}
225 }300 }
226- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))301+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
227 if templ_7745c5c3_Err != nil {302 if templ_7745c5c3_Err != nil {
228 return templ_7745c5c3_Err303 return templ_7745c5c3_Err
229 }304 }
230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")305+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
231 if templ_7745c5c3_Err != nil {306 if templ_7745c5c3_Err != nil {
232 return templ_7745c5c3_Err307 return templ_7745c5c3_Err
233 }308 }
234- var templ_7745c5c3_Var11 string309+ var templ_7745c5c3_Var14 string
235- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)310+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Subject)
236 if templ_7745c5c3_Err != nil {311 if templ_7745c5c3_Err != nil {
237- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 58, Col: 50}312+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 75, Col: 50}
238 }313 }
239- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))314+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
240 if templ_7745c5c3_Err != nil {315 if templ_7745c5c3_Err != nil {
241 return templ_7745c5c3_Err316 return templ_7745c5c3_Err
242 }317 }
243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a>")318+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</a>")
244 if templ_7745c5c3_Err != nil {319 if templ_7745c5c3_Err != nil {
245 return templ_7745c5c3_Err320 return templ_7745c5c3_Err
246 }321 }
247 }322 }
248- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"c-who dim\">")323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td class=\"c-who dim\">")
249 if templ_7745c5c3_Err != nil {324 if templ_7745c5c3_Err != nil {
250 return templ_7745c5c3_Err325 return templ_7745c5c3_Err
251 }326 }
252- var templ_7745c5c3_Var12 string327+ var templ_7745c5c3_Var15 string
253- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Author)328+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.Author)
254 if templ_7745c5c3_Err != nil {329 if templ_7745c5c3_Err != nil {
255- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 61, Col: 34}330+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 78, Col: 34}
256 }331 }
257- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))332+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
258 if templ_7745c5c3_Err != nil {333 if templ_7745c5c3_Err != nil {
259 return templ_7745c5c3_Err334 return templ_7745c5c3_Err
260 }335 }
261- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"c-when nowrap dim\">")336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td class=\"c-when nowrap dim\">")
262 if templ_7745c5c3_Err != nil {337 if templ_7745c5c3_Err != nil {
263 return templ_7745c5c3_Err338 return templ_7745c5c3_Err
264 }339 }
265 if r.WhenAt != "" {340 if r.WhenAt != "" {
266- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<time datetime=\"")341+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<time datetime=\"")
267 if templ_7745c5c3_Err != nil {342 if templ_7745c5c3_Err != nil {
268 return templ_7745c5c3_Err343 return templ_7745c5c3_Err
269 }344 }
270- var templ_7745c5c3_Var13 string345+ var templ_7745c5c3_Var16 string
271- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)346+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
272 if templ_7745c5c3_Err != nil {347 if templ_7745c5c3_Err != nil {
273- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 29}348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 81, Col: 29}
274 }349 }
275- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)350+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
276 if templ_7745c5c3_Err != nil {351 if templ_7745c5c3_Err != nil {
277 return templ_7745c5c3_Err352 return templ_7745c5c3_Err
278 }353 }
279- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" title=\"")354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" title=\"")
280 if templ_7745c5c3_Err != nil {355 if templ_7745c5c3_Err != nil {
281 return templ_7745c5c3_Err356 return templ_7745c5c3_Err
282 }357 }
283- var templ_7745c5c3_Var14 string358+ var templ_7745c5c3_Var17 string
284- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)359+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
285 if templ_7745c5c3_Err != nil {360 if templ_7745c5c3_Err != nil {
286- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 48}361+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 81, Col: 48}
287 }362 }
288- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)363+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
289 if templ_7745c5c3_Err != nil {364 if templ_7745c5c3_Err != nil {
290 return templ_7745c5c3_Err365 return templ_7745c5c3_Err
291 }366 }
292- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
293 if templ_7745c5c3_Err != nil {368 if templ_7745c5c3_Err != nil {
294 return templ_7745c5c3_Err369 return templ_7745c5c3_Err
295 }370 }
296- var templ_7745c5c3_Var15 string371+ var templ_7745c5c3_Var18 string
297- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)372+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
298 if templ_7745c5c3_Err != nil {373 if templ_7745c5c3_Err != nil {
299- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 64, Col: 59}374+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/tree.templ`, Line: 81, Col: 59}
300 }375 }
301- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))376+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
302 if templ_7745c5c3_Err != nil {377 if templ_7745c5c3_Err != nil {
303 return templ_7745c5c3_Err378 return templ_7745c5c3_Err
304 }379 }
305- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</time>")380+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</time>")
306 if templ_7745c5c3_Err != nil {381 if templ_7745c5c3_Err != nil {
307 return templ_7745c5c3_Err382 return templ_7745c5c3_Err
308 }383 }
309 }384 }
310- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td></tr>")385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td></tr>")
311 if templ_7745c5c3_Err != nil {386 if templ_7745c5c3_Err != nil {
312 return templ_7745c5c3_Err387 return templ_7745c5c3_Err
313 }388 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 44

↑↓ moveenter openesc close