detached 951a93a

refactor(web): share the diff layout choice between pages

commit
951a93a6961fc698ae752408892eaf5bdbc32517
author
brice · · 2026-10-10 18:06 UTC
parent
509893d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +50 -30
M internal/adapters/web/commit.go +40 -20
@@ -41,6 +41,16 @@ type CommitPage struct {
4141 Files []DiffFile
4242 // Note says the diff is incomplete.
4343 Note string
44+ DiffView
45+ // Invisible is true when some line holds an invisible character, shown as a
46+ // marker.
47+ Invisible bool
48+}
49+
50+// DiffView is how a page lays its diff out: the layouts in the page and the
51+// way to switch between them. The commit page and the merge request pages
52+// share it.
53+type DiffView struct {
4454 // View is the layout shown: "unified" or "split". Pinned is true when the
4555 // URL chose it, which then wins over the choice remembered by the browser.
4656 View string
@@ -50,9 +60,33 @@ type CommitPage struct {
5060 Both bool
5161 ShowUnified, ShowSplit bool
5262 UnifiedHref, SplitHref string
53- // Invisible is true when some line holds an invisible character, shown as a
54- // marker.
55- Invisible bool
63+}
64+
65+// newDiffView reads ?view= and decides which layouts a diff of that many
66+// lines sends; the links switch to the other layout.
67+func newDiffView(r *http.Request, lines int, unifiedHref, splitHref string) DiffView {
68+ v := DiffView{View: "unified", UnifiedHref: unifiedHref, SplitHref: splitHref}
69+ switch r.URL.Query().Get("view") {
70+ case "split":
71+ v.View, v.Pinned = "split", true
72+ case "unified":
73+ v.Pinned = true
74+ }
75+ v.Both = lines <= bothViewsLines
76+ v.ShowUnified, v.ShowSplit = v.Both || v.View == "unified", v.Both || v.View == "split"
77+ return v
78+}
79+
80+// diffLines counts the lines of the hunks of files, and tells whether some
81+// line holds a marker.
82+func diffLines(files []DiffFile) (lines int, invisible bool) {
83+ for _, f := range files {
84+ for _, h := range f.Hunks {
85+ lines += len(h.Lines)
86+ invisible = invisible || slices.ContainsFunc(h.Lines, hasMarker)
87+ }
88+ }
89+ return lines, invisible
5690 }
5791
5892 // commit shows a commit: its message, its totals, the changed files and the
@@ -87,7 +121,6 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component,
87121 Adds: int(stats.GetAdditions()),
88122 Dels: int(stats.GetDeletions()),
89123 Files: diffFiles(res.Msg.GetDiff().GetFiles()),
90- View: "unified",
91124 }
92125 if cm := person(c.GetCommitter(), now); cm != p.Author || c.GetCommitter().GetEmail() != c.GetAuthor().GetEmail() {
93126 p.Committer = &cm
@@ -107,23 +140,10 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component,
107140 }
108141 p.Note = diffNote(res.Msg.GetDiff(), len(p.Files), p.NFiles)
109142
110- lines := 0
111- for _, f := range p.Files {
112- for _, h := range f.Hunks {
113- lines += len(h.Lines)
114- p.Invisible = p.Invisible || slices.ContainsFunc(h.Lines, hasMarker)
115- }
116- }
117- switch r.URL.Query().Get("view") {
118- case "split":
119- p.View, p.Pinned = "split", true
120- case "unified":
121- p.Pinned = true
122- }
123- p.Both = lines <= bothViewsLines
124- p.ShowUnified, p.ShowSplit = p.Both || p.View == "unified", p.Both || p.View == "split"
143+ lines, invisible := diffLines(p.Files)
144+ p.Invisible = invisible
125145 base := repoHref(d.name(), "commit", c.GetId())
126- p.UnifiedHref, p.SplitHref = base+"?view=unified", base+"?view=split"
146+ p.DiffView = newDiffView(r, lines, base+"?view=unified", base+"?view=split")
127147
128148 l := s.repoLayout(ctx, d, "commits")
129149 l.Title = p.Short + " " + p.Subject + " · " + d.name() + " · " + s.instance
@@ -41,6 +41,16 @@ type CommitPage struct {
41 Files []DiffFile41 Files []DiffFile
42 // Note says the diff is incomplete.42 // Note says the diff is incomplete.
43 Note string43 Note string
44+ DiffView
45+ // Invisible is true when some line holds an invisible character, shown as a
46+ // marker.
47+ Invisible bool
48+}
49+
50+// DiffView is how a page lays its diff out: the layouts in the page and the
51+// way to switch between them. The commit page and the merge request pages
52+// share it.
53+type DiffView struct {
44 // View is the layout shown: "unified" or "split". Pinned is true when the54 // View is the layout shown: "unified" or "split". Pinned is true when the
45 // URL chose it, which then wins over the choice remembered by the browser.55 // URL chose it, which then wins over the choice remembered by the browser.
46 View string56 View string
@@ -50,9 +60,33 @@ type CommitPage struct {
50 Both bool60 Both bool
51 ShowUnified, ShowSplit bool61 ShowUnified, ShowSplit bool
52 UnifiedHref, SplitHref string62 UnifiedHref, SplitHref string
53- // Invisible is true when some line holds an invisible character, shown as a63+}
54- // marker.64+
55- Invisible bool65+// newDiffView reads ?view= and decides which layouts a diff of that many
66+// lines sends; the links switch to the other layout.
67+func newDiffView(r *http.Request, lines int, unifiedHref, splitHref string) DiffView {
68+ v := DiffView{View: "unified", UnifiedHref: unifiedHref, SplitHref: splitHref}
69+ switch r.URL.Query().Get("view") {
70+ case "split":
71+ v.View, v.Pinned = "split", true
72+ case "unified":
73+ v.Pinned = true
74+ }
75+ v.Both = lines <= bothViewsLines
76+ v.ShowUnified, v.ShowSplit = v.Both || v.View == "unified", v.Both || v.View == "split"
77+ return v
78+}
79+
80+// diffLines counts the lines of the hunks of files, and tells whether some
81+// line holds a marker.
82+func diffLines(files []DiffFile) (lines int, invisible bool) {
83+ for _, f := range files {
84+ for _, h := range f.Hunks {
85+ lines += len(h.Lines)
86+ invisible = invisible || slices.ContainsFunc(h.Lines, hasMarker)
87+ }
88+ }
89+ return lines, invisible
56 }90 }
57 91
58 // commit shows a commit: its message, its totals, the changed files and the92 // commit shows a commit: its message, its totals, the changed files and the
@@ -87,7 +121,6 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component,
87 Adds: int(stats.GetAdditions()),121 Adds: int(stats.GetAdditions()),
88 Dels: int(stats.GetDeletions()),122 Dels: int(stats.GetDeletions()),
89 Files: diffFiles(res.Msg.GetDiff().GetFiles()),123 Files: diffFiles(res.Msg.GetDiff().GetFiles()),
90- View: "unified",
91 }124 }
92 if cm := person(c.GetCommitter(), now); cm != p.Author || c.GetCommitter().GetEmail() != c.GetAuthor().GetEmail() {125 if cm := person(c.GetCommitter(), now); cm != p.Author || c.GetCommitter().GetEmail() != c.GetAuthor().GetEmail() {
93 p.Committer = &cm126 p.Committer = &cm
@@ -107,23 +140,10 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component,
107 }140 }
108 p.Note = diffNote(res.Msg.GetDiff(), len(p.Files), p.NFiles)141 p.Note = diffNote(res.Msg.GetDiff(), len(p.Files), p.NFiles)
109 142
110- lines := 0143+ lines, invisible := diffLines(p.Files)
111- for _, f := range p.Files {144+ p.Invisible = invisible
112- for _, h := range f.Hunks {
113- lines += len(h.Lines)
114- p.Invisible = p.Invisible || slices.ContainsFunc(h.Lines, hasMarker)
115- }
116- }
117- switch r.URL.Query().Get("view") {
118- case "split":
119- p.View, p.Pinned = "split", true
120- case "unified":
121- p.Pinned = true
122- }
123- p.Both = lines <= bothViewsLines
124- p.ShowUnified, p.ShowSplit = p.Both || p.View == "unified", p.Both || p.View == "split"
125 base := repoHref(d.name(), "commit", c.GetId())145 base := repoHref(d.name(), "commit", c.GetId())
126- p.UnifiedHref, p.SplitHref = base+"?view=unified", base+"?view=split"146+ p.DiffView = newDiffView(r, lines, base+"?view=unified", base+"?view=split")
127 147
128 l := s.repoLayout(ctx, d, "commits")148 l := s.repoLayout(ctx, d, "commits")
129 l.Title = p.Short + " " + p.Subject + " · " + d.name() + " · " + s.instance149 l.Title = p.Short + " " + p.Subject + " · " + d.name() + " · " + s.instance
M internal/adapters/web/diff.templ +5 -5
@@ -53,8 +53,8 @@ templ Commit(l Layout, p CommitPage) {
5353 <div class="diff-tools" role="toolbar" aria-label="Diff view">
5454 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
5555 <span class="grow"></span>
56- @viewLink(p, "unified", "[u]nified", "u", p.UnifiedHref)
57- @viewLink(p, "split", "[d] split", "d", p.SplitHref)
56+ @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref)
57+ @viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref)
5858 <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button>
5959 </div>
6060 if p.Invisible {
@@ -78,7 +78,7 @@ templ Commit(l Layout, p CommitPage) {
7878 }
7979 >
8080 for _, f := range p.Files {
81- @DiffFileView(f, p)
81+ @DiffFileView(f, p.DiffView)
8282 }
8383 if len(p.Files) == 0 {
8484 <p class="note">no changed files{ " " }
@@ -96,7 +96,7 @@ templ Commit(l Layout, p CommitPage) {
9696 // viewLink is a button of the diff tools. It is a link, so that the split
9797 // layout works without JavaScript; with it, and when both layouts are in the
9898 // page, a click only flips the layout.
99-templ viewLink(p CommitPage, view, label, key, href string) {
99+templ viewLink(p DiffView, view, label, key, href string) {
100100 <a
101101 href={ templ.SafeURL(href) }
102102 class="vbtn"
@@ -151,7 +151,7 @@ templ FileList(files []DiffFile) {
151151 // unified and the split tables are both in it; #diffs[data-diff] shows one.
152152 // Every changed line keeps its +/- marker, so a change is never conveyed by
153153 // colour alone.
154-templ DiffFileView(f DiffFile, p CommitPage) {
154+templ DiffFileView(f DiffFile, p DiffView) {
155155 <details
156156 class="dfile"
157157 id={ f.ID }
@@ -53,8 +53,8 @@ templ Commit(l Layout, p CommitPage) {
53 <div class="diff-tools" role="toolbar" aria-label="Diff view">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>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>55 <span class="grow"></span>
56- @viewLink(p, "unified", "[u]nified", "u", p.UnifiedHref)56+ @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref)
57- @viewLink(p, "split", "[d] split", "d", p.SplitHref)57+ @viewLink(p.DiffView, "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>58 <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button>
59 </div>59 </div>
60 if p.Invisible {60 if p.Invisible {
@@ -78,7 +78,7 @@ templ Commit(l Layout, p CommitPage) {
78 }78 }
79 >79 >
80 for _, f := range p.Files {80 for _, f := range p.Files {
81- @DiffFileView(f, p)81+ @DiffFileView(f, p.DiffView)
82 }82 }
83 if len(p.Files) == 0 {83 if len(p.Files) == 0 {
84 <p class="note">no changed files{ " " }84 <p class="note">no changed files{ " " }
@@ -96,7 +96,7 @@ templ Commit(l Layout, p CommitPage) {
96 // viewLink is a button of the diff tools. It is a link, so that the split96 // 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 the97 // layout works without JavaScript; with it, and when both layouts are in the
98 // page, a click only flips the layout.98 // page, a click only flips the layout.
99-templ viewLink(p CommitPage, view, label, key, href string) {99+templ viewLink(p DiffView, view, label, key, href string) {
100 <a100 <a
101 href={ templ.SafeURL(href) }101 href={ templ.SafeURL(href) }
102 class="vbtn"102 class="vbtn"
@@ -151,7 +151,7 @@ templ FileList(files []DiffFile) {
151 // unified and the split tables are both in it; #diffs[data-diff] shows one.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 by152 // Every changed line keeps its +/- marker, so a change is never conveyed by
153 // colour alone.153 // colour alone.
154-templ DiffFileView(f DiffFile, p CommitPage) {154+templ DiffFileView(f DiffFile, p DiffView) {
155 <details155 <details
156 class="dfile"156 class="dfile"
157 id={ f.ID }157 id={ f.ID }
M internal/adapters/web/diff_templ.go +5 -5 generated file: collapsed

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