1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5// Commit is a commit: the message and facts, the diff tools, the diff.
6templ Commit(l Layout, p CommitPage) {
7 @Shell(l, commitRail(p)) {
8 <header class="show">
9 <h1>{ p.Subject }</h1>
10 if p.Body != "" {
11 <pre class="msg">{ p.Body }</pre>
12 }
13 <dl class="kv show-kv">
14 <dt>commit</dt>
15 <dd>{ p.SHA }</dd>
16 <dt>author</dt>
17 <dd>@personView(p.Author)</dd>
18 if p.Committer != nil {
19 <dt>committer</dt>
20 <dd>@personView(*p.Committer)</dd>
21 }
22 <dt>
23 if p.Merge {
24 merge of
25 } else {
26 parent
27 }
28 </dt>
29 <dd>
30 if p.Root {
31 none (root commit)
32 }
33 for i, par := range p.Parents {
34 if i > 0 {
35 { " + " }
36 }
37 <a href={ templ.SafeURL(par.Href) }>{ par.Label }</a>
38 }
39 if p.Merge {
40 { " · diff against the first parent" }
41 }
42 </dd>
43 if len(p.Refs) > 0 {
44 <dt>refs</dt>
45 <dd>
46 for _, r := range p.Refs {
47 [{ r }]{ " " }
48 }
49 </dd>
50 }
51 </dl>
52 </header>
53 <div class="diff-tools" role="toolbar" aria-label="Diff view">
54 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
55 <span class="grow"></span>
56 @viewLink(p, "unified", "[u]nified", "u", p.UnifiedHref)
57 @viewLink(p, "split", "[d] split", "d", p.SplitHref)
58 <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button>
59 </div>
60 if p.Invisible {
61 <p class="notice">
62 this diff holds invisible characters (direction controls, zero-width characters). They are shown as
63 <span class="inv"><U+XXXX></span> markers, never applied.
64 </p>
65 }
66 if p.Note != "" {
67 <p class="notice">{ p.Note }</p>
68 }
69 <div
70 id="diffs"
71 if p.Both {
72 data-diff={ p.View }
73 } else {
74 data-view={ p.View }
75 }
76 if p.Both && !p.Pinned {
77 hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"
78 }
79 >
80 for _, f := range p.Files {
81 @DiffFileView(f, p)
82 }
83 if len(p.Files) == 0 {
84 <p class="note">no changed files{ " " }
85 if p.Merge {
86 against the first parent.
87 } else {
88 in this commit.
89 }
90 </p>
91 }
92 </div>
93 }
94}
95
96// viewLink is a button of the diff tools. It is a link, so that the split
97// layout works without JavaScript; with it, and when both layouts are in the
98// page, a click only flips the layout.
99templ viewLink(p CommitPage, view, label, key, href string) {
100 <a
101 href={ templ.SafeURL(href) }
102 class="vbtn"
103 data-view={ view }
104 aria-keyshortcuts={ key }
105 if p.View == view {
106 aria-current="true"
107 }
108 if p.Both {
109 hx-on:click="event.preventDefault(); document.body.dispatchEvent(new CustomEvent('k-' + this.dataset.view))"
110 :aria-current={ "q('#diffs').attr['data-diff'] === '" + view + "' ? 'true' : false" }
111 }
112 >{ label }</a>
113}
114
115templ personView(pr Person) {
116 { pr.Name }
117 if pr.WhenAt != "" {
118 { " · " }<time datetime={ pr.WhenAt } title={ pr.Abs }>{ pr.When }</time>{ " · " + pr.Abs }
119 }
120}
121
122templ commitRail(p CommitPage) {
123 <section aria-labelledby="r-files">
124 <h2 id="r-files">files <span>{ itoa(len(p.Files)) }</span></h2>
125 @FileList(p.Files)
126 </section>
127}
128
129// FileList is the table of contents of a diff: status letter, path, +n -m
130// and an ASCII bar. The row of the file in view is marked.
131templ FileList(files []DiffFile) {
132 <ol class="files" aria-label="Changed files">
133 for _, f := range files {
134 <li>
135 <a
136 href={ templ.SafeURL("#" + f.ID) }
137 :aria-current={ "(() => { let all = q('.dfile').arr(); let cur = all.filter(f => f.getBoundingClientRect().top < innerHeight / 3).at(-1) || all[0]; return cur?.id === '" + f.ID + "' ? 'true' : false })()" }
138 hx-on:click="document.getElementById(this.hash.slice(1)).open = true"
139 >
140 <span class="st" title={ f.Status }>{ f.Status }</span>
141 <span class="p" title={ f.Path }>{ f.Path }</span>
142 <span class="n"><span class="plus">+{ itoa(f.Add) }</span> <span class="minus">-{ itoa(f.Del) }</span></span>
143 <span class="b" aria-hidden="true"><span class="plus">{ f.Bar.Add }</span><span class="minus">{ f.Bar.Del }</span>{ f.Bar.Rest }</span>
144 </a>
145 </li>
146 }
147 </ol>
148}
149
150// DiffFileView is one file of the diff: a <details> (per-file collapse). The
151// unified and the split tables are both in it; #diffs[data-diff] shows one.
152// Every changed line keeps its +/- marker, so a change is never conveyed by
153// colour alone.
154templ DiffFileView(f DiffFile, p CommitPage) {
155 <details
156 class="dfile"
157 id={ f.ID }
158 data-file
159 if f.Open {
160 open
161 }
162 >
163 <summary>
164 <span class="st">{ f.Status }</span>{ " " }<span class="p">{ f.Path }</span>{ " " }<span class="plus">+{ itoa(f.Add) }</span>{ " " }<span class="minus">-{ itoa(f.Del) }</span>
165 if f.Note != "" {
166 { " " }<span class="note">{ f.Note }</span>
167 }
168 </summary>
169 if len(f.Hunks) > 0 {
170 <div class="dscroll">
171 if p.ShowUnified {
172 <table class="dt uni" aria-label={ f.Path + ", unified" }>
173 for _, h := range f.Hunks {
174 <tbody>
175 <tr class="hunk"><td colspan="4">{ h.Header }</td></tr>
176 for _, ln := range h.Lines {
177 <tr class={ ln.Kind }>
178 <td class="ln">{ ln.Old }</td>
179 <td class="ln">{ ln.New }</td>
180 <td class="m">{ ln.Mark }</td>
181 <td class="tx">@codeLine(ln)</td>
182 </tr>
183 }
184 </tbody>
185 }
186 </table>
187 }
188 if p.ShowSplit {
189 <table class="dt spl" aria-label={ f.Path + ", split" }>
190 for _, h := range f.Hunks {
191 <tbody>
192 <tr class="hunk"><td colspan="6">{ h.Header }</td></tr>
193 for _, row := range h.Split {
194 <tr>
195 @splitSide(row.L, true)
196 @splitSide(row.R, false)
197 </tr>
198 }
199 </tbody>
200 }
201 </table>
202 }
203 </div>
204 } else {
205 <p class="note pad">
206 if f.Note != "" {
207 { f.Note }
208 } else {
209 no textual change.
210 }
211 </p>
212 }
213 </details>
214}
215
216// splitSide is one half of a row of the split view; a side without a line is
217// an empty cell.
218templ splitSide(ln *DiffLine, old bool) {
219 if ln == nil {
220 <td class="ln none"></td>
221 <td class="m none"></td>
222 <td class="tx none"></td>
223 } else {
224 <td class={ "ln", ln.Kind }>
225 if old {
226 { ln.Old }
227 } else {
228 { ln.New }
229 }
230 </td>
231 <td class={ "m", ln.Kind }>{ ln.Mark }</td>
232 <td class={ "tx", ln.Kind }>@codeLine(*ln)</td>
233 }
234}