v0.8.0 eaf4295

diff.templ RawHistoryFolder

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 p.Checks.Total > 0 {
44					<dt>checks</dt>
45					<dd>@CommitChecks(p.Checks)</dd>
46				}
47				if len(p.Refs) > 0 {
48					<dt>refs</dt>
49					<dd>
50						for _, r := range p.Refs {
51							<span class="pill">{ r }</span>
52						}
53					</dd>
54				}
55			</dl>
56		</header>
57		<div class="diff-tools" role="toolbar" aria-label="Diff view">
58			<span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
59			<span class="grow"></span>
60			@viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
61			@viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
62			@collapseButton()
63		</div>
64		if p.Invisible {
65			<p class="notice">
66				this diff holds invisible characters (direction controls, zero-width characters). They are shown as
67				<span class="inv">&lt;U+XXXX&gt;</span> markers, never applied.
68			</p>
69		}
70		if p.Note != "" {
71			<p class="notice">{ p.Note }</p>
72		}
73		<div
74			id="diffs"
75			if p.Both {
76				data-diff={ p.View }
77			} else {
78				data-view={ p.View }
79			}
80			if p.Both && !p.Pinned {
81				hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"
82			}
83		>
84			for _, f := range p.Files {
85				@DiffFileView(f, p.DiffView)
86			}
87			if len(p.Files) == 0 {
88				<p class="note">no changed files{ " " }
89					if p.Merge {
90						against the first parent.
91					} else {
92						in this commit.
93					}
94				</p>
95			}
96		</div>
97	}
98}
99
100// viewLink is a button of the diff tools. It is a link, so that the split
101// layout works without JavaScript; with it, and when both layouts are in the
102// page, a click only flips the layout.
103templ viewLink(p DiffView, view, label, key, href string) {
104	<a
105		href={ templ.SafeURL(href) }
106		class="btn vbtn"
107		data-view={ view }
108		aria-keyshortcuts={ key }
109		if p.View == view {
110			aria-current="true"
111		}
112		if p.Both {
113			data-fire={ view }
114			:aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false"
115		}
116	>{ label }<kbd aria-hidden="true">{ key }</kbd></a>
117}
118
119// collapseButton folds every file of the diff, or opens them all again when
120// none is open; its label follows the state.
121templ collapseButton() {
122	<button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
123		<span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span><kbd aria-hidden="true">x</kbd>
124	</button>
125}
126
127templ personView(pr Person) {
128	{ pr.Name }
129	if pr.WhenAt != "" {
130		{ " · " }<time datetime={ pr.WhenAt } title={ pr.Abs }>{ pr.When }</time>{ " · " + pr.Abs }
131	}
132}
133
134templ commitRail(p CommitPage) {
135	<section aria-labelledby="r-files">
136		<h2 id="r-files">Files <span class="count">{ itoa(len(p.Files)) }</span></h2>
137		@FileList(p.Files)
138	</section>
139}
140
141// FileList is the table of contents of a diff: status letter, path, +n -m
142// and an ASCII bar. The row of the file in view is marked.
143templ FileList(files []DiffFile) {
144	<ol class="files" aria-label="Changed files">
145		for _, f := range files {
146			<li>
147				<a
148					href={ templ.SafeURL("#" + f.ID) }
149					hx-on:click="document.getElementById(this.hash.slice(1)).open = true"
150				>
151					<span class="st" title={ f.Status }>{ f.Status }</span>
152					<span class="p" title={ f.Path }>{ f.Path }</span>
153					<span class="n"><span class="plus">+{ itoa(f.Add) }</span> <span class="minus">-{ itoa(f.Del) }</span></span>
154					<span class="b" aria-hidden="true"><span class="plus">{ f.Bar.Add }</span><span class="minus">{ f.Bar.Del }</span>{ f.Bar.Rest }</span>
155				</a>
156			</li>
157		}
158	</ol>
159}
160
161// DiffFileView is one file of the diff: a <details> (per-file collapse). The
162// unified and the split tables are both in it; #diffs[data-diff] shows one.
163// Every changed line keeps its +/- marker, so a change is never conveyed by
164// colour alone.
165templ DiffFileView(f DiffFile, p DiffView) {
166	<details
167		class="dfile"
168		id={ f.ID }
169		data-file
170		if f.Open {
171			open
172		}
173	>
174		<summary>
175			<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>
176			if f.Note != "" {
177				{ " " }<span class="note">{ f.Note }</span>
178			}
179		</summary>
180		if len(f.Hunks) > 0 {
181			<div class="dscroll">
182				if p.ShowUnified {
183					<table class="dt uni" aria-label={ f.Path + ", unified" }>
184						for _, h := range f.Hunks {
185							<tbody>
186								<tr class="hunk"><td colspan="4">{ h.Header }</td></tr>
187								for _, ln := range h.Lines {
188									<tr class={ ln.Kind }>
189										<td class="ln">@gutter(ln.Old, ln.OldHref)</td>
190										<td class="ln">@gutter(ln.New, ln.NewHref)</td>
191										<td class="m">{ ln.Mark }</td>
192										<td class="tx">@codeLine(ln)</td>
193									</tr>
194									if len(ln.Notes) > 0 || ln.Form != nil {
195										<tr class="fnote">
196											<td colspan="3"></td>
197											<td>@lineNotes(ln.Notes, ln.Form, "")</td>
198										</tr>
199									}
200								}
201							</tbody>
202						}
203					</table>
204				}
205				if p.ShowSplit {
206					<table class="dt spl" aria-label={ f.Path + ", split" }>
207						<colgroup><col class="c-ln"/><col class="c-m"/><col/><col class="c-ln"/><col class="c-m"/><col/></colgroup>
208						for _, h := range f.Hunks {
209							<tbody>
210								<tr class="hunk"><td colspan="6">{ h.Header }</td></tr>
211								for _, row := range h.Split {
212									<tr>
213										@splitSide(row.L, true)
214										@splitSide(row.R, false)
215									</tr>
216									if notes, form := row.Notes(), row.Form(); len(notes) > 0 || form != nil {
217										<tr class="fnote">
218											<td colspan="6">@lineNotes(notes, form, "-s")</td>
219										</tr>
220									}
221								}
222							</tbody>
223						}
224					</table>
225				}
226			</div>
227		} else {
228			<p class="note pad">
229				if f.Note != "" {
230					{ f.Note }
231				} else {
232					no textual change.
233				}
234			</p>
235		}
236	</details>
237}
238
239// splitSide is one half of a row of the split view; a side without a line is
240// an empty cell.
241templ splitSide(ln *DiffLine, old bool) {
242	if ln == nil {
243		<td class="ln none"></td>
244		<td class="m none"></td>
245		<td class="tx none"></td>
246	} else {
247		<td class={ "ln", ln.Kind }>
248			if old {
249				@gutter(ln.Old, ln.OldHref)
250			} else {
251				@gutter(ln.New, ln.NewHref)
252			}
253		</td>
254		<td class={ "m", ln.Kind }>{ ln.Mark }</td>
255		<td class={ "tx", ln.Kind }>@codeLine(*ln)</td>
256	}
257}
258
259// gutter is a line number of the gutter. On a merge request that can be
260// commented on, it links to the form that comments on that line.
261templ gutter(n, href string) {
262	if href != "" {
263		<a href={ templ.SafeURL(href) } title="comment on this line">{ n }</a>
264	} else {
265		{ n }
266	}
267}

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 45

↑↓ moveenter openesc close