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/web/templates/code.html +128−95

@@ -2,26 +2,31 @@
2 2 {{template "header" .}}
3 3 {{template "repohead" (nav .Name "code")}}
4 4
5 <div class="repo-sub">
6 <form method="get" action="/r/{{.Name}}/code" class="inline">
7 <select name="ref" data-autosubmit aria-label="Branch">
8 {{range .Branches}}<option value="{{.}}"{{if eq . $.Ref}} selected{{end}}>{{.}}</option>{{end}}
9 </select>
10 <noscript><button class="secondary" type="submit">Go</button></noscript>
11 </form>
12 <span class="item">{{.Count}} commit(s)</span>
13 <details class="clone">
14 <summary>{{icon "copy"}}Clone</summary>
15 <div class="clone-pop">
16 <p class="muted">Read only, no credentials:</p>
17 <code>{{.CloneURL}}</code>
18 <p class="muted">Write over SSH with a registered key:</p>
19 <code>git clone {{.SSHURL}}</code>
20 <p><button class="secondary" type="button" data-copy="{{.CloneURL}}">{{icon "copy"}}Copy HTTPS URL</button></p>
21 </div>
22 </details>
23 <a class="button secondary" href="/r/{{.Name}}/commits/{{esc .Ref}}">{{icon "history"}}History</a>
24 {{if .Site}}<a class="item" href="{{.Site}}">Site</a>{{end}}
5 <div class="page-head">
6 <div class="page-head-main">
7 <h1>Code</h1>
8 <p class="page-sub">{{.Count}} commit(s) on <code>{{.Ref}}</code></p>
9 </div>
10 <div class="page-head-actions">
11 <form method="get" action="/r/{{.Name}}/code" class="inline">
12 <select name="ref" data-autosubmit aria-label="Branch">
13 {{range .Branches}}<option value="{{.}}"{{if eq . $.Ref}} selected{{end}}>{{.}}</option>{{end}}
14 </select>
15 <noscript><button class="secondary" type="submit">Go</button></noscript>
16 </form>
17 <details class="clone">
18 <summary>{{icon "copy"}}Clone</summary>
19 <div class="clone-pop">
20 <p class="muted">Read only, no credentials:</p>
21 <code>{{.CloneURL}}</code>
22 <p class="muted">Write over SSH with a registered key:</p>
23 <code>git clone {{.SSHURL}}</code>
24 <p><button class="secondary" type="button" data-copy="{{.CloneURL}}">{{icon "copy"}}Copy HTTPS URL</button></p>
25 </div>
26 </details>
27 <a class="button secondary" href="/r/{{.Name}}/commits/{{esc .Ref}}">{{icon "history"}}History</a>
28 {{if .Site}}<a class="button secondary" href="{{.Site}}">{{icon "external-link"}}Site</a>{{end}}
29 </div>
25 30 </div>
26 31
27 32 {{range .Latest}}
@@ -32,83 +37,123 @@
32 37 </div>
33 38 {{end}}
34 39
35 <h2>Files{{if .Ref}} <span class="muted">on {{.Ref}}</span>{{end}}</h2>
36 {{if .Entries}}
37 <div class="panel">
38 <table>
39 <tbody>
40 {{range .Entries}}
41 <tr>
40 <div class="code-browser">
41 {{template "code_tree" .}}
42 {{template "code_main" .}}
43 </div>
44
45 {{template "footer" .}}
46 {{end}}
47
48 {{define "code_tree"}}
49 <aside class="file-tree" id="file-tree" aria-label="Files">
50 <div class="tree-head">
51 <span>{{icon "folder-open"}}Files</span>
52 <span class="muted">{{.Ref}}</span>
53 </div>
54 {{if .Tree}}
55 <div class="tree-scroll">
56 {{template "tree_nodes" (treeContext .Name .Ref .Tree)}}
57 </div>
58 {{if .TreeTruncated}}<p class="tree-note">Showing the first 4,000 files.</p>{{end}}
59 {{else}}
60 <p class="tree-empty muted">{{if .Commit}}No files on this branch.{{else}}Push a branch to browse it.{{end}}</p>
61 {{end}}
62 </aside>
63 {{end}}
64
65 {{define "tree_nodes"}}
66 <ul class="tree-list">
67 {{range .Nodes}}
68 <li>
42 69 {{if eq .Type "tree"}}
43 <td><a class="dir" href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{.Name}}">{{.Name}}/</a></td>
70 <details class="tree-dir{{if .Active}} active{{end}}" data-path="{{.Path}}"{{if .Open}} open{{end}}>
71 <summary class="tree-row">
72 <span class="tree-chevron">{{icon "chevron-right"}}</span>
73 <span class="tree-icon">{{icon "folder"}}</span>
74 <span class="tree-name" title="{{.Path}}">{{.Name}}</span>
75 </summary>
76 <div class="tree-children">{{template "tree_nodes" (treeContext $.Name $.Ref .Children)}}</div>
77 </details>
44 78 {{else}}
45 <td><a href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{.Name}}">{{.Name}}</a></td>
79 <a class="tree-row tree-file{{if .Active}} active{{end}}" data-path="{{.Path}}" href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl .Path}}" hx-get="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl .Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true" title="{{.Path}}">
80 <span class="tree-icon">{{icon "file"}}</span>
81 <span class="tree-name">{{.Name}}</span>
82 </a>
46 83 {{end}}
47 </tr>
84 </li>
48 85 {{end}}
49 </tbody>
50 </table>
51 </div>
52 {{else}}
53 <p class="muted">This repository is empty. Push a branch to fill it:</p>
54 <pre>git push origin HEAD:refs/heads/{{.Ref}}</pre>
86 </ul>
55 87 {{end}}
56 88
57 {{if .Readme}}
58 <div class="filehead">
59 <span>{{.ReadmeName}}</span>
60 <a href="/r/{{.Name}}/blob/{{esc .Ref}}/{{.ReadmeName}}">view source</a>
61 </div>
62 <article class="md readme">{{.Readme}}</article>
89 {{define "code_main"}}
90 <section id="code-main" class="code-main" data-path="{{.Path}}" data-mode="{{.Mode}}" data-ref="{{.Ref}}">
91 {{if eq .Mode "blob"}}{{template "code_blob" .}}
92 {{else if eq .Mode "tree"}}{{template "code_dir" .}}
93 {{else}}{{template "code_overview" .}}{{end}}
94 </section>
63 95 {{end}}
64 96
65 {{if .ContributorBranches}}
66 <h2>Contributor branches</h2>
67 <p class="muted">CI does not run for these branches until a maintainer allows the current commit.</p>
68 <div class="panel">
69 <table>
70 <thead><tr><th>Branch</th><th>CI</th></tr></thead>
71 <tbody>
72 {{range .ContributorBranches}}
73 <tr>
74 <td><code>{{.}}</code></td>
75 <td>
76 {{if $.CanAllowCI}}
77 <form method="post" action="/r/{{$.Name}}/ci/allow">
78 <input type="hidden" name="csrf" value="{{$.CSRF}}">
79 <input type="hidden" name="ref" value="refs/heads/{{.}}">
80 <button type="submit">Allow CI for this tip</button>
81 </form>
82 {{else}}<span class="muted">waiting for a maintainer to allow it</span>{{end}}
83 </td>
84 </tr>
85 {{end}}
86 </tbody>
87 </table>
97 {{define "code_tabbar"}}
98 <div class="code-tabbar">
99 <span class="code-tab-icon">{{if or (eq .Mode "tree") (and (eq .Mode "overview") (not .Readme))}}{{icon "folder"}}{{else if eq .Mode "blob"}}{{icon "file"}}{{else}}{{icon "book-open"}}{{end}}</span>
100 <nav class="code-crumbs" aria-label="Breadcrumb">
101 <a href="/r/{{.Name}}/code?ref={{esc .Ref}}" hx-get="/r/{{.Name}}/code?ref={{esc .Ref}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{.Ref}}</a>
102 {{range $i, $c := .Crumbs}}<span class="sep">/</span>{{if eq (inc $i) (len $.Crumbs)}}<span class="current" title="{{$c.Path}}">{{$c.Name}}</span>{{else}}<a href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl $c.Path}}" hx-get="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl $c.Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{$c.Name}}</a>{{end}}{{end}}
103 {{if not .Crumbs}}<span class="sep">/</span><span class="current">Files</span>{{end}}
104 </nav>
105 <span class="code-tab-actions">
106 {{if eq .Mode "blob"}}
107 {{if and .IsMarkdown (not .Truncated)}}
108 <a class="{{if not .Preview}}active{{end}}" href="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}" hx-get="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">source</a>
109 <a class="{{if .Preview}}active{{end}}" href="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}?preview=1" hx-get="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}?preview=1" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">preview</a>
110 {{end}}
111 <a href="/r/{{.Name}}/raw/{{esc .Ref}}/{{pathurl .Path}}">raw</a>
112 <span class="muted">{{bytes .Size}}</span>
113 {{else if and (eq .Mode "overview") .Readme}}
114 <a href="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .ReadmeName}}">source</a>
115 {{else if .Entries}}
116 <span class="muted">{{len .Entries}} item(s)</span>
117 {{end}}
118 </span>
88 119 </div>
89 120 {{end}}
90 121
91 {{template "footer" .}}
122 {{define "code_overview"}}
123 {{if .Readme}}
124 {{template "code_tabbar" .}}
125 <article class="md readme">{{.Readme}}</article>
126 {{else if not .Commit}}
127 {{template "code_tabbar" .}}
128 <div class="card empty-code">
129 <h2>Empty repository</h2>
130 <p class="muted">Nothing has been pushed yet. Add this repository and push a branch:</p>
131 <pre>git remote add origin {{.SSHURL}}
132 git push -u origin {{.Ref}}</pre>
133 </div>
134 {{else}}
135 {{template "code_dir" .}}
136 {{end}}
92 137 {{end}}
93 138
94 {{define "tree"}}
95 {{template "header" .}}
96 {{template "repohead" (nav .Name "code")}}
97 <p class="crumbs">
98 <a href="/r/{{.Name}}/code?ref={{esc .Ref}}">{{.Ref}}</a>
99 {{range .Crumbs}} / <a href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{.Path}}">{{.Name}}</a>{{end}}
100 </p>
101 {{if .Entries}}
102 <div class="panel">
139 {{define "code_dir"}}
140 {{template "code_tabbar" .}}
141 {{if or .Entries .Path}}
142 <div class="panel dirlist">
103 143 <table>
104 144 <tbody>
145 {{if .Path}}
146 <tr><td><a class="dir parent" href="{{if .Parent}}/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl .Parent}}{{else}}/r/{{$.Name}}/tree/{{esc $.Ref}}{{end}}" hx-get="{{if .Parent}}/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl .Parent}}{{else}}/r/{{$.Name}}/tree/{{esc $.Ref}}{{end}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">..</a></td></tr>
147 {{end}}
105 148 {{range .Entries}}
106 149 <tr>
107 {{if eq .Type "tree"}}
108 <td><a class="dir" href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{$.Path}}{{if $.Path}}/{{end}}{{.Name}}">{{.Name}}/</a></td>
109 {{else}}
110 <td><a href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{$.Path}}{{if $.Path}}/{{end}}{{.Name}}">{{.Name}}</a></td>
111 {{end}}
150 <td>
151 {{if eq .Type "tree"}}
152 <a class="dir" href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-get="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{icon "folder"}}{{.Name}}/</a>
153 {{else}}
154 <a class="file" href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-get="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{icon "file"}}{{.Name}}</a>
155 {{end}}
156 </td>
112 157 </tr>
113 158 {{end}}
114 159 </tbody>
@@ -117,25 +162,14 @@
117 162 {{else}}
118 163 <p class="muted">This directory is empty.</p>
119 164 {{end}}
120 {{template "footer" .}}
121 165 {{end}}
122 166
123 {{define "blob"}}
124 {{template "header" .}}
125 {{template "repohead" (nav .Name "code")}}
126 <h1 class="path-title">{{.Path}}</h1>
127 <p class="crumbs">
128 <a href="/r/{{.Name}}/code?ref={{esc .Ref}}">{{.Ref}}</a>
129 {{range .Crumbs}} / <a href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{.Path}}">{{.Name}}</a>{{end}}
130 <span class="crumb-actions">
131 <a href="/r/{{.Name}}/raw/{{esc .Ref}}/{{.Path}}">raw</a>
132 <span class="muted">{{bytes .Size}}</span>
133 </span>
134 </p>
167 {{define "code_blob"}}
168 {{template "code_tabbar" .}}
135 169 {{if .Binary}}
136 170 <div class="card">
137 171 <p class="muted">Binary file · {{bytes .Size}}. It is not shown inline.</p>
138 <p><a class="button secondary" href="/r/{{.Name}}/raw/{{esc .Ref}}/{{.Path}}">Download</a></p>
172 <p><a class="button secondary" href="/r/{{.Name}}/raw/{{esc .Ref}}/{{pathurl .Path}}">Download</a></p>
139 173 </div>
140 174 {{else}}
141 175 {{if .Truncated}}<p class="muted">Showing the first 1 MiB.</p>{{end}}
@@ -154,7 +188,6 @@
154 188 </div>
155 189 {{end}}
156 190 {{end}}
157 {{template "footer" .}}
158 191 {{end}}
159 192
160 193 {{define "commits"}}