Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Turn the code tab into an editor-style file browser

a464f8927bfb3edcf170694f120dc9b3f68320da · Keenan Nemetz · 15d ago · parent f09ef39e

Turn the code tab into an editor-style file browser

- Add a persistent nested file tree with HTMX file and folder swaps, active
  rows, expanded ancestors, and deep-link support.
- Share one page header across every repository tab: title and summary on the
  left, tab actions grouped on the right.
- Make the repository name the first tab and show visibility with a lock or
  globe icon derived from the ACL.
- Open markdown as syntax-highlighted source by default, with an explicit
  full-width preview toggle.
- Point the branch breadcrumb at the Code overview instead of a second root
  directory listing.
- Move contributor-branch CI approval from the code browser to the Runs tab.
- Extend UI tests for the file tree, fragments, repo visibility, markdown
  source/preview, shared headers, and the moved CI control.

server/ui_test.go +95−1

@@ -39,11 +39,34 @@ func TestRepositoryPagesShareTheTabHeader(t *testing.T) {
39 39 if code != 200 {
40 40 t.Fatalf("%s = %d", path, code)
41 41 }
42 for _, want := range []string{">Overview<", ">Activity<", ">Code<", ">Issues<", ">Changes<", ">Releases<", ">Runs<", ">Locks<"} {
42 for _, want := range []string{`class="repotab`, ">Activity<", ">Code<", ">Issues<", ">Changes<", ">Releases<", ">Runs<", ">Locks<"} {
43 43 if !strings.Contains(body, want) {
44 44 t.Fatalf("%s is missing the %s tab:\n%s", path, want, body)
45 45 }
46 46 }
47 if !strings.Contains(body, `class="page-head"`) {
48 t.Fatalf("%s is missing the shared page header:\n%s", path, body)
49 }
50 }
51 }
52
53 func TestRepoTabShowsNameAndVisibility(t *testing.T) {
54 t.Parallel()
55 f := newFixture(t)
56
57 // A new repository is private until its ACL default is raised to read.
58 _, cookie := f.browserSession(t, f.ownerID)
59 resp, body := f.browserGet(t, "/r/"+f.repo, cookie)
60 if resp.StatusCode != 200 || !strings.Contains(body, `class="repotab active"`) ||
61 !strings.Contains(body, `title="Private repository"`) || !strings.Contains(body, ">demo</a>") {
62 t.Fatalf("private repo tab = %d:\n%s", resp.StatusCode, body)
63 }
64
65 f.publish(f.repo)
66 _, body = f.get("/r/" + f.repo + "/code")
67 if !strings.Contains(body, `title="Public repository"`) ||
68 !strings.Contains(body, `aria-label="demo (public repository)"`) {
69 t.Fatalf("public repo tab does not show the globe state:\n%s", body)
47 70 }
48 71 }
49 72
@@ -114,6 +137,77 @@ func TestOverviewAndActivityPages(t *testing.T) {
114 137 }
115 138 }
116 139
140 func TestCodeBrowserTreeAndFragments(t *testing.T) {
141 t.Parallel()
142 f := newFixture(t)
143 f.publish(f.repo)
144 work := workRepo(t, map[string]string{
145 "README.md": "# Code browser\n",
146 "main.go": "package main\n",
147 "internal/run.go": "package internal\n",
148 })
149 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
150 t.Fatalf("push: %s", errs)
151 }
152
153 code, body := f.get("/r/" + f.repo + "/code")
154 if code != 200 {
155 t.Fatalf("code browser = %d", code)
156 }
157 for _, want := range []string{
158 `id="file-tree"`, `id="code-main"`, `data-path="internal/run.go"`,
159 `data-path="README.md"`, "tree-file active", "internal",
160 } {
161 if !strings.Contains(body, want) {
162 t.Fatalf("code browser does not contain %q:\n%s", want, body)
163 }
164 }
165
166 // A file click is a fragment: the tree stays put and the main pane is replaced.
167 resp, fragment := f.htmxGet(t, "/r/"+f.repo+"/blob/main/internal/run.go", "")
168 if resp.StatusCode != 200 || !strings.Contains(fragment, `id="code-main"`) ||
169 !strings.Contains(fragment, `data-mode="blob"`) ||
170 strings.Contains(fragment, "<!doctype html>") ||
171 strings.Contains(fragment, `id="file-tree"`) ||
172 !strings.Contains(fragment, `class="chroma"`) {
173 t.Fatalf("blob fragment = %d:\n%s", resp.StatusCode, fragment)
174 }
175
176 // A directory click replaces the main pane with a listing whose child links
177 // carry the directory path rather than only the leaf name.
178 resp, fragment = f.htmxGet(t, "/r/"+f.repo+"/tree/main/internal", "")
179 if resp.StatusCode != 200 || !strings.Contains(fragment, `data-mode="tree"`) ||
180 !strings.Contains(fragment, "/r/"+f.repo+"/blob/main/internal/run.go") {
181 t.Fatalf("tree fragment = %d:\n%s", resp.StatusCode, fragment)
182 }
183
184 // The branch crumb returns to the Code overview, not a second directory
185 // listing inside the main pane.
186 resp, fragment = f.htmxGet(t, "/r/"+f.repo+"/code?ref=main", "")
187 if resp.StatusCode != 200 || !strings.Contains(fragment, `data-mode="overview"`) ||
188 !strings.Contains(fragment, "<h1>Code browser</h1>") {
189 t.Fatalf("branch crumb fragment = %d:\n%s", resp.StatusCode, fragment)
190 }
191 if !strings.Contains(body, `href="/r/`+f.repo+`/code?ref=main"`) {
192 t.Fatalf("branch crumb does not target the code overview:\n%s", body)
193 }
194
195 // The branch root still has its own directory URL, used by the ".." row.
196 resp, fragment = f.htmxGet(t, "/r/"+f.repo+"/tree/main", "")
197 if resp.StatusCode != 200 || !strings.Contains(fragment, `data-mode="tree"`) ||
198 !strings.Contains(fragment, "main.go") {
199 t.Fatalf("root tree fragment = %d:\n%s", resp.StatusCode, fragment)
200 }
201
202 // A direct directory visit renders the same shell, with the folder open in
203 // the tree and a link back to its parent.
204 code, body = f.get("/r/" + f.repo + "/tree/main/internal")
205 if code != 200 || !strings.Contains(body, `id="file-tree"`) ||
206 !strings.Contains(body, "internal/run.go") || !strings.Contains(body, "..") {
207 t.Fatalf("directory page = %d:\n%s", code, body)
208 }
209 }
210
117 211 func TestSourceBlobIsHighlightedAndEscaped(t *testing.T) {
118 212 t.Parallel()
119 213 f := newFixture(t)