detached e55e679

Merge 5e16168889b2df04cfacb1c1d7482d493a06601d into 5d69f2b26c46390bac7207c48c805519b99b6653

commit
e55e67925b6f96b6ad0c6f0e59ab17ec012ea98c
author
ermite · · 2026-10-22 08:51 UTC
merge of
5d69f2b + 5e16168 · diff against the first parent
signature
unsigned
73 files · +2058 -1284
M README.md +1 -0
@@ -139,6 +139,7 @@ Give the agent the [`skills/ermite`](skills/ermite/SKILL.md) skill so it knows t
139139 - [Issues](docs/issues.md)
140140 - [Commit signatures](docs/commit-signatures.md): SSH and OpenPGP badges, trusted keys
141141 - [Web sign-in](docs/web-sign-in.md): passkeys and one-time links
142+- [Keyboard shortcuts](docs/keyboard-shortcuts.md): web section navigation and page actions
142143 - [CI](docs/ci.md): runners, trust, concurrency, reusable workflows
143144 - [CI events](docs/ci-events.md)
144145 - [OIDC](docs/ci-oidc.md)
M docs/issues.md +1 -1
@@ -50,7 +50,7 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
5050
5151 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
5252
53-The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.
53+The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; `g i` opens the issues tab.
5454
5555 The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
5656
A docs/keyboard-shortcuts.md +30 -0
@@ -0,0 +1,30 @@
1+# Web keyboard shortcuts
2+
3+Press `?` for the complete, page-specific list. Shortcuts do not run while typing
4+in a field. Section navigation uses two successive keys, not a chord:
5+
6+| Keys | Destination |
7+| --- | --- |
8+| `g s` | Summary |
9+| `g f` | Files |
10+| `g c` | Commits |
11+| `g b` | Branches & tags |
12+| `g m` | Merge requests |
13+| `g i` | Issues |
14+| `g r` | Releases |
15+| `g a` | Actions (when CI is enabled) |
16+| `g h` | Home |
17+| `g g` | Top / first row |
18+| `G` (Shift+g) | Bottom / last row |
19+
20+There is no `g t` alias, and `s`, `t`, `c`, `r`, `m`, and `i` alone do
21+not navigate sections. `b` still opens the branch/tag switcher; `f`, `:`,
22+and Ctrl+K (Cmd+K on macOS) open the command palette.
23+
24+On merge requests, `l` focuses the files side panel; `g f` always navigates
25+to the repository Files section. `h` returns to the content. Uppercase `M`
26+opens the merge menu, `R` marks the head read, `E` expands every diff file,
27+and `I` opens the signed-in owner's inbox.
28+
29+The help dialog can turn off character shortcuts, including `g` sequences
30+and shifted letters. Ctrl+K / Cmd+K and Escape remain available.
M docs/merge-requests.md +1 -1
@@ -142,7 +142,7 @@ the diff opens the Checks panel and continues to update live. Commit totals use
142142 `N+` when the branch list is capped: the reader's existing count operation counts all reachable
143143 ancestors, not the source-minus-target range.
144144
145-The rail keeps Files (including its `g f` shortcut) and Verdicts. Merge settings
145+The rail keeps Files (including its `l` shortcut) and Verdicts. Merge settings
146146 are read-only information in the More (⋯) menu, available to every viewer.
147147 Signing-key information is also there when a merge box offers signed modes;
148148 merge actions remain restricted to their existing permissions.
M internal/adapters/kvstore/issues_test.go +1 -1
@@ -364,7 +364,7 @@ func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
364364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
365365 _, err := kvstore.Open(ctx, dir)
366366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
367- assert.Equal(t, uint32(6), uint32(kvstore.SchemaVersion))
367+ assert.Equal(t, uint32(7), uint32(kvstore.SchemaVersion))
368368 }
369369
370370 // A reopen racing the delete of the repository either lands before the delete
M internal/adapters/kvstore/releases.go +28 -2
@@ -36,7 +36,26 @@ func relNamePrefix(repo domain.RepoName, id int64) string {
3636 func relNameKey(a domain.ReleaseAsset) []byte {
3737 return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)
3838 }
39-func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
39+func relLatestKey(repo domain.RepoName) []byte { return []byte("rellatest/" + repo.String()) }
40+func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
41+
42+// GetLatestRelease resolves the index and release from the same snapshot.
43+func (c *Releases) GetLatestRelease(ctx context.Context, repo domain.RepoName) (domain.Release, error) {
44+ snap := c.db.Snapshot(ctx)
45+ item, ok := snap.Get(ctx, relLatestKey(repo))
46+ if !ok {
47+ return domain.Release{}, domain.ErrNotFound
48+ }
49+ id, err := strconv.ParseInt(string(item.Value), 10, 64)
50+ if err != nil {
51+ return domain.Release{}, err
52+ }
53+ item, ok = snap.Get(ctx, relKey(repo, id))
54+ if !ok {
55+ return domain.Release{}, domain.ErrNotFound
56+ }
57+ return decodeRelease(item)
58+}
4059 func encodeRelease(r domain.Release) []byte {
4160 return mustMarshal(&storagepb.Release{
4261 Id: r.ID,
@@ -285,6 +304,13 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
285304 b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)
286305 }
287306 b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))
307+ if w.SetLatest {
308+ if w.LatestID > 0 {
309+ b.Put(relLatestKey(w.Repo), []byte(strconv.FormatInt(w.LatestID, 10)))
310+ } else {
311+ b.Delete(relLatestKey(w.Repo))
312+ }
313+ }
288314 snap := c.db.Snapshot(ctx)
289315 for _, r := range w.DeleteReleases {
290316 if r.Repo != w.Repo || r.Version == 0 {
@@ -388,7 +414,7 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
388414
389415 // releaseKeys collects all repository-owned release keys, including global indexes.
390416 func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
391- keys := [][]byte{relUseKey(repo)}
417+ keys := [][]byte{relUseKey(repo), relLatestKey(repo)}
392418 for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {
393419 for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {
394420 keys = append(keys, slices.Clone(key))
A internal/adapters/kvstore/releases_index.go +67 -0
@@ -0,0 +1,67 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ "context"
6+ "fmt"
7+ "strconv"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "bnema.dev/ermite/internal/domain"
11+)
12+
13+const releaseLatestIndexMarker = "meta/release-latest-index"
14+
15+// buildReleaseLatestIndex runs before the store is exposed to writers. A crash
16+// before the marker is written safely repeats the pass on the next open.
17+// Selection remains owned by domain.LatestRelease.
18+func (s *Store) buildReleaseLatestIndex(ctx context.Context) error {
19+ if _, ok := s.db.Get(ctx, []byte(releaseLatestIndexMarker)); ok {
20+ return nil
21+ }
22+ var repo domain.RepoName
23+ var best domain.Release
24+ var eligible bool
25+ flush := func() error {
26+ var b kv.Batch
27+ if eligible {
28+ b.Put(relLatestKey(repo), []byte(strconv.FormatInt(best.ID, 10)))
29+ } else {
30+ b.Delete(relLatestKey(repo))
31+ }
32+ if _, err := s.db.Apply(ctx, &b); err != nil {
33+ return fmt.Errorf("build latest release index: %w", err)
34+ }
35+ return nil
36+ }
37+ haveRepo := false
38+ for _, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte("rel/")) {
39+ rel, err := decodeRelease(item)
40+ if err != nil {
41+ return fmt.Errorf("build latest release index: %w", err)
42+ }
43+ if haveRepo && rel.Repo != repo {
44+ if err := flush(); err != nil {
45+ return err
46+ }
47+ best, eligible = domain.Release{}, false
48+ }
49+ repo, haveRepo = rel.Repo, true
50+ if eligible {
51+ best, eligible = domain.LatestRelease([]domain.Release{best, rel})
52+ } else {
53+ best, eligible = domain.LatestRelease([]domain.Release{rel})
54+ }
55+ }
56+ if haveRepo {
57+ if err := flush(); err != nil {
58+ return err
59+ }
60+ }
61+ var b kv.Batch
62+ b.Put([]byte(releaseLatestIndexMarker), []byte{1})
63+ if _, err := s.db.Apply(ctx, &b); err != nil {
64+ return fmt.Errorf("mark latest release index: %w", err)
65+ }
66+ return nil
67+}
A internal/adapters/kvstore/releases_index_test.go +97 -0
@@ -0,0 +1,97 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ "context"
6+ "testing"
7+ "time"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestReleaseLatestIndexBackfill(t *testing.T) {
16+ ctx := t.Context()
17+ dir := t.TempDir()
18+ s, err := Open(ctx, dir)
19+ require.NoError(t, err)
20+ var repos []domain.RepoName
21+ for _, raw := range []string{"backfill", "empty", "newest"} {
22+ repo, err := domain.ParseRepoName(raw)
23+ require.NoError(t, err)
24+ repos = append(repos, repo)
25+ }
26+ for _, repo := range repos {
27+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
28+ }
29+ var b kv.Batch
30+ for i, repo := range repos {
31+ for j := 1; j <= 5; j++ {
32+ rel := domain.Release{ID: int64(i*10 + j), Repo: repo, Tag: string(rune('a' + j)), CreatedAt: time.Unix(int64(j), 0)}
33+ rel.Draft = j == 5 || i == 1
34+ rel.Prerelease = j == 4
35+ rel.MakeLatest = j == 1 && i == 0
36+ b.Put(relKey(repo, rel.ID), encodeRelease(rel))
37+ }
38+ // A stale key must also be removed for a repo with no eligible release.
39+ b.Put(relLatestKey(repo), []byte("999"))
40+ }
41+ b.Delete([]byte(releaseLatestIndexMarker))
42+ _, err = s.db.Apply(ctx, &b)
43+ require.NoError(t, err)
44+ require.NoError(t, s.Close(context.Background()))
45+ s, err = Open(ctx, dir)
46+ require.NoError(t, err)
47+ defer func() { require.NoError(t, s.Close(context.Background())) }()
48+ for i, want := range []int64{1, 0, 23} {
49+ got, err := s.Releases().GetLatestRelease(ctx, repos[i])
50+ if want == 0 {
51+ require.ErrorIs(t, err, domain.ErrNotFound)
52+ } else {
53+ require.NoError(t, err)
54+ require.Equal(t, want, got.ID)
55+ }
56+ }
57+ require.NoError(t, s.buildReleaseLatestIndex(ctx))
58+ got, err := s.Releases().GetLatestRelease(ctx, repos[0])
59+ require.NoError(t, err)
60+ require.EqualValues(t, 1, got.ID)
61+ require.NoError(t, s.Delete(ctx, repos[0]))
62+ _, err = s.Releases().GetLatestRelease(ctx, repos[0])
63+ require.ErrorIs(t, err, domain.ErrNotFound)
64+}
65+
66+func TestGetLatestReleaseAtomicUpdate(t *testing.T) {
67+ ctx := t.Context()
68+ s, err := Open(ctx, t.TempDir())
69+ require.NoError(t, err)
70+ defer func() { require.NoError(t, s.Close(context.Background())) }()
71+ repo, err := domain.ParseRepoName("atomic")
72+ require.NoError(t, err)
73+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}))
74+ a := domain.Release{ID: 1, Repo: repo, Tag: "a"}
75+ b := domain.Release{ID: 2, Repo: repo, Tag: "b"}
76+ store := s.Releases()
77+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, PutReleases: []domain.Release{a, b}, SetLatest: true, LatestID: a.ID}))
78+ a, err = store.GetLatestRelease(ctx, repo)
79+ require.NoError(t, err)
80+ require.EqualValues(t, 1, a.ID)
81+ _, version, err := store.ReleaseUsage(ctx, repo)
82+ require.NoError(t, err)
83+ a.Prerelease = true
84+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{a}, SetLatest: true, LatestID: b.ID}))
85+ got, err := store.GetLatestRelease(ctx, repo)
86+ require.NoError(t, err)
87+ require.EqualValues(t, 2, got.ID)
88+ // An asset-only/usage write leaves the index untouched, including its version.
89+ before, ok := s.db.Get(ctx, relLatestKey(repo))
90+ require.True(t, ok)
91+ _, version, err = store.ReleaseUsage(ctx, repo)
92+ require.NoError(t, err)
93+ require.NoError(t, store.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 10}))
94+ after, ok := s.db.Get(ctx, relLatestKey(repo))
95+ require.True(t, ok)
96+ require.Equal(t, before, after)
97+}
M internal/adapters/kvstore/store.go +6 -1
@@ -43,7 +43,8 @@ import (
4343 // holding all eleven previous scopes, preserving their owner rights. Version 6
4444 // adds releases and release:write; older builds cannot clean up their metadata
4545 // or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).
46-const SchemaVersion = 6
46+// Version 7 adds the latest-release index; older writers would leave it stale.
47+const SchemaVersion = 7
4748
4849 const schemaKey = "meta/schema"
4950
@@ -76,6 +77,10 @@ func Open(ctx context.Context, dir string) (*Store, error) {
7677 _ = db.Close(context.WithoutCancel(ctx))
7778 return nil, err
7879 }
80+ if err := s.buildReleaseLatestIndex(ctx); err != nil {
81+ _ = db.Close(context.WithoutCancel(ctx))
82+ return nil, err
83+ }
7984 return s, nil
8085 }
8186
M internal/adapters/web/fragments.go +1 -1
@@ -167,7 +167,7 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error)
167167 func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
168168 switch ev := we.Event.(type) {
169169 case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished:
170- if l.view == "releases" || l.view == "release" {
170+ if l.view == "releases" || l.view == "release" || l.view == "summary" {
171171 return gap, nil // notes, assets, latest or visibility changed: reload
172172 }
173173 return nil, nil
M internal/adapters/web/fragments_test.go +3 -3
@@ -101,8 +101,8 @@ func TestMRPushedHeadSignal(t *testing.T) {
101101 }
102102 }
103103
104-// Release lifecycle events invalidate the release pages without putting notes
105-// or asset metadata in SSE payloads. Other views need no release fragments.
104+// Release lifecycle events invalidate release pages and the summary's release
105+// fact without putting notes or asset metadata in SSE payloads.
106106 func TestReleaseLiveUpdates(t *testing.T) {
107107 for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} {
108108 we := ports.WatchEvent{Event: ev}
@@ -111,7 +111,7 @@ func TestReleaseLiveUpdates(t *testing.T) {
111111 st := &liveStream{repo: "proj", view: view}
112112 got, err := st.render(t.Context(), we)
113113 require.NoError(t, err)
114- if view == "release" || view == "releases" {
114+ if view == "release" || view == "releases" || view == "summary" {
115115 assert.Equal(t, gap, got)
116116 } else {
117117 assert.Empty(t, got)
M internal/adapters/web/issues_test.go +3 -3
@@ -100,7 +100,7 @@ func TestIssueList(t *testing.T) {
100100 assert.Contains(t, body, "crash &lt;b&gt;on&lt;/b&gt; start")
101101 assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page")
102102 assert.Contains(t, body, `href="/proj/issues?state=closed"`, "closed tab")
103- assert.Contains(t, body, `href="/proj/issues" data-key="i"`, "the Issues tab")
103+ assert.Contains(t, body, `href="/proj/issues" data-key="g i"`, "the Issues tab")
104104 assert.NotContains(t, body, "New issue", "no form for an anonymous visitor")
105105 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
106106
@@ -169,7 +169,7 @@ func TestIssueNotFound(t *testing.T) {
169169 }
170170 summary := off.do("GET", "/proj", false, nil)
171171 require.Equal(t, 200, summary.Code)
172- assert.NotContains(t, summary.Body.String(), `data-key="i"`)
172+ assert.NotContains(t, summary.Body.String(), `data-key="g i"`)
173173
174174 // The mode is unset in the settings: off as well.
175175 unset := newIssueRig(t, "", false)
@@ -257,7 +257,7 @@ func TestIssueRoutesAbsentWithoutService(t *testing.T) {
257257 }
258258 rec := httptest.NewRecorder()
259259 srv.ServeHTTP(rec, httptest.NewRequest("GET", "/proj", nil))
260- assert.NotContains(t, rec.Body.String(), `data-key="i"`)
260+ assert.NotContains(t, rec.Body.String(), `data-key="g i"`)
261261 }
262262
263263 func TestIssueNumberRedirects(t *testing.T) {
A internal/adapters/web/jstest/keyboard.test.mjs +76 -0
@@ -0,0 +1,76 @@
1+// Exercise the production key handler without a browser or dependencies.
2+import assert from 'node:assert/strict';
3+import { readFileSync } from 'node:fs';
4+import { test } from 'node:test';
5+import { runInNewContext } from 'node:vm';
6+const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
7+function keyboard({ owner = true, off = false, typing = false, dialog = false } = {}) {
8+ let handler;
9+ const fired = [];
10+ const timers = new Map();
11+ let timerID = 0;
12+ runInNewContext(source.slice(source.indexOf(' const single ='), source.indexOf(' // 2. A [data-fire]')), {
13+ addEventListener: (_, fn) => { handler = fn; },
14+ window: { ermiteKeys: { blocks: () => off } },
15+ ermite: { fire: name => fired.push(name) },
16+ $: selector => (owner && selector === '[data-inbox]') || (dialog && selector === 'dialog[open]'),
17+ closeMenus: () => false,
18+ clearTimeout: id => timers.delete(id),
19+ setTimeout: fn => { timers.set(++timerID, fn); return timerID; },
20+ });
21+ return { fired, expireTimers() {
22+ const callbacks = [...timers.values()];
23+ timers.clear();
24+ for (const callback of callbacks) callback();
25+ }, press(key, shiftKey = false) {
26+ handler({ key, shiftKey, target: { closest: selector => typing && selector.startsWith('input'), blur() {} }, preventDefault() {} });
27+ } };
28+}
29+test('sections require g and have no old t alias', () => {
30+ const k = keyboard();
31+ for (const key of ['s', 't', 'c', 'r', 'm', 'i']) k.press(key);
32+ assert.deepEqual(k.fired, []);
33+ for (const key of ['s', 'f', 'c', 'b', 'm', 'i', 'r', 'a', 'h', 'g', 't']) { k.press('g'); k.press(key); }
34+ assert.deepEqual(k.fired, ['s', 'f', 'c', 'b', 'm', 'i', 'r', 'a', 'gh', 'top']);
35+});
36+test('uppercase actions work with either reported casing and interrupt prefixes', () => {
37+ for (const [key, action] of Object.entries({ G: 'bottom', M: 'merge', R: 'read', E: 'expandAll', I: 'inbox' })) {
38+ for (const reported of [key, key.toLowerCase()]) {
39+ const k = keyboard();
40+ k.press('g'); k.press(reported, true); k.press(reported, true);
41+ assert.deepEqual(k.fired, [action, action]);
42+ }
43+ }
44+});
45+test('uppercase actions respect preference, fields and dialogs', () => {
46+ for (const options of [{ off: true }, { typing: true }, { dialog: true }]) {
47+ const k = keyboard(options); k.press('g', true);
48+ assert.deepEqual(k.fired, []);
49+ }
50+});
51+
52+test('Shift+I opens inbox while issues need the g i sequence', () => {
53+ const k = keyboard();
54+ k.press('I', true); k.press('i');
55+ assert.deepEqual(k.fired, ['inbox']);
56+ k.press('g'); k.press('i');
57+ assert.deepEqual(k.fired, ['inbox', 'i']);
58+});
59+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
60+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
61+ const k = keyboard(options); k.press('I', true);
62+ assert.deepEqual(k.fired, [], JSON.stringify(options));
63+ }
64+});
65+test('shifted punctuation still opens help and the command palette', () => {
66+ const k = keyboard();
67+ k.press('?', true); k.press(':', true);
68+ assert.deepEqual(k.fired, ['help', 'cmd']);
69+});
70+test('g prefix expires without scheduling real timers', () => {
71+ const k = keyboard();
72+ k.press('g'); k.expireTimers(); k.press('s');
73+ assert.deepEqual(k.fired, []);
74+ k.press('g'); k.press('s');
75+ assert.deepEqual(k.fired, ['s']);
76+});
D internal/adapters/web/jstest/keys.test.mjs +0 -29
@@ -1,29 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox', 'i']);
24-});
25-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28- }
29-});
M internal/adapters/web/keys.templ +3 -2
@@ -23,7 +23,9 @@ templ Keys() {
2323 k-filter from:body -> ermite.focusFilter();
2424 k-gh from:body -> ermite.go('.brand');
2525 k-s from:body -> ermite.tab('s');
26- k-t from:body -> ermite.tab('t');
26+ k-f from:body -> ermite.tab('f');
27+ k-b from:body -> ermite.tab('b');
28+ k-a from:body -> ermite.tab('a');
2729 k-c from:body -> ermite.tab('c');
2830 k-r from:body -> ermite.tab('r');
2931 k-m from:body -> ermite.tab('m');
@@ -39,7 +41,6 @@ templ Keys() {
3941 k-permalink from:body -> ermite.permalink();
4042 k-merge from:body -> window.ermiteMR?.openMerge();
4143 k-read from:body -> window.ermiteMR?.markRead();
42- k-gf from:body -> window.ermiteMR?.focusFiles();
4344 k-keys from:body -> ermite.toggleKeys()"
4445 ></div>
4546 }
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
A internal/adapters/web/keys_test.go +26 -0
@@ -0,0 +1,26 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestSectionShortcutSequences(t *testing.T) {
12+ sections := []Section{}
13+ for _, key := range []string{"s", "f", "c", "b", "m", "i", "r", "a"} {
14+ sections = append(sections, Section{Key: key, Label: key, Href: "/" + key})
15+ }
16+ header := renderToString(t, Header(Layout{Sections: sections}))
17+ bindings := renderToString(t, Keys())
18+ for _, section := range sections {
19+ assert.Contains(t, header, `data-key="g `+section.Key+`"`)
20+ assert.NotContains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)
21+ assert.Contains(t, header, `<kbd aria-hidden="true">g `+section.Key+`</kbd>`)
22+ assert.Contains(t, bindings, "ermite.tab('"+section.Key+"')")
23+ }
24+ assert.NotContains(t, bindings, "k-gf")
25+ assert.NotContains(t, bindings, "k-t from:body")
26+}
M internal/adapters/web/mr.templ +1 -1
@@ -430,7 +430,7 @@ templ mrFooter(p MRPage) {
430430 templ mrRail(p MRPage) {
431431 <section aria-labelledby="r-files" class="r-files">
432432 <h2 id="r-files">
433- Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">g f</kbd>
433+ Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">l</kbd>
434434 <button type="button" class="btn icon fold" data-js aria-expanded="true" title="fold the list of files" hx-on:click="ermiteMR.foldFiles()">
435435 <span class="ico i-chevron" aria-hidden="true"></span><span class="sr">fold the list of files</span>
436436 </button>
M internal/adapters/web/mr_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr_test.go +1 -1
@@ -157,7 +157,7 @@ func TestMRPanelsAndMenus(t *testing.T) {
157157 assert.Contains(t, footer, "more commits")
158158 assert.Contains(t, footer, "1/2 failed")
159159 rail := renderToString(t, mrRail(p))
160- assert.Contains(t, rail, "g f")
160+ assert.Contains(t, rail, `<kbd aria-hidden="true">l</kbd>`)
161161 for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {
162162 assert.NotContains(t, rail, text)
163163 }
M internal/adapters/web/overlays.templ +2 -2
@@ -26,7 +26,7 @@ templ OverlayHelp(mr bool) {
2626 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>
2727 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
2828 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
29- <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>
29+ <div><dt><kbd>g s</kbd> <kbd>g f</kbd> <kbd>g c</kbd> <kbd>g b</kbd> <kbd>g m</kbd> <kbd>g i</kbd> <kbd>g r</kbd> <kbd>g a</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd></div>
3030 if SignedIn(ctx) {
3131 <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
3232 }
@@ -37,7 +37,7 @@ templ OverlayHelp(mr bool) {
3737 <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div>
3838 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
3939 if mr {
40- <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div>
40+ <div><dt><kbd>l</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div>
4141 <div><dt><kbd>M</kbd></dt><dd>merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges</dd></div>
4242 <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div>
4343 }
M internal/adapters/web/overlays_templ.go +2 -2 generated file: collapsed
M internal/adapters/web/pages.templ +8 -0
@@ -115,6 +115,14 @@ templ summaryRail(p SummaryPage) {
115115 if p.Latest != nil {
116116 @latestTagFact(*p.Latest)
117117 }
118+ if r := p.Release; r != nil {
119+ <dt>release</dt>
120+ <dd>
121+ <a href={ templ.SafeURL(r.Href) }>{ r.Tag }</a>{ " · " }
122+ <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>{ " · " }
123+ <a href={ templ.SafeURL(r.AllHref) }>all releases</a>
124+ </dd>
125+ }
118126 </dl>
119127 </section>
120128 if len(p.Recent) > 0 {
M internal/adapters/web/pages_templ.go +371 -261 generated file: collapsed
M internal/adapters/web/releases.templ +16 -9
@@ -40,15 +40,22 @@ templ Releases(l Layout, p ReleaseListPage) {
4040
4141 templ ReleaseDetail(l Layout, p ReleaseDetailPage) {
4242 @Shell(l, nil) {
43- <h1>{ p.Title }</h1>
44- <p>
45- <span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)
46- if p.SHA != "" {
47- <a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a>
48- }
49- <span class="dim">{ p.Published }</span>
50- </p>
51- <div class="md">@templ.Raw(string(p.Notes))</div>
43+ <section class="mr-card">
44+ <header class="show mr-h">
45+ <h1>{ p.Title }</h1>
46+ <dl class="kv show-kv">
47+ <dt>tag</dt>
48+ <dd><span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)</dd>
49+ if p.SHA != "" {
50+ <dt>commit</dt>
51+ <dd><a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a></dd>
52+ }
53+ <dt>published</dt>
54+ <dd>{ p.Published }</dd>
55+ </dl>
56+ </header>
57+ <article class="mr-desc prose">@templ.Raw(string(p.Notes))</article>
58+ </section>
5259 <h2>Assets</h2>
5360 if len(p.Assets) == 0 {
5461 <p class="dim">no assets.</p>
M internal/adapters/web/releases_templ.go +32 -28 generated file: collapsed
M internal/adapters/web/releases_test.go +12 -2
@@ -38,7 +38,7 @@ func newReleaseFixture(t *testing.T) *releaseFixture {
3838 t.Helper()
3939 repo := repoName(t, "proj")
4040 f := &releaseFixture{svc: mocks.NewReleaseService(t), data: "0123456789"}
41- f.published = domain.Release{ID: 1, Repo: repo, Tag: "stable/v1", Title: "First release", Notes: "# Notes\n\n**Ready** [bad](javascript:alert(1)) <script>alert(1)</script>\n\n[guide](guide.md)", TargetCommitish: strings.Repeat("a", 40), PublishedAt: fixedNow, Prerelease: true}
41+ f.published = domain.Release{ID: 1, Repo: repo, Tag: "stable/v1", Title: "First release", Notes: "# Notes\n\n**Ready** [bad](javascript:alert(1)) <script>alert(1)</script>\n\n[guide](guide.md)\n\n```sh\ncurl https://example.com\n```", TargetCommitish: strings.Repeat("a", 40), PublishedAt: fixedNow, Prerelease: true}
4242 f.draft = domain.Release{ID: 2, Repo: repo, Tag: "unpublished", Title: "Secret draft", Draft: true, MakeLatest: true}
4343 f.asset = domain.ReleaseAsset{ID: 7, ReleaseID: 1, Repo: repo, Name: "build.zip", Size: 10, ContentType: "application/zip", Digest: "sha256:" + strings.Repeat("b", 64)}
4444 f.svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, mock.Anything).RunAndReturn(func(_ context.Context, p domain.Principal, _ domain.RepoName, ref ports.ReleaseRef) (domain.Release, error) {
@@ -120,7 +120,8 @@ func TestReleasePages(t *testing.T) {
120120 assert.Contains(t, body, "2026-10-12")
121121 assert.Contains(t, body, `<td>1</td>`)
122122 assert.Contains(t, body, `?page=next%2B%2F%3D`)
123- assert.Contains(t, body, `aria-keyshortcuts="l"`)
123+ assert.Contains(t, body, `data-key="g r"`)
124+ assert.NotContains(t, body, `aria-keyshortcuts="g r"`)
124125 assert.Contains(t, body, "view=releases")
125126 }
126127 owner := f.request("GET", "/proj/releases?page=cursor%2B%2F%3D", "tok", "")
@@ -137,6 +138,15 @@ func TestReleasePages(t *testing.T) {
137138 for _, text := range []string{"<strong>Ready</strong>", "build.zip", humanSize(10), "application/zip", `href="/proj/releases/assets/7"`, `href="/proj/tree/stable/v1"`, `href="/proj/commit/` + strings.Repeat("a", 40) + `"`, "view=release"} {
138139 assert.Contains(t, body, text)
139140 }
141+ notes := strings.SplitN(body, `<article class="mr-desc prose">`, 2)
142+ require.Len(t, notes, 2)
143+ notesBody := strings.SplitN(notes[1], "</article>", 2)[0]
144+ assert.Contains(t, notesBody, "<pre")
145+ assert.Contains(t, notesBody, "<code")
146+ assert.Contains(t, notesBody, "curl")
147+ assert.Contains(t, notesBody, "</code></pre>")
148+ assert.Contains(t, notesBody, `<h1 id="md-notes">Notes</h1>`)
149+ assert.Contains(t, body, `<section class="mr-card">`)
140150 assert.NotContains(t, body, "<script>alert")
141151 assert.NotContains(t, body, `href="javascript:`)
142152 assert.Contains(t, body, `href="/proj/blob/stable/v1/guide.md"`)
M internal/adapters/web/repo.go +4 -4
@@ -144,9 +144,9 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La
144144 }
145145 l.Sections = []Section{
146146 mk("s", "Summary", repoHref(d.name()), "summary"),
147- mk("t", "Files", repoHref(d.name(), "tree", def), "tree"),
147+ mk("f", "Files", repoHref(d.name(), "tree", def), "tree"),
148148 mk("c", "Commits", repoHref(d.name(), "commits", def), "commits"),
149- mk("r", "Branches & tags", repoHref(d.name(), "refs"), "refs"),
149+ mk("b", "Branches & tags", repoHref(d.name(), "refs"), "refs"),
150150 }
151151 if s.cfg.MergeRequests != nil {
152152 mrs := mk("m", "Merge requests", mrBase(d.name()), "merge-requests")
@@ -157,14 +157,14 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La
157157 l.Sections = append(l.Sections, mk("i", "Issues", issuesBase(d.name()), "issues"))
158158 }
159159 if s.cfg.Releases != nil {
160- l.Sections = append(l.Sections, mk("l", "Releases", releasesBase(d.name()), "releases"))
160+ l.Sections = append(l.Sections, mk("r", "Releases", releasesBase(d.name()), "releases"))
161161 }
162162 l.Sections = s.withActions(l.Sections, d.name(), section)
163163 l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}}
164164
165165 l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"})
166166 for _, sec := range l.Sections {
167- l.Cmd = append(l.Cmd, CmdItem{Kind: "section", Label: strings.ToLower(sec.Label), Href: sec.Href})
167+ l.Cmd = append(l.Cmd, CmdItem{Kind: "section", Label: strings.ToLower(sec.Label) + " (g " + sec.Key + ")", Href: sec.Href})
168168 }
169169 if l.Ref != "" {
170170 for _, b := range d.branches {
M internal/adapters/web/shell.templ +2 -3
@@ -172,8 +172,7 @@ templ Header(l Layout) {
172172 <li>
173173 <a
174174 href={ templ.SafeURL(s.Href) }
175- data-key={ s.Key }
176- aria-keyshortcuts={ s.Key }
175+ data-key={ "g " + s.Key }
177176 if s.Current {
178177 aria-current="page"
179178 }
@@ -182,7 +181,7 @@ templ Header(l Layout) {
182181 if s.Count != "" {
183182 <span class="count">{ s.Count }<span class="sr">{ " open" }</span></span>
184183 }
185- <kbd aria-hidden="true">{ s.Key }</kbd>
184+ <kbd aria-hidden="true">{ "g " + s.Key }</kbd>
186185 </a>
187186 </li>
188187 }
M internal/adapters/web/shell_templ.go +48 -61 generated file: collapsed
M internal/adapters/web/static/js/ermite.js +13 -4
@@ -112,7 +112,7 @@
112112 $(sel)?.click();
113113 },
114114 tab(key) {
115- $(`.tabs [data-key=${key}]`)?.click();
115+ $(`.tabs [data-key="g ${key}"]`)?.click();
116116 },
117117
118118 // The preference "single-key shortcuts" (ermiteKeys): the checkbox of the
@@ -617,11 +617,11 @@
617617
618618 // 1. Keys.
619619 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
620- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', I: 'inbox', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
620+ I: 'inbox', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
621621 // the merge request page (mr.js): the merge menu, mark read
622622 M: 'merge', R: 'read' };
623- // "g t" is the key "t": the tabs have one event each.
624- const goto = { h: 'gh', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', g: 'top', f: 'gf' };
623+ // Section keys are the second letter of the g sequence.
624+ const goto = { h: 'gh', s: 's', f: 'f', c: 'c', b: 'b', m: 'm', i: 'i', r: 'r', a: 'a', g: 'top' };
625625 let pending = 0;
626626 let pendingTimer = 0;
627627 addEventListener('keydown', (e) => {
@@ -647,6 +647,15 @@
647647 // behind it does not react (scripted clicks would go through inert).
648648 if ($('dialog[open]') && !['?', 'b', ':', 'f', 'Enter'].includes(e.key)) { pending = 0; return; }
649649 if (e.key === 'Enter' && e.target.closest('a, button, summary')) return;
650+ // Some layouts report lowercase e.key even with Shift. Uppercase actions
651+ // also interrupt a pending g rather than being swallowed by it.
652+ const actionKey = e.shiftKey ? e.key.toUpperCase() : e.key;
653+ if (['G', 'M', 'R', 'E', 'I'].includes(actionKey)) {
654+ pending = 0;
655+ if (actionKey === 'I' && !$('[data-inbox]')) return;
656+ e.preventDefault();
657+ return ermite.fire(single[actionKey]);
658+ }
650659 if (pending) {
651660 const name = goto[e.key];
652661 pending = 0;
M internal/adapters/web/static/js/mr.js +2 -2
@@ -8,7 +8,7 @@
88 // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter
99 // submits (the browser does it: the focus is on the button), Esc closes.
1010 // R marks the head read. Nothing merges on one key.
11-// 3. g f puts the focus in the list of files (opening it when folded); j / k
11+// 3. l puts the focus in the list of files (opening it when folded); j / k
1212 // move in it, Enter opens the file, Esc goes back to the diff. The button
1313 // of its header folds the list, and the choice is remembered.
1414 //
@@ -186,7 +186,7 @@ window.ermiteMR = (() => {
186186 return true;
187187 }
188188
189- // The list of files by keyboard: g f focuses the current file.
189+ // The list of files by keyboard: l focuses the current file.
190190 const links = () => $$('.r-files .files > li:not([hidden]) > a');
191191 function focusFiles() {
192192 foldFiles(false);
M internal/adapters/web/summary.go +21 -0
@@ -68,6 +68,7 @@ func (s *Server) summary(ctx context.Context, r *http.Request) (templ.Component,
6868
6969 p.Facts = s.facts(d, now)
7070 p.Latest = s.latest(ctx, d)
71+ p.Release = s.summaryRelease(ctx, d.repo.Name, now)
7172 p.Actions = s.recentActions(ctx, d.repo.Name, now)
7273
7374 if rd, err := s.readme(ctx, d, ref); err != nil {
@@ -272,3 +273,23 @@ func (s *Server) listRecentActions(ctx context.Context, repo domain.RepoName, no
272273 }
273274 return a, nil
274275 }
276+
277+// summaryRelease uses the same Latest selection as the releases page. A
278+// missing service or an unavailable release must not prevent repository browsing.
279+func (s *Server) summaryRelease(ctx context.Context, repo domain.RepoName, now time.Time) *LatestRelease {
280+ if s.cfg.Releases == nil {
281+ return nil
282+ }
283+ rel, err := s.cfg.Releases.GetRelease(ctx, principal(ctx), repo, ports.ReleaseRef{Latest: true})
284+ if err != nil {
285+ if !errors.Is(err, domain.ErrNotFound) {
286+ log := zerowrap.FromCtx(ctx)
287+ log.Warn().Err(err).Str("repo", repo.String()).Msg("read the latest release")
288+ }
289+ return nil
290+ }
291+ if rel.Draft || rel.PublishedAt.IsZero() {
292+ return nil
293+ }
294+ return &LatestRelease{Tag: rel.Tag, Href: releaseHref(repo.String(), rel.Tag), When: ago(now, rel.PublishedAt), WhenAt: rfc3339(rel.PublishedAt), AllHref: releasesBase(repo.String())}
295+}
A internal/adapters/web/summary_releases_test.go +49 -0
@@ -0,0 +1,49 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+
14+ "bnema.dev/ermite/internal/domain"
15+ "bnema.dev/ermite/internal/ports"
16+ "bnema.dev/ermite/internal/ports/mocks"
17+)
18+
19+func TestSummaryRelease(t *testing.T) {
20+ repo := repoName(t, "proj")
21+ for _, tc := range []struct {
22+ name string
23+ release domain.Release
24+ err error
25+ visible bool
26+ }{
27+ {name: "published", release: domain.Release{Tag: "v0.9.1", PublishedAt: fixedNow.Add(-6 * time.Minute)}, visible: true},
28+ {name: "none", err: domain.ErrNotFound},
29+ {name: "draft", release: domain.Release{Tag: "secret", Draft: true, PublishedAt: fixedNow}},
30+ } {
31+ t.Run(tc.name, func(t *testing.T) {
32+ svc := mocks.NewReleaseService(t)
33+ svc.EXPECT().GetRelease(mock.Anything, mock.Anything, repo, ports.ReleaseRef{Latest: true}).Return(tc.release, tc.err).Once()
34+ srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: mocks.NewBrowseService(t), Releases: svc})
35+ rel := srv.summaryRelease(t.Context(), repo, fixedNow)
36+ out := strings.Join(strings.Fields(renderRail(t, SummaryPage{Latest: &LatestTag{Name: "v1", Count: 1}, Release: rel})), " ")
37+ if !tc.visible {
38+ assert.Nil(t, rel)
39+ assert.NotContains(t, out, "<dt>release</dt>")
40+ return
41+ }
42+ require.NotNil(t, rel)
43+ assert.Contains(t, out, `<a href="/proj/releases/tag/v0.9.1">v0.9.1</a> · <time`)
44+ assert.Contains(t, out, `datetime="`+rfc3339(tc.release.PublishedAt)+`"`)
45+ assert.Contains(t, out, `>`+ago(fixedNow, tc.release.PublishedAt)+`</time> · <a href="/proj/releases">all releases</a>`)
46+ assert.Regexp(t, `(?s)<dt>latest tag</dt>.*<dt>release</dt>`, out)
47+ })
48+ }
49+}
M internal/adapters/web/view.go +7 -1
@@ -211,12 +211,18 @@ type SummaryPage struct {
211211 Outline []Heading
212212 AllHref string
213213 // Latest is the newest tag, nil when the repository has none.
214- Latest *LatestTag
214+ Latest *LatestTag
215+ Release *LatestRelease
215216 // Actions is the recent runs of the rail, nil when the viewer cannot read
216217 // CI (or the server runs none).
217218 Actions *RecentActions
218219 }
219220
221+// LatestRelease is the published release selected as Latest by the release service.
222+type LatestRelease struct {
223+ Tag, Href, When, WhenAt, AllHref string
224+}
225+
220226 // RecentActions is the "Recent actions" block of the summary rail: the newest
221227 // runs. With no run the block is in the page but hidden, so that the first
222228 // run reaches it live.
M internal/app/testdata/web/blob-binary.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -228,14 +228,16 @@
228228 </div>
229229 <div>
230230 <dt>
231-<kbd>s</kbd>
232-<kbd>t</kbd>
233-<kbd>c</kbd>
234-<kbd>r</kbd>
235-<kbd>m</kbd>
236-<kbd>i</kbd>
231+<kbd>g s</kbd>
232+<kbd>g f</kbd>
233+<kbd>g c</kbd>
234+<kbd>g b</kbd>
235+<kbd>g m</kbd>
236+<kbd>g i</kbd>
237+<kbd>g r</kbd>
238+<kbd>g a</kbd>
237239 </dt>
238-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
240+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
239241 </div>
240242 <div>
241243 <dt>
@@ -334,45 +336,45 @@
334336 <span class="k">go</span>
335337 </a>
336338 </li>
337-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
339+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
338340 <a role="presentation" data-href="/files">
339-<span class="lbl">summary</span>
341+<span class="lbl">summary (g s)</span>
340342 <span class="k">section</span>
341343 </a>
342344 </li>
343-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
345+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
344346 <a role="presentation" data-href="/files/tree/main">
345-<span class="lbl">files</span>
347+<span class="lbl">files (g f)</span>
346348 <span class="k">section</span>
347349 </a>
348350 </li>
349-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
351+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
350352 <a role="presentation" data-href="/files/commits/main">
351-<span class="lbl">commits</span>
353+<span class="lbl">commits (g c)</span>
352354 <span class="k">section</span>
353355 </a>
354356 </li>
355-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
357+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
356358 <a role="presentation" data-href="/files/refs">
357-<span class="lbl">branches &amp; tags</span>
359+<span class="lbl">branches &amp; tags (g b)</span>
358360 <span class="k">section</span>
359361 </a>
360362 </li>
361-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
363+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
362364 <a role="presentation" data-href="/files/merge-requests">
363-<span class="lbl">merge requests</span>
365+<span class="lbl">merge requests (g m)</span>
364366 <span class="k">section</span>
365367 </a>
366368 </li>
367-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
369+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
368370 <a role="presentation" data-href="/files/releases">
369-<span class="lbl">releases</span>
371+<span class="lbl">releases (g r)</span>
370372 <span class="k">section</span>
371373 </a>
372374 </li>
373-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
375+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
374376 <a role="presentation" data-href="/files/actions">
375-<span class="lbl">actions</span>
377+<span class="lbl">actions (g a)</span>
376378 <span class="k">section</span>
377379 </a>
378380 </li>
@@ -425,7 +427,9 @@
425427 k-filter from:body -> ermite.focusFilter();
426428 k-gh from:body -> ermite.go('.brand');
427429 k-s from:body -> ermite.tab('s');
428- k-t from:body -> ermite.tab('t');
430+ k-f from:body -> ermite.tab('f');
431+ k-b from:body -> ermite.tab('b');
432+ k-a from:body -> ermite.tab('a');
429433 k-c from:body -> ermite.tab('c');
430434 k-r from:body -> ermite.tab('r');
431435 k-m from:body -> ermite.tab('m');
@@ -441,7 +445,6 @@
441445 k-permalink from:body -> ermite.permalink();
442446 k-merge from:body -> window.ermiteMR?.openMerge();
443447 k-read from:body -> window.ermiteMR?.markRead();
444- k-gf from:body -> window.ermiteMR?.focusFiles();
445448 k-keys from:body -> ermite.toggleKeys()">
446449 </div>
447450 </body>
M internal/app/testdata/web/blob-code.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -298,14 +298,16 @@
298298 </div>
299299 <div>
300300 <dt>
301-<kbd>s</kbd>
302-<kbd>t</kbd>
303-<kbd>c</kbd>
304-<kbd>r</kbd>
305-<kbd>m</kbd>
306-<kbd>i</kbd>
301+<kbd>g s</kbd>
302+<kbd>g f</kbd>
303+<kbd>g c</kbd>
304+<kbd>g b</kbd>
305+<kbd>g m</kbd>
306+<kbd>g i</kbd>
307+<kbd>g r</kbd>
308+<kbd>g a</kbd>
307309 </dt>
308-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
310+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
309311 </div>
310312 <div>
311313 <dt>
@@ -404,45 +406,45 @@
404406 <span class="k">go</span>
405407 </a>
406408 </li>
407-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
409+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
408410 <a role="presentation" data-href="/files">
409-<span class="lbl">summary</span>
411+<span class="lbl">summary (g s)</span>
410412 <span class="k">section</span>
411413 </a>
412414 </li>
413-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
415+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
414416 <a role="presentation" data-href="/files/tree/main">
415-<span class="lbl">files</span>
417+<span class="lbl">files (g f)</span>
416418 <span class="k">section</span>
417419 </a>
418420 </li>
419-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
421+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
420422 <a role="presentation" data-href="/files/commits/main">
421-<span class="lbl">commits</span>
423+<span class="lbl">commits (g c)</span>
422424 <span class="k">section</span>
423425 </a>
424426 </li>
425-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
427+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
426428 <a role="presentation" data-href="/files/refs">
427-<span class="lbl">branches &amp; tags</span>
429+<span class="lbl">branches &amp; tags (g b)</span>
428430 <span class="k">section</span>
429431 </a>
430432 </li>
431-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
433+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
432434 <a role="presentation" data-href="/files/merge-requests">
433-<span class="lbl">merge requests</span>
435+<span class="lbl">merge requests (g m)</span>
434436 <span class="k">section</span>
435437 </a>
436438 </li>
437-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
439+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
438440 <a role="presentation" data-href="/files/releases">
439-<span class="lbl">releases</span>
441+<span class="lbl">releases (g r)</span>
440442 <span class="k">section</span>
441443 </a>
442444 </li>
443-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
445+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
444446 <a role="presentation" data-href="/files/actions">
445-<span class="lbl">actions</span>
447+<span class="lbl">actions (g a)</span>
446448 <span class="k">section</span>
447449 </a>
448450 </li>
@@ -495,7 +497,9 @@
495497 k-filter from:body -> ermite.focusFilter();
496498 k-gh from:body -> ermite.go('.brand');
497499 k-s from:body -> ermite.tab('s');
498- k-t from:body -> ermite.tab('t');
500+ k-f from:body -> ermite.tab('f');
501+ k-b from:body -> ermite.tab('b');
502+ k-a from:body -> ermite.tab('a');
499503 k-c from:body -> ermite.tab('c');
500504 k-r from:body -> ermite.tab('r');
501505 k-m from:body -> ermite.tab('m');
@@ -511,7 +515,6 @@
511515 k-permalink from:body -> ermite.permalink();
512516 k-merge from:body -> window.ermiteMR?.openMerge();
513517 k-read from:body -> window.ermiteMR?.markRead();
514- k-gf from:body -> window.ermiteMR?.focusFiles();
515518 k-keys from:body -> ermite.toggleKeys()">
516519 </div>
517520 </body>
M internal/app/testdata/web/blob-empty.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -228,14 +228,16 @@
228228 </div>
229229 <div>
230230 <dt>
231-<kbd>s</kbd>
232-<kbd>t</kbd>
233-<kbd>c</kbd>
234-<kbd>r</kbd>
235-<kbd>m</kbd>
236-<kbd>i</kbd>
231+<kbd>g s</kbd>
232+<kbd>g f</kbd>
233+<kbd>g c</kbd>
234+<kbd>g b</kbd>
235+<kbd>g m</kbd>
236+<kbd>g i</kbd>
237+<kbd>g r</kbd>
238+<kbd>g a</kbd>
237239 </dt>
238-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
240+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
239241 </div>
240242 <div>
241243 <dt>
@@ -334,45 +336,45 @@
334336 <span class="k">go</span>
335337 </a>
336338 </li>
337-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
339+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
338340 <a role="presentation" data-href="/files">
339-<span class="lbl">summary</span>
341+<span class="lbl">summary (g s)</span>
340342 <span class="k">section</span>
341343 </a>
342344 </li>
343-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
345+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
344346 <a role="presentation" data-href="/files/tree/main">
345-<span class="lbl">files</span>
347+<span class="lbl">files (g f)</span>
346348 <span class="k">section</span>
347349 </a>
348350 </li>
349-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
351+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
350352 <a role="presentation" data-href="/files/commits/main">
351-<span class="lbl">commits</span>
353+<span class="lbl">commits (g c)</span>
352354 <span class="k">section</span>
353355 </a>
354356 </li>
355-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
357+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
356358 <a role="presentation" data-href="/files/refs">
357-<span class="lbl">branches &amp; tags</span>
359+<span class="lbl">branches &amp; tags (g b)</span>
358360 <span class="k">section</span>
359361 </a>
360362 </li>
361-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
363+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
362364 <a role="presentation" data-href="/files/merge-requests">
363-<span class="lbl">merge requests</span>
365+<span class="lbl">merge requests (g m)</span>
364366 <span class="k">section</span>
365367 </a>
366368 </li>
367-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
369+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
368370 <a role="presentation" data-href="/files/releases">
369-<span class="lbl">releases</span>
371+<span class="lbl">releases (g r)</span>
370372 <span class="k">section</span>
371373 </a>
372374 </li>
373-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
375+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
374376 <a role="presentation" data-href="/files/actions">
375-<span class="lbl">actions</span>
377+<span class="lbl">actions (g a)</span>
376378 <span class="k">section</span>
377379 </a>
378380 </li>
@@ -425,7 +427,9 @@
425427 k-filter from:body -> ermite.focusFilter();
426428 k-gh from:body -> ermite.go('.brand');
427429 k-s from:body -> ermite.tab('s');
428- k-t from:body -> ermite.tab('t');
430+ k-f from:body -> ermite.tab('f');
431+ k-b from:body -> ermite.tab('b');
432+ k-a from:body -> ermite.tab('a');
429433 k-c from:body -> ermite.tab('c');
430434 k-r from:body -> ermite.tab('r');
431435 k-m from:body -> ermite.tab('m');
@@ -441,7 +445,6 @@
441445 k-permalink from:body -> ermite.permalink();
442446 k-merge from:body -> window.ermiteMR?.openMerge();
443447 k-read from:body -> window.ermiteMR?.markRead();
444- k-gf from:body -> window.ermiteMR?.focusFiles();
445448 k-keys from:body -> ermite.toggleKeys()">
446449 </div>
447450 </body>
M internal/app/testdata/web/blob-image.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -230,14 +230,16 @@
230230 </div>
231231 <div>
232232 <dt>
233-<kbd>s</kbd>
234-<kbd>t</kbd>
235-<kbd>c</kbd>
236-<kbd>r</kbd>
237-<kbd>m</kbd>
238-<kbd>i</kbd>
233+<kbd>g s</kbd>
234+<kbd>g f</kbd>
235+<kbd>g c</kbd>
236+<kbd>g b</kbd>
237+<kbd>g m</kbd>
238+<kbd>g i</kbd>
239+<kbd>g r</kbd>
240+<kbd>g a</kbd>
239241 </dt>
240-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
242+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
241243 </div>
242244 <div>
243245 <dt>
@@ -336,45 +338,45 @@
336338 <span class="k">go</span>
337339 </a>
338340 </li>
339-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
341+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
340342 <a role="presentation" data-href="/files">
341-<span class="lbl">summary</span>
343+<span class="lbl">summary (g s)</span>
342344 <span class="k">section</span>
343345 </a>
344346 </li>
345-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
347+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
346348 <a role="presentation" data-href="/files/tree/main">
347-<span class="lbl">files</span>
349+<span class="lbl">files (g f)</span>
348350 <span class="k">section</span>
349351 </a>
350352 </li>
351-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
353+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
352354 <a role="presentation" data-href="/files/commits/main">
353-<span class="lbl">commits</span>
355+<span class="lbl">commits (g c)</span>
354356 <span class="k">section</span>
355357 </a>
356358 </li>
357-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
359+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
358360 <a role="presentation" data-href="/files/refs">
359-<span class="lbl">branches &amp; tags</span>
361+<span class="lbl">branches &amp; tags (g b)</span>
360362 <span class="k">section</span>
361363 </a>
362364 </li>
363-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
365+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
364366 <a role="presentation" data-href="/files/merge-requests">
365-<span class="lbl">merge requests</span>
367+<span class="lbl">merge requests (g m)</span>
366368 <span class="k">section</span>
367369 </a>
368370 </li>
369-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
371+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
370372 <a role="presentation" data-href="/files/releases">
371-<span class="lbl">releases</span>
373+<span class="lbl">releases (g r)</span>
372374 <span class="k">section</span>
373375 </a>
374376 </li>
375-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
377+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
376378 <a role="presentation" data-href="/files/actions">
377-<span class="lbl">actions</span>
379+<span class="lbl">actions (g a)</span>
378380 <span class="k">section</span>
379381 </a>
380382 </li>
@@ -427,7 +429,9 @@
427429 k-filter from:body -> ermite.focusFilter();
428430 k-gh from:body -> ermite.go('.brand');
429431 k-s from:body -> ermite.tab('s');
430- k-t from:body -> ermite.tab('t');
432+ k-f from:body -> ermite.tab('f');
433+ k-b from:body -> ermite.tab('b');
434+ k-a from:body -> ermite.tab('a');
431435 k-c from:body -> ermite.tab('c');
432436 k-r from:body -> ermite.tab('r');
433437 k-m from:body -> ermite.tab('m');
@@ -443,7 +447,6 @@
443447 k-permalink from:body -> ermite.permalink();
444448 k-merge from:body -> window.ermiteMR?.openMerge();
445449 k-read from:body -> window.ermiteMR?.markRead();
446- k-gf from:body -> window.ermiteMR?.focusFiles();
447450 k-keys from:body -> ermite.toggleKeys()">
448451 </div>
449452 </body>
M internal/app/testdata/web/blob-markdown.html +33 -30
@@ -76,31 +76,31 @@
7676 <nav class="tabs" aria-label="Sections">
7777 <ul>
7878 <li>
79-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
79+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
83+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
87+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
91+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
95+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
99+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
100100 </a>
101101 </li>
102102 <li>
103-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
103+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
104104 </a>
105105 </li>
106106 </ul>
@@ -295,14 +295,16 @@
295295 </div>
296296 <div>
297297 <dt>
298-<kbd>s</kbd>
299-<kbd>t</kbd>
300-<kbd>c</kbd>
301-<kbd>r</kbd>
302-<kbd>m</kbd>
303-<kbd>i</kbd>
298+<kbd>g s</kbd>
299+<kbd>g f</kbd>
300+<kbd>g c</kbd>
301+<kbd>g b</kbd>
302+<kbd>g m</kbd>
303+<kbd>g i</kbd>
304+<kbd>g r</kbd>
305+<kbd>g a</kbd>
304306 </dt>
305-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
307+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
306308 </div>
307309 <div>
308310 <dt>
@@ -401,45 +403,45 @@
401403 <span class="k">go</span>
402404 </a>
403405 </li>
404-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
406+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
405407 <a role="presentation" data-href="/files">
406-<span class="lbl">summary</span>
408+<span class="lbl">summary (g s)</span>
407409 <span class="k">section</span>
408410 </a>
409411 </li>
410-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
412+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
411413 <a role="presentation" data-href="/files/tree/main">
412-<span class="lbl">files</span>
414+<span class="lbl">files (g f)</span>
413415 <span class="k">section</span>
414416 </a>
415417 </li>
416-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
418+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
417419 <a role="presentation" data-href="/files/commits/main">
418-<span class="lbl">commits</span>
420+<span class="lbl">commits (g c)</span>
419421 <span class="k">section</span>
420422 </a>
421423 </li>
422-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
424+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
423425 <a role="presentation" data-href="/files/refs">
424-<span class="lbl">branches &amp; tags</span>
426+<span class="lbl">branches &amp; tags (g b)</span>
425427 <span class="k">section</span>
426428 </a>
427429 </li>
428-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
430+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
429431 <a role="presentation" data-href="/files/merge-requests">
430-<span class="lbl">merge requests</span>
432+<span class="lbl">merge requests (g m)</span>
431433 <span class="k">section</span>
432434 </a>
433435 </li>
434-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
436+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
435437 <a role="presentation" data-href="/files/releases">
436-<span class="lbl">releases</span>
438+<span class="lbl">releases (g r)</span>
437439 <span class="k">section</span>
438440 </a>
439441 </li>
440-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
442+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
441443 <a role="presentation" data-href="/files/actions">
442-<span class="lbl">actions</span>
444+<span class="lbl">actions (g a)</span>
443445 <span class="k">section</span>
444446 </a>
445447 </li>
@@ -492,7 +494,9 @@
492494 k-filter from:body -> ermite.focusFilter();
493495 k-gh from:body -> ermite.go('.brand');
494496 k-s from:body -> ermite.tab('s');
495- k-t from:body -> ermite.tab('t');
497+ k-f from:body -> ermite.tab('f');
498+ k-b from:body -> ermite.tab('b');
499+ k-a from:body -> ermite.tab('a');
496500 k-c from:body -> ermite.tab('c');
497501 k-r from:body -> ermite.tab('r');
498502 k-m from:body -> ermite.tab('m');
@@ -508,7 +512,6 @@
508512 k-permalink from:body -> ermite.permalink();
509513 k-merge from:body -> window.ermiteMR?.openMerge();
510514 k-read from:body -> window.ermiteMR?.markRead();
511- k-gf from:body -> window.ermiteMR?.focusFiles();
512515 k-keys from:body -> ermite.toggleKeys()">
513516 </div>
514517 </body>
M internal/app/testdata/web/blob-svg.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -253,14 +253,16 @@
253253 </div>
254254 <div>
255255 <dt>
256-<kbd>s</kbd>
257-<kbd>t</kbd>
258-<kbd>c</kbd>
259-<kbd>r</kbd>
260-<kbd>m</kbd>
261-<kbd>i</kbd>
256+<kbd>g s</kbd>
257+<kbd>g f</kbd>
258+<kbd>g c</kbd>
259+<kbd>g b</kbd>
260+<kbd>g m</kbd>
261+<kbd>g i</kbd>
262+<kbd>g r</kbd>
263+<kbd>g a</kbd>
262264 </dt>
263-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
265+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
264266 </div>
265267 <div>
266268 <dt>
@@ -359,45 +361,45 @@
359361 <span class="k">go</span>
360362 </a>
361363 </li>
362-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
364+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
363365 <a role="presentation" data-href="/files">
364-<span class="lbl">summary</span>
366+<span class="lbl">summary (g s)</span>
365367 <span class="k">section</span>
366368 </a>
367369 </li>
368-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
370+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
369371 <a role="presentation" data-href="/files/tree/main">
370-<span class="lbl">files</span>
372+<span class="lbl">files (g f)</span>
371373 <span class="k">section</span>
372374 </a>
373375 </li>
374-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
376+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
375377 <a role="presentation" data-href="/files/commits/main">
376-<span class="lbl">commits</span>
378+<span class="lbl">commits (g c)</span>
377379 <span class="k">section</span>
378380 </a>
379381 </li>
380-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
382+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
381383 <a role="presentation" data-href="/files/refs">
382-<span class="lbl">branches &amp; tags</span>
384+<span class="lbl">branches &amp; tags (g b)</span>
383385 <span class="k">section</span>
384386 </a>
385387 </li>
386-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
388+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
387389 <a role="presentation" data-href="/files/merge-requests">
388-<span class="lbl">merge requests</span>
390+<span class="lbl">merge requests (g m)</span>
389391 <span class="k">section</span>
390392 </a>
391393 </li>
392-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
394+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
393395 <a role="presentation" data-href="/files/releases">
394-<span class="lbl">releases</span>
396+<span class="lbl">releases (g r)</span>
395397 <span class="k">section</span>
396398 </a>
397399 </li>
398-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
400+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
399401 <a role="presentation" data-href="/files/actions">
400-<span class="lbl">actions</span>
402+<span class="lbl">actions (g a)</span>
401403 <span class="k">section</span>
402404 </a>
403405 </li>
@@ -450,7 +452,9 @@
450452 k-filter from:body -> ermite.focusFilter();
451453 k-gh from:body -> ermite.go('.brand');
452454 k-s from:body -> ermite.tab('s');
453- k-t from:body -> ermite.tab('t');
455+ k-f from:body -> ermite.tab('f');
456+ k-b from:body -> ermite.tab('b');
457+ k-a from:body -> ermite.tab('a');
454458 k-c from:body -> ermite.tab('c');
455459 k-r from:body -> ermite.tab('r');
456460 k-m from:body -> ermite.tab('m');
@@ -466,7 +470,6 @@
466470 k-permalink from:body -> ermite.permalink();
467471 k-merge from:body -> window.ermiteMR?.openMerge();
468472 k-read from:body -> window.ermiteMR?.markRead();
469- k-gf from:body -> window.ermiteMR?.focusFiles();
470473 k-keys from:body -> ermite.toggleKeys()">
471474 </div>
472475 </body>
M internal/app/testdata/web/blob-text.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -248,14 +248,16 @@
248248 </div>
249249 <div>
250250 <dt>
251-<kbd>s</kbd>
252-<kbd>t</kbd>
253-<kbd>c</kbd>
254-<kbd>r</kbd>
255-<kbd>m</kbd>
256-<kbd>i</kbd>
251+<kbd>g s</kbd>
252+<kbd>g f</kbd>
253+<kbd>g c</kbd>
254+<kbd>g b</kbd>
255+<kbd>g m</kbd>
256+<kbd>g i</kbd>
257+<kbd>g r</kbd>
258+<kbd>g a</kbd>
257259 </dt>
258-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
260+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
259261 </div>
260262 <div>
261263 <dt>
@@ -354,45 +356,45 @@
354356 <span class="k">go</span>
355357 </a>
356358 </li>
357-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
359+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
358360 <a role="presentation" data-href="/files">
359-<span class="lbl">summary</span>
361+<span class="lbl">summary (g s)</span>
360362 <span class="k">section</span>
361363 </a>
362364 </li>
363-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
365+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
364366 <a role="presentation" data-href="/files/tree/main">
365-<span class="lbl">files</span>
367+<span class="lbl">files (g f)</span>
366368 <span class="k">section</span>
367369 </a>
368370 </li>
369-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
371+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
370372 <a role="presentation" data-href="/files/commits/main">
371-<span class="lbl">commits</span>
373+<span class="lbl">commits (g c)</span>
372374 <span class="k">section</span>
373375 </a>
374376 </li>
375-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
377+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
376378 <a role="presentation" data-href="/files/refs">
377-<span class="lbl">branches &amp; tags</span>
379+<span class="lbl">branches &amp; tags (g b)</span>
378380 <span class="k">section</span>
379381 </a>
380382 </li>
381-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
383+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
382384 <a role="presentation" data-href="/files/merge-requests">
383-<span class="lbl">merge requests</span>
385+<span class="lbl">merge requests (g m)</span>
384386 <span class="k">section</span>
385387 </a>
386388 </li>
387-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
389+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
388390 <a role="presentation" data-href="/files/releases">
389-<span class="lbl">releases</span>
391+<span class="lbl">releases (g r)</span>
390392 <span class="k">section</span>
391393 </a>
392394 </li>
393-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
395+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
394396 <a role="presentation" data-href="/files/actions">
395-<span class="lbl">actions</span>
397+<span class="lbl">actions (g a)</span>
396398 <span class="k">section</span>
397399 </a>
398400 </li>
@@ -445,7 +447,9 @@
445447 k-filter from:body -> ermite.focusFilter();
446448 k-gh from:body -> ermite.go('.brand');
447449 k-s from:body -> ermite.tab('s');
448- k-t from:body -> ermite.tab('t');
450+ k-f from:body -> ermite.tab('f');
451+ k-b from:body -> ermite.tab('b');
452+ k-a from:body -> ermite.tab('a');
449453 k-c from:body -> ermite.tab('c');
450454 k-r from:body -> ermite.tab('r');
451455 k-m from:body -> ermite.tab('m');
@@ -461,7 +465,6 @@
461465 k-permalink from:body -> ermite.permalink();
462466 k-merge from:body -> window.ermiteMR?.openMerge();
463467 k-read from:body -> window.ermiteMR?.markRead();
464- k-gf from:body -> window.ermiteMR?.focusFiles();
465468 k-keys from:body -> ermite.toggleKeys()">
466469 </div>
467470 </body>
M internal/app/testdata/web/blob-too-large.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -228,14 +228,16 @@
228228 </div>
229229 <div>
230230 <dt>
231-<kbd>s</kbd>
232-<kbd>t</kbd>
233-<kbd>c</kbd>
234-<kbd>r</kbd>
235-<kbd>m</kbd>
236-<kbd>i</kbd>
231+<kbd>g s</kbd>
232+<kbd>g f</kbd>
233+<kbd>g c</kbd>
234+<kbd>g b</kbd>
235+<kbd>g m</kbd>
236+<kbd>g i</kbd>
237+<kbd>g r</kbd>
238+<kbd>g a</kbd>
237239 </dt>
238-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
240+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
239241 </div>
240242 <div>
241243 <dt>
@@ -334,45 +336,45 @@
334336 <span class="k">go</span>
335337 </a>
336338 </li>
337-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
339+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
338340 <a role="presentation" data-href="/files">
339-<span class="lbl">summary</span>
341+<span class="lbl">summary (g s)</span>
340342 <span class="k">section</span>
341343 </a>
342344 </li>
343-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
345+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
344346 <a role="presentation" data-href="/files/tree/main">
345-<span class="lbl">files</span>
347+<span class="lbl">files (g f)</span>
346348 <span class="k">section</span>
347349 </a>
348350 </li>
349-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
351+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
350352 <a role="presentation" data-href="/files/commits/main">
351-<span class="lbl">commits</span>
353+<span class="lbl">commits (g c)</span>
352354 <span class="k">section</span>
353355 </a>
354356 </li>
355-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
357+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
356358 <a role="presentation" data-href="/files/refs">
357-<span class="lbl">branches &amp; tags</span>
359+<span class="lbl">branches &amp; tags (g b)</span>
358360 <span class="k">section</span>
359361 </a>
360362 </li>
361-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
363+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
362364 <a role="presentation" data-href="/files/merge-requests">
363-<span class="lbl">merge requests</span>
365+<span class="lbl">merge requests (g m)</span>
364366 <span class="k">section</span>
365367 </a>
366368 </li>
367-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
369+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
368370 <a role="presentation" data-href="/files/releases">
369-<span class="lbl">releases</span>
371+<span class="lbl">releases (g r)</span>
370372 <span class="k">section</span>
371373 </a>
372374 </li>
373-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
375+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
374376 <a role="presentation" data-href="/files/actions">
375-<span class="lbl">actions</span>
377+<span class="lbl">actions (g a)</span>
376378 <span class="k">section</span>
377379 </a>
378380 </li>
@@ -425,7 +427,9 @@
425427 k-filter from:body -> ermite.focusFilter();
426428 k-gh from:body -> ermite.go('.brand');
427429 k-s from:body -> ermite.tab('s');
428- k-t from:body -> ermite.tab('t');
430+ k-f from:body -> ermite.tab('f');
431+ k-b from:body -> ermite.tab('b');
432+ k-a from:body -> ermite.tab('a');
429433 k-c from:body -> ermite.tab('c');
430434 k-r from:body -> ermite.tab('r');
431435 k-m from:body -> ermite.tab('m');
@@ -441,7 +445,6 @@
441445 k-permalink from:body -> ermite.permalink();
442446 k-merge from:body -> window.ermiteMR?.openMerge();
443447 k-read from:body -> window.ermiteMR?.markRead();
444- k-gf from:body -> window.ermiteMR?.focusFiles();
445448 k-keys from:body -> ermite.toggleKeys()">
446449 </div>
447450 </body>
M internal/app/testdata/web/blob-trojan.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -295,14 +295,16 @@
295295 </div>
296296 <div>
297297 <dt>
298-<kbd>s</kbd>
299-<kbd>t</kbd>
300-<kbd>c</kbd>
301-<kbd>r</kbd>
302-<kbd>m</kbd>
303-<kbd>i</kbd>
298+<kbd>g s</kbd>
299+<kbd>g f</kbd>
300+<kbd>g c</kbd>
301+<kbd>g b</kbd>
302+<kbd>g m</kbd>
303+<kbd>g i</kbd>
304+<kbd>g r</kbd>
305+<kbd>g a</kbd>
304306 </dt>
305-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
307+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
306308 </div>
307309 <div>
308310 <dt>
@@ -401,45 +403,45 @@
401403 <span class="k">go</span>
402404 </a>
403405 </li>
404-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
406+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
405407 <a role="presentation" data-href="/files">
406-<span class="lbl">summary</span>
408+<span class="lbl">summary (g s)</span>
407409 <span class="k">section</span>
408410 </a>
409411 </li>
410-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
412+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
411413 <a role="presentation" data-href="/files/tree/main">
412-<span class="lbl">files</span>
414+<span class="lbl">files (g f)</span>
413415 <span class="k">section</span>
414416 </a>
415417 </li>
416-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
418+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
417419 <a role="presentation" data-href="/files/commits/main">
418-<span class="lbl">commits</span>
420+<span class="lbl">commits (g c)</span>
419421 <span class="k">section</span>
420422 </a>
421423 </li>
422-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
424+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
423425 <a role="presentation" data-href="/files/refs">
424-<span class="lbl">branches &amp; tags</span>
426+<span class="lbl">branches &amp; tags (g b)</span>
425427 <span class="k">section</span>
426428 </a>
427429 </li>
428-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
430+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
429431 <a role="presentation" data-href="/files/merge-requests">
430-<span class="lbl">merge requests</span>
432+<span class="lbl">merge requests (g m)</span>
431433 <span class="k">section</span>
432434 </a>
433435 </li>
434-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
436+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
435437 <a role="presentation" data-href="/files/releases">
436-<span class="lbl">releases</span>
438+<span class="lbl">releases (g r)</span>
437439 <span class="k">section</span>
438440 </a>
439441 </li>
440-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
442+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
441443 <a role="presentation" data-href="/files/actions">
442-<span class="lbl">actions</span>
444+<span class="lbl">actions (g a)</span>
443445 <span class="k">section</span>
444446 </a>
445447 </li>
@@ -492,7 +494,9 @@
492494 k-filter from:body -> ermite.focusFilter();
493495 k-gh from:body -> ermite.go('.brand');
494496 k-s from:body -> ermite.tab('s');
495- k-t from:body -> ermite.tab('t');
497+ k-f from:body -> ermite.tab('f');
498+ k-b from:body -> ermite.tab('b');
499+ k-a from:body -> ermite.tab('a');
496500 k-c from:body -> ermite.tab('c');
497501 k-r from:body -> ermite.tab('r');
498502 k-m from:body -> ermite.tab('m');
@@ -508,7 +512,6 @@
508512 k-permalink from:body -> ermite.permalink();
509513 k-merge from:body -> window.ermiteMR?.openMerge();
510514 k-read from:body -> window.ermiteMR?.markRead();
511- k-gf from:body -> window.ermiteMR?.focusFiles();
512515 k-keys from:body -> ermite.toggleKeys()">
513516 </div>
514517 </body>
M internal/app/testdata/web/commit-big.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -280,14 +280,16 @@
280280 </div>
281281 <div>
282282 <dt>
283-<kbd>s</kbd>
284-<kbd>t</kbd>
285-<kbd>c</kbd>
286-<kbd>r</kbd>
287-<kbd>m</kbd>
288-<kbd>i</kbd>
283+<kbd>g s</kbd>
284+<kbd>g f</kbd>
285+<kbd>g c</kbd>
286+<kbd>g b</kbd>
287+<kbd>g m</kbd>
288+<kbd>g i</kbd>
289+<kbd>g r</kbd>
290+<kbd>g a</kbd>
289291 </dt>
290-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
292+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
291293 </div>
292294 <div>
293295 <dt>
@@ -386,45 +388,45 @@
386388 <span class="k">go</span>
387389 </a>
388390 </li>
389-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
391+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
390392 <a role="presentation" data-href="/diffs">
391-<span class="lbl">summary</span>
393+<span class="lbl">summary (g s)</span>
392394 <span class="k">section</span>
393395 </a>
394396 </li>
395-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
397+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
396398 <a role="presentation" data-href="/diffs/tree/main">
397-<span class="lbl">files</span>
399+<span class="lbl">files (g f)</span>
398400 <span class="k">section</span>
399401 </a>
400402 </li>
401-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
403+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
402404 <a role="presentation" data-href="/diffs/commits/main">
403-<span class="lbl">commits</span>
405+<span class="lbl">commits (g c)</span>
404406 <span class="k">section</span>
405407 </a>
406408 </li>
407-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
409+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
408410 <a role="presentation" data-href="/diffs/refs">
409-<span class="lbl">branches &amp; tags</span>
411+<span class="lbl">branches &amp; tags (g b)</span>
410412 <span class="k">section</span>
411413 </a>
412414 </li>
413-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
415+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
414416 <a role="presentation" data-href="/diffs/merge-requests">
415-<span class="lbl">merge requests</span>
417+<span class="lbl">merge requests (g m)</span>
416418 <span class="k">section</span>
417419 </a>
418420 </li>
419-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
421+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
420422 <a role="presentation" data-href="/diffs/releases">
421-<span class="lbl">releases</span>
423+<span class="lbl">releases (g r)</span>
422424 <span class="k">section</span>
423425 </a>
424426 </li>
425-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
427+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
426428 <a role="presentation" data-href="/diffs/actions">
427-<span class="lbl">actions</span>
429+<span class="lbl">actions (g a)</span>
428430 <span class="k">section</span>
429431 </a>
430432 </li>
@@ -477,7 +479,9 @@
477479 k-filter from:body -> ermite.focusFilter();
478480 k-gh from:body -> ermite.go('.brand');
479481 k-s from:body -> ermite.tab('s');
480- k-t from:body -> ermite.tab('t');
482+ k-f from:body -> ermite.tab('f');
483+ k-b from:body -> ermite.tab('b');
484+ k-a from:body -> ermite.tab('a');
481485 k-c from:body -> ermite.tab('c');
482486 k-r from:body -> ermite.tab('r');
483487 k-m from:body -> ermite.tab('m');
@@ -493,7 +497,6 @@
493497 k-permalink from:body -> ermite.permalink();
494498 k-merge from:body -> window.ermiteMR?.openMerge();
495499 k-read from:body -> window.ermiteMR?.markRead();
496- k-gf from:body -> window.ermiteMR?.focusFiles();
497500 k-keys from:body -> ermite.toggleKeys()">
498501 </div>
499502 </body>
M internal/app/testdata/web/commit-binary.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -271,14 +271,16 @@
271271 </div>
272272 <div>
273273 <dt>
274-<kbd>s</kbd>
275-<kbd>t</kbd>
276-<kbd>c</kbd>
277-<kbd>r</kbd>
278-<kbd>m</kbd>
279-<kbd>i</kbd>
274+<kbd>g s</kbd>
275+<kbd>g f</kbd>
276+<kbd>g c</kbd>
277+<kbd>g b</kbd>
278+<kbd>g m</kbd>
279+<kbd>g i</kbd>
280+<kbd>g r</kbd>
281+<kbd>g a</kbd>
280282 </dt>
281-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
283+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
282284 </div>
283285 <div>
284286 <dt>
@@ -377,45 +379,45 @@
377379 <span class="k">go</span>
378380 </a>
379381 </li>
380-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
382+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
381383 <a role="presentation" data-href="/diffs">
382-<span class="lbl">summary</span>
384+<span class="lbl">summary (g s)</span>
383385 <span class="k">section</span>
384386 </a>
385387 </li>
386-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
388+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
387389 <a role="presentation" data-href="/diffs/tree/main">
388-<span class="lbl">files</span>
390+<span class="lbl">files (g f)</span>
389391 <span class="k">section</span>
390392 </a>
391393 </li>
392-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
394+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
393395 <a role="presentation" data-href="/diffs/commits/main">
394-<span class="lbl">commits</span>
396+<span class="lbl">commits (g c)</span>
395397 <span class="k">section</span>
396398 </a>
397399 </li>
398-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
400+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
399401 <a role="presentation" data-href="/diffs/refs">
400-<span class="lbl">branches &amp; tags</span>
402+<span class="lbl">branches &amp; tags (g b)</span>
401403 <span class="k">section</span>
402404 </a>
403405 </li>
404-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
406+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
405407 <a role="presentation" data-href="/diffs/merge-requests">
406-<span class="lbl">merge requests</span>
408+<span class="lbl">merge requests (g m)</span>
407409 <span class="k">section</span>
408410 </a>
409411 </li>
410-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
412+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
411413 <a role="presentation" data-href="/diffs/releases">
412-<span class="lbl">releases</span>
414+<span class="lbl">releases (g r)</span>
413415 <span class="k">section</span>
414416 </a>
415417 </li>
416-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
418+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
417419 <a role="presentation" data-href="/diffs/actions">
418-<span class="lbl">actions</span>
420+<span class="lbl">actions (g a)</span>
419421 <span class="k">section</span>
420422 </a>
421423 </li>
@@ -468,7 +470,9 @@
468470 k-filter from:body -> ermite.focusFilter();
469471 k-gh from:body -> ermite.go('.brand');
470472 k-s from:body -> ermite.tab('s');
471- k-t from:body -> ermite.tab('t');
473+ k-f from:body -> ermite.tab('f');
474+ k-b from:body -> ermite.tab('b');
475+ k-a from:body -> ermite.tab('a');
472476 k-c from:body -> ermite.tab('c');
473477 k-r from:body -> ermite.tab('r');
474478 k-m from:body -> ermite.tab('m');
@@ -484,7 +488,6 @@
484488 k-permalink from:body -> ermite.permalink();
485489 k-merge from:body -> window.ermiteMR?.openMerge();
486490 k-read from:body -> window.ermiteMR?.markRead();
487- k-gf from:body -> window.ermiteMR?.focusFiles();
488491 k-keys from:body -> ermite.toggleKeys()">
489492 </div>
490493 </body>
M internal/app/testdata/web/commit-merge.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -313,14 +313,16 @@
313313 </div>
314314 <div>
315315 <dt>
316-<kbd>s</kbd>
317-<kbd>t</kbd>
318-<kbd>c</kbd>
319-<kbd>r</kbd>
320-<kbd>m</kbd>
321-<kbd>i</kbd>
316+<kbd>g s</kbd>
317+<kbd>g f</kbd>
318+<kbd>g c</kbd>
319+<kbd>g b</kbd>
320+<kbd>g m</kbd>
321+<kbd>g i</kbd>
322+<kbd>g r</kbd>
323+<kbd>g a</kbd>
322324 </dt>
323-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
325+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
324326 </div>
325327 <div>
326328 <dt>
@@ -419,45 +421,45 @@
419421 <span class="k">go</span>
420422 </a>
421423 </li>
422-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
424+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
423425 <a role="presentation" data-href="/diffs">
424-<span class="lbl">summary</span>
426+<span class="lbl">summary (g s)</span>
425427 <span class="k">section</span>
426428 </a>
427429 </li>
428-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
430+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
429431 <a role="presentation" data-href="/diffs/tree/main">
430-<span class="lbl">files</span>
432+<span class="lbl">files (g f)</span>
431433 <span class="k">section</span>
432434 </a>
433435 </li>
434-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
436+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
435437 <a role="presentation" data-href="/diffs/commits/main">
436-<span class="lbl">commits</span>
438+<span class="lbl">commits (g c)</span>
437439 <span class="k">section</span>
438440 </a>
439441 </li>
440-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
442+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
441443 <a role="presentation" data-href="/diffs/refs">
442-<span class="lbl">branches &amp; tags</span>
444+<span class="lbl">branches &amp; tags (g b)</span>
443445 <span class="k">section</span>
444446 </a>
445447 </li>
446-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
448+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
447449 <a role="presentation" data-href="/diffs/merge-requests">
448-<span class="lbl">merge requests</span>
450+<span class="lbl">merge requests (g m)</span>
449451 <span class="k">section</span>
450452 </a>
451453 </li>
452-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
454+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
453455 <a role="presentation" data-href="/diffs/releases">
454-<span class="lbl">releases</span>
456+<span class="lbl">releases (g r)</span>
455457 <span class="k">section</span>
456458 </a>
457459 </li>
458-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
460+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
459461 <a role="presentation" data-href="/diffs/actions">
460-<span class="lbl">actions</span>
462+<span class="lbl">actions (g a)</span>
461463 <span class="k">section</span>
462464 </a>
463465 </li>
@@ -510,7 +512,9 @@
510512 k-filter from:body -> ermite.focusFilter();
511513 k-gh from:body -> ermite.go('.brand');
512514 k-s from:body -> ermite.tab('s');
513- k-t from:body -> ermite.tab('t');
515+ k-f from:body -> ermite.tab('f');
516+ k-b from:body -> ermite.tab('b');
517+ k-a from:body -> ermite.tab('a');
514518 k-c from:body -> ermite.tab('c');
515519 k-r from:body -> ermite.tab('r');
516520 k-m from:body -> ermite.tab('m');
@@ -526,7 +530,6 @@
526530 k-permalink from:body -> ermite.permalink();
527531 k-merge from:body -> window.ermiteMR?.openMerge();
528532 k-read from:body -> window.ermiteMR?.markRead();
529- k-gf from:body -> window.ermiteMR?.focusFiles();
530533 k-keys from:body -> ermite.toggleKeys()">
531534 </div>
532535 </body>
M internal/app/testdata/web/commit-modify-split.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -436,14 +436,16 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
436436 </div>
437437 <div>
438438 <dt>
439-<kbd>s</kbd>
440-<kbd>t</kbd>
441-<kbd>c</kbd>
442-<kbd>r</kbd>
443-<kbd>m</kbd>
444-<kbd>i</kbd>
439+<kbd>g s</kbd>
440+<kbd>g f</kbd>
441+<kbd>g c</kbd>
442+<kbd>g b</kbd>
443+<kbd>g m</kbd>
444+<kbd>g i</kbd>
445+<kbd>g r</kbd>
446+<kbd>g a</kbd>
445447 </dt>
446-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
448+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
447449 </div>
448450 <div>
449451 <dt>
@@ -542,45 +544,45 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
542544 <span class="k">go</span>
543545 </a>
544546 </li>
545-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
547+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
546548 <a role="presentation" data-href="/diffs">
547-<span class="lbl">summary</span>
549+<span class="lbl">summary (g s)</span>
548550 <span class="k">section</span>
549551 </a>
550552 </li>
551-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
553+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
552554 <a role="presentation" data-href="/diffs/tree/main">
553-<span class="lbl">files</span>
555+<span class="lbl">files (g f)</span>
554556 <span class="k">section</span>
555557 </a>
556558 </li>
557-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
559+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
558560 <a role="presentation" data-href="/diffs/commits/main">
559-<span class="lbl">commits</span>
561+<span class="lbl">commits (g c)</span>
560562 <span class="k">section</span>
561563 </a>
562564 </li>
563-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
565+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
564566 <a role="presentation" data-href="/diffs/refs">
565-<span class="lbl">branches &amp; tags</span>
567+<span class="lbl">branches &amp; tags (g b)</span>
566568 <span class="k">section</span>
567569 </a>
568570 </li>
569-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
571+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
570572 <a role="presentation" data-href="/diffs/merge-requests">
571-<span class="lbl">merge requests</span>
573+<span class="lbl">merge requests (g m)</span>
572574 <span class="k">section</span>
573575 </a>
574576 </li>
575-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
577+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
576578 <a role="presentation" data-href="/diffs/releases">
577-<span class="lbl">releases</span>
579+<span class="lbl">releases (g r)</span>
578580 <span class="k">section</span>
579581 </a>
580582 </li>
581-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
583+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
582584 <a role="presentation" data-href="/diffs/actions">
583-<span class="lbl">actions</span>
585+<span class="lbl">actions (g a)</span>
584586 <span class="k">section</span>
585587 </a>
586588 </li>
@@ -633,7 +635,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
633635 k-filter from:body -> ermite.focusFilter();
634636 k-gh from:body -> ermite.go('.brand');
635637 k-s from:body -> ermite.tab('s');
636- k-t from:body -> ermite.tab('t');
638+ k-f from:body -> ermite.tab('f');
639+ k-b from:body -> ermite.tab('b');
640+ k-a from:body -> ermite.tab('a');
637641 k-c from:body -> ermite.tab('c');
638642 k-r from:body -> ermite.tab('r');
639643 k-m from:body -> ermite.tab('m');
@@ -649,7 +653,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
649653 k-permalink from:body -> ermite.permalink();
650654 k-merge from:body -> window.ermiteMR?.openMerge();
651655 k-read from:body -> window.ermiteMR?.markRead();
652- k-gf from:body -> window.ermiteMR?.focusFiles();
653656 k-keys from:body -> ermite.toggleKeys()">
654657 </div>
655658 </body>
M internal/app/testdata/web/commit-modify.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -436,14 +436,16 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
436436 </div>
437437 <div>
438438 <dt>
439-<kbd>s</kbd>
440-<kbd>t</kbd>
441-<kbd>c</kbd>
442-<kbd>r</kbd>
443-<kbd>m</kbd>
444-<kbd>i</kbd>
439+<kbd>g s</kbd>
440+<kbd>g f</kbd>
441+<kbd>g c</kbd>
442+<kbd>g b</kbd>
443+<kbd>g m</kbd>
444+<kbd>g i</kbd>
445+<kbd>g r</kbd>
446+<kbd>g a</kbd>
445447 </dt>
446-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
448+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
447449 </div>
448450 <div>
449451 <dt>
@@ -542,45 +544,45 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
542544 <span class="k">go</span>
543545 </a>
544546 </li>
545-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
547+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
546548 <a role="presentation" data-href="/diffs">
547-<span class="lbl">summary</span>
549+<span class="lbl">summary (g s)</span>
548550 <span class="k">section</span>
549551 </a>
550552 </li>
551-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
553+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
552554 <a role="presentation" data-href="/diffs/tree/main">
553-<span class="lbl">files</span>
555+<span class="lbl">files (g f)</span>
554556 <span class="k">section</span>
555557 </a>
556558 </li>
557-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
559+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
558560 <a role="presentation" data-href="/diffs/commits/main">
559-<span class="lbl">commits</span>
561+<span class="lbl">commits (g c)</span>
560562 <span class="k">section</span>
561563 </a>
562564 </li>
563-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
565+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
564566 <a role="presentation" data-href="/diffs/refs">
565-<span class="lbl">branches &amp; tags</span>
567+<span class="lbl">branches &amp; tags (g b)</span>
566568 <span class="k">section</span>
567569 </a>
568570 </li>
569-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
571+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
570572 <a role="presentation" data-href="/diffs/merge-requests">
571-<span class="lbl">merge requests</span>
573+<span class="lbl">merge requests (g m)</span>
572574 <span class="k">section</span>
573575 </a>
574576 </li>
575-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
577+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
576578 <a role="presentation" data-href="/diffs/releases">
577-<span class="lbl">releases</span>
579+<span class="lbl">releases (g r)</span>
578580 <span class="k">section</span>
579581 </a>
580582 </li>
581-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
583+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
582584 <a role="presentation" data-href="/diffs/actions">
583-<span class="lbl">actions</span>
585+<span class="lbl">actions (g a)</span>
584586 <span class="k">section</span>
585587 </a>
586588 </li>
@@ -633,7 +635,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
633635 k-filter from:body -> ermite.focusFilter();
634636 k-gh from:body -> ermite.go('.brand');
635637 k-s from:body -> ermite.tab('s');
636- k-t from:body -> ermite.tab('t');
638+ k-f from:body -> ermite.tab('f');
639+ k-b from:body -> ermite.tab('b');
640+ k-a from:body -> ermite.tab('a');
637641 k-c from:body -> ermite.tab('c');
638642 k-r from:body -> ermite.tab('r');
639643 k-m from:body -> ermite.tab('m');
@@ -649,7 +653,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
649653 k-permalink from:body -> ermite.permalink();
650654 k-merge from:body -> window.ermiteMR?.openMerge();
651655 k-read from:body -> window.ermiteMR?.markRead();
652- k-gf from:body -> window.ermiteMR?.focusFiles();
653656 k-keys from:body -> ermite.toggleKeys()">
654657 </div>
655658 </body>
M internal/app/testdata/web/commit-rename.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -340,14 +340,16 @@
340340 </div>
341341 <div>
342342 <dt>
343-<kbd>s</kbd>
344-<kbd>t</kbd>
345-<kbd>c</kbd>
346-<kbd>r</kbd>
347-<kbd>m</kbd>
348-<kbd>i</kbd>
343+<kbd>g s</kbd>
344+<kbd>g f</kbd>
345+<kbd>g c</kbd>
346+<kbd>g b</kbd>
347+<kbd>g m</kbd>
348+<kbd>g i</kbd>
349+<kbd>g r</kbd>
350+<kbd>g a</kbd>
349351 </dt>
350-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
352+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
351353 </div>
352354 <div>
353355 <dt>
@@ -446,45 +448,45 @@
446448 <span class="k">go</span>
447449 </a>
448450 </li>
449-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
451+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
450452 <a role="presentation" data-href="/diffs">
451-<span class="lbl">summary</span>
453+<span class="lbl">summary (g s)</span>
452454 <span class="k">section</span>
453455 </a>
454456 </li>
455-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
457+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
456458 <a role="presentation" data-href="/diffs/tree/main">
457-<span class="lbl">files</span>
459+<span class="lbl">files (g f)</span>
458460 <span class="k">section</span>
459461 </a>
460462 </li>
461-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
463+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
462464 <a role="presentation" data-href="/diffs/commits/main">
463-<span class="lbl">commits</span>
465+<span class="lbl">commits (g c)</span>
464466 <span class="k">section</span>
465467 </a>
466468 </li>
467-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
469+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
468470 <a role="presentation" data-href="/diffs/refs">
469-<span class="lbl">branches &amp; tags</span>
471+<span class="lbl">branches &amp; tags (g b)</span>
470472 <span class="k">section</span>
471473 </a>
472474 </li>
473-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
475+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
474476 <a role="presentation" data-href="/diffs/merge-requests">
475-<span class="lbl">merge requests</span>
477+<span class="lbl">merge requests (g m)</span>
476478 <span class="k">section</span>
477479 </a>
478480 </li>
479-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
481+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
480482 <a role="presentation" data-href="/diffs/releases">
481-<span class="lbl">releases</span>
483+<span class="lbl">releases (g r)</span>
482484 <span class="k">section</span>
483485 </a>
484486 </li>
485-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
487+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
486488 <a role="presentation" data-href="/diffs/actions">
487-<span class="lbl">actions</span>
489+<span class="lbl">actions (g a)</span>
488490 <span class="k">section</span>
489491 </a>
490492 </li>
@@ -537,7 +539,9 @@
537539 k-filter from:body -> ermite.focusFilter();
538540 k-gh from:body -> ermite.go('.brand');
539541 k-s from:body -> ermite.tab('s');
540- k-t from:body -> ermite.tab('t');
542+ k-f from:body -> ermite.tab('f');
543+ k-b from:body -> ermite.tab('b');
544+ k-a from:body -> ermite.tab('a');
541545 k-c from:body -> ermite.tab('c');
542546 k-r from:body -> ermite.tab('r');
543547 k-m from:body -> ermite.tab('m');
@@ -553,7 +557,6 @@
553557 k-permalink from:body -> ermite.permalink();
554558 k-merge from:body -> window.ermiteMR?.openMerge();
555559 k-read from:body -> window.ermiteMR?.markRead();
556- k-gf from:body -> window.ermiteMR?.focusFiles();
557560 k-keys from:body -> ermite.toggleKeys()">
558561 </div>
559562 </body>
M internal/app/testdata/web/commit-root.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -659,14 +659,16 @@
659659 </div>
660660 <div>
661661 <dt>
662-<kbd>s</kbd>
663-<kbd>t</kbd>
664-<kbd>c</kbd>
665-<kbd>r</kbd>
666-<kbd>m</kbd>
667-<kbd>i</kbd>
662+<kbd>g s</kbd>
663+<kbd>g f</kbd>
664+<kbd>g c</kbd>
665+<kbd>g b</kbd>
666+<kbd>g m</kbd>
667+<kbd>g i</kbd>
668+<kbd>g r</kbd>
669+<kbd>g a</kbd>
668670 </dt>
669-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
671+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
670672 </div>
671673 <div>
672674 <dt>
@@ -765,45 +767,45 @@
765767 <span class="k">go</span>
766768 </a>
767769 </li>
768-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
770+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
769771 <a role="presentation" data-href="/diffs">
770-<span class="lbl">summary</span>
772+<span class="lbl">summary (g s)</span>
771773 <span class="k">section</span>
772774 </a>
773775 </li>
774-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
776+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
775777 <a role="presentation" data-href="/diffs/tree/main">
776-<span class="lbl">files</span>
778+<span class="lbl">files (g f)</span>
777779 <span class="k">section</span>
778780 </a>
779781 </li>
780-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
782+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
781783 <a role="presentation" data-href="/diffs/commits/main">
782-<span class="lbl">commits</span>
784+<span class="lbl">commits (g c)</span>
783785 <span class="k">section</span>
784786 </a>
785787 </li>
786-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
788+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
787789 <a role="presentation" data-href="/diffs/refs">
788-<span class="lbl">branches &amp; tags</span>
790+<span class="lbl">branches &amp; tags (g b)</span>
789791 <span class="k">section</span>
790792 </a>
791793 </li>
792-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
794+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
793795 <a role="presentation" data-href="/diffs/merge-requests">
794-<span class="lbl">merge requests</span>
796+<span class="lbl">merge requests (g m)</span>
795797 <span class="k">section</span>
796798 </a>
797799 </li>
798-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
800+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
799801 <a role="presentation" data-href="/diffs/releases">
800-<span class="lbl">releases</span>
802+<span class="lbl">releases (g r)</span>
801803 <span class="k">section</span>
802804 </a>
803805 </li>
804-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
806+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
805807 <a role="presentation" data-href="/diffs/actions">
806-<span class="lbl">actions</span>
808+<span class="lbl">actions (g a)</span>
807809 <span class="k">section</span>
808810 </a>
809811 </li>
@@ -856,7 +858,9 @@
856858 k-filter from:body -> ermite.focusFilter();
857859 k-gh from:body -> ermite.go('.brand');
858860 k-s from:body -> ermite.tab('s');
859- k-t from:body -> ermite.tab('t');
861+ k-f from:body -> ermite.tab('f');
862+ k-b from:body -> ermite.tab('b');
863+ k-a from:body -> ermite.tab('a');
860864 k-c from:body -> ermite.tab('c');
861865 k-r from:body -> ermite.tab('r');
862866 k-m from:body -> ermite.tab('m');
@@ -872,7 +876,6 @@
872876 k-permalink from:body -> ermite.permalink();
873877 k-merge from:body -> window.ermiteMR?.openMerge();
874878 k-read from:body -> window.ermiteMR?.markRead();
875- k-gf from:body -> window.ermiteMR?.focusFiles();
876879 k-keys from:body -> ermite.toggleKeys()">
877880 </div>
878881 </body>
M internal/app/testdata/web/commit-trojan.html +33 -30
@@ -72,31 +72,31 @@
7272 <nav class="tabs" aria-label="Sections">
7373 <ul>
7474 <li>
75-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
95+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
99+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
100100 </a>
101101 </li>
102102 </ul>
@@ -488,14 +488,16 @@
488488 </div>
489489 <div>
490490 <dt>
491-<kbd>s</kbd>
492-<kbd>t</kbd>
493-<kbd>c</kbd>
494-<kbd>r</kbd>
495-<kbd>m</kbd>
496-<kbd>i</kbd>
491+<kbd>g s</kbd>
492+<kbd>g f</kbd>
493+<kbd>g c</kbd>
494+<kbd>g b</kbd>
495+<kbd>g m</kbd>
496+<kbd>g i</kbd>
497+<kbd>g r</kbd>
498+<kbd>g a</kbd>
497499 </dt>
498-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
500+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
499501 </div>
500502 <div>
501503 <dt>
@@ -594,45 +596,45 @@
594596 <span class="k">go</span>
595597 </a>
596598 </li>
597-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
599+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
598600 <a role="presentation" data-href="/diffs">
599-<span class="lbl">summary</span>
601+<span class="lbl">summary (g s)</span>
600602 <span class="k">section</span>
601603 </a>
602604 </li>
603-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
605+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
604606 <a role="presentation" data-href="/diffs/tree/main">
605-<span class="lbl">files</span>
607+<span class="lbl">files (g f)</span>
606608 <span class="k">section</span>
607609 </a>
608610 </li>
609-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
611+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
610612 <a role="presentation" data-href="/diffs/commits/main">
611-<span class="lbl">commits</span>
613+<span class="lbl">commits (g c)</span>
612614 <span class="k">section</span>
613615 </a>
614616 </li>
615-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
617+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
616618 <a role="presentation" data-href="/diffs/refs">
617-<span class="lbl">branches &amp; tags</span>
619+<span class="lbl">branches &amp; tags (g b)</span>
618620 <span class="k">section</span>
619621 </a>
620622 </li>
621-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
623+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
622624 <a role="presentation" data-href="/diffs/merge-requests">
623-<span class="lbl">merge requests</span>
625+<span class="lbl">merge requests (g m)</span>
624626 <span class="k">section</span>
625627 </a>
626628 </li>
627-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
629+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
628630 <a role="presentation" data-href="/diffs/releases">
629-<span class="lbl">releases</span>
631+<span class="lbl">releases (g r)</span>
630632 <span class="k">section</span>
631633 </a>
632634 </li>
633-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
635+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
634636 <a role="presentation" data-href="/diffs/actions">
635-<span class="lbl">actions</span>
637+<span class="lbl">actions (g a)</span>
636638 <span class="k">section</span>
637639 </a>
638640 </li>
@@ -685,7 +687,9 @@
685687 k-filter from:body -> ermite.focusFilter();
686688 k-gh from:body -> ermite.go('.brand');
687689 k-s from:body -> ermite.tab('s');
688- k-t from:body -> ermite.tab('t');
690+ k-f from:body -> ermite.tab('f');
691+ k-b from:body -> ermite.tab('b');
692+ k-a from:body -> ermite.tab('a');
689693 k-c from:body -> ermite.tab('c');
690694 k-r from:body -> ermite.tab('r');
691695 k-m from:body -> ermite.tab('m');
@@ -701,7 +705,6 @@
701705 k-permalink from:body -> ermite.permalink();
702706 k-merge from:body -> window.ermiteMR?.openMerge();
703707 k-read from:body -> window.ermiteMR?.markRead();
704- k-gf from:body -> window.ermiteMR?.focusFiles();
705708 k-keys from:body -> ermite.toggleKeys()">
706709 </div>
707710 </body>
M internal/app/testdata/web/commits-page2.html +33 -30
@@ -70,31 +70,31 @@
7070 <nav class="tabs" aria-label="Sections">
7171 <ul>
7272 <li>
73-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
73+<a href="/long" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7474 </a>
7575 </li>
7676 <li>
77-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
77+<a href="/long/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
81+<a href="/long/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
85+<a href="/long/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8686 </a>
8787 </li>
8888 <li>
89-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
89+<a href="/long/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9090 </a>
9191 </li>
9292 <li>
93-<a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
93+<a href="/long/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9494 </a>
9595 </li>
9696 <li>
97-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
97+<a href="/long/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
9898 </a>
9999 </li>
100100 </ul>
@@ -255,14 +255,16 @@
255255 </div>
256256 <div>
257257 <dt>
258-<kbd>s</kbd>
259-<kbd>t</kbd>
260-<kbd>c</kbd>
261-<kbd>r</kbd>
262-<kbd>m</kbd>
263-<kbd>i</kbd>
258+<kbd>g s</kbd>
259+<kbd>g f</kbd>
260+<kbd>g c</kbd>
261+<kbd>g b</kbd>
262+<kbd>g m</kbd>
263+<kbd>g i</kbd>
264+<kbd>g r</kbd>
265+<kbd>g a</kbd>
264266 </dt>
265-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
267+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
266268 </div>
267269 <div>
268270 <dt>
@@ -353,45 +355,45 @@
353355 <span class="k">go</span>
354356 </a>
355357 </li>
356-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
358+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
357359 <a role="presentation" data-href="/long">
358-<span class="lbl">summary</span>
360+<span class="lbl">summary (g s)</span>
359361 <span class="k">section</span>
360362 </a>
361363 </li>
362-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
364+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
363365 <a role="presentation" data-href="/long/tree/main">
364-<span class="lbl">files</span>
366+<span class="lbl">files (g f)</span>
365367 <span class="k">section</span>
366368 </a>
367369 </li>
368-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
370+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
369371 <a role="presentation" data-href="/long/commits/main">
370-<span class="lbl">commits</span>
372+<span class="lbl">commits (g c)</span>
371373 <span class="k">section</span>
372374 </a>
373375 </li>
374-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
376+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
375377 <a role="presentation" data-href="/long/refs">
376-<span class="lbl">branches &amp; tags</span>
378+<span class="lbl">branches &amp; tags (g b)</span>
377379 <span class="k">section</span>
378380 </a>
379381 </li>
380-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
382+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
381383 <a role="presentation" data-href="/long/merge-requests">
382-<span class="lbl">merge requests</span>
384+<span class="lbl">merge requests (g m)</span>
383385 <span class="k">section</span>
384386 </a>
385387 </li>
386-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
388+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
387389 <a role="presentation" data-href="/long/releases">
388-<span class="lbl">releases</span>
390+<span class="lbl">releases (g r)</span>
389391 <span class="k">section</span>
390392 </a>
391393 </li>
392-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
394+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
393395 <a role="presentation" data-href="/long/actions">
394-<span class="lbl">actions</span>
396+<span class="lbl">actions (g a)</span>
395397 <span class="k">section</span>
396398 </a>
397399 </li>
@@ -438,7 +440,9 @@
438440 k-filter from:body -> ermite.focusFilter();
439441 k-gh from:body -> ermite.go('.brand');
440442 k-s from:body -> ermite.tab('s');
441- k-t from:body -> ermite.tab('t');
443+ k-f from:body -> ermite.tab('f');
444+ k-b from:body -> ermite.tab('b');
445+ k-a from:body -> ermite.tab('a');
442446 k-c from:body -> ermite.tab('c');
443447 k-r from:body -> ermite.tab('r');
444448 k-m from:body -> ermite.tab('m');
@@ -454,7 +458,6 @@
454458 k-permalink from:body -> ermite.permalink();
455459 k-merge from:body -> window.ermiteMR?.openMerge();
456460 k-read from:body -> window.ermiteMR?.markRead();
457- k-gf from:body -> window.ermiteMR?.focusFiles();
458461 k-keys from:body -> ermite.toggleKeys()">
459462 </div>
460463 </body>
M internal/app/testdata/web/commits-path.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/long" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/long/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/long/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/long/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/long/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/long/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/long/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -270,14 +270,16 @@
270270 </div>
271271 <div>
272272 <dt>
273-<kbd>s</kbd>
274-<kbd>t</kbd>
275-<kbd>c</kbd>
276-<kbd>r</kbd>
277-<kbd>m</kbd>
278-<kbd>i</kbd>
273+<kbd>g s</kbd>
274+<kbd>g f</kbd>
275+<kbd>g c</kbd>
276+<kbd>g b</kbd>
277+<kbd>g m</kbd>
278+<kbd>g i</kbd>
279+<kbd>g r</kbd>
280+<kbd>g a</kbd>
279281 </dt>
280-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
282+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
281283 </div>
282284 <div>
283285 <dt>
@@ -368,45 +370,45 @@
368370 <span class="k">go</span>
369371 </a>
370372 </li>
371-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
373+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
372374 <a role="presentation" data-href="/long">
373-<span class="lbl">summary</span>
375+<span class="lbl">summary (g s)</span>
374376 <span class="k">section</span>
375377 </a>
376378 </li>
377-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
379+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
378380 <a role="presentation" data-href="/long/tree/main">
379-<span class="lbl">files</span>
381+<span class="lbl">files (g f)</span>
380382 <span class="k">section</span>
381383 </a>
382384 </li>
383-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
385+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
384386 <a role="presentation" data-href="/long/commits/main">
385-<span class="lbl">commits</span>
387+<span class="lbl">commits (g c)</span>
386388 <span class="k">section</span>
387389 </a>
388390 </li>
389-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
391+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
390392 <a role="presentation" data-href="/long/refs">
391-<span class="lbl">branches &amp; tags</span>
393+<span class="lbl">branches &amp; tags (g b)</span>
392394 <span class="k">section</span>
393395 </a>
394396 </li>
395-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
397+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
396398 <a role="presentation" data-href="/long/merge-requests">
397-<span class="lbl">merge requests</span>
399+<span class="lbl">merge requests (g m)</span>
398400 <span class="k">section</span>
399401 </a>
400402 </li>
401-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
403+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
402404 <a role="presentation" data-href="/long/releases">
403-<span class="lbl">releases</span>
405+<span class="lbl">releases (g r)</span>
404406 <span class="k">section</span>
405407 </a>
406408 </li>
407-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
409+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
408410 <a role="presentation" data-href="/long/actions">
409-<span class="lbl">actions</span>
411+<span class="lbl">actions (g a)</span>
410412 <span class="k">section</span>
411413 </a>
412414 </li>
@@ -453,7 +455,9 @@
453455 k-filter from:body -> ermite.focusFilter();
454456 k-gh from:body -> ermite.go('.brand');
455457 k-s from:body -> ermite.tab('s');
456- k-t from:body -> ermite.tab('t');
458+ k-f from:body -> ermite.tab('f');
459+ k-b from:body -> ermite.tab('b');
460+ k-a from:body -> ermite.tab('a');
457461 k-c from:body -> ermite.tab('c');
458462 k-r from:body -> ermite.tab('r');
459463 k-m from:body -> ermite.tab('m');
@@ -469,7 +473,6 @@
469473 k-permalink from:body -> ermite.permalink();
470474 k-merge from:body -> window.ermiteMR?.openMerge();
471475 k-read from:body -> window.ermiteMR?.markRead();
472- k-gf from:body -> window.ermiteMR?.focusFiles();
473476 k-keys from:body -> ermite.toggleKeys()">
474477 </div>
475478 </body>
M internal/app/testdata/web/commits-root.html +33 -30
@@ -70,31 +70,31 @@
7070 <nav class="tabs" aria-label="Sections">
7171 <ul>
7272 <li>
73-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
73+<a href="/long" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7474 </a>
7575 </li>
7676 <li>
77-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
77+<a href="/long/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
81+<a href="/long/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
85+<a href="/long/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8686 </a>
8787 </li>
8888 <li>
89-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
89+<a href="/long/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9090 </a>
9191 </li>
9292 <li>
93-<a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
93+<a href="/long/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9494 </a>
9595 </li>
9696 <li>
97-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
97+<a href="/long/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
9898 </a>
9999 </li>
100100 </ul>
@@ -1060,14 +1060,16 @@
10601060 </div>
10611061 <div>
10621062 <dt>
1063-<kbd>s</kbd>
1064-<kbd>t</kbd>
1065-<kbd>c</kbd>
1066-<kbd>r</kbd>
1067-<kbd>m</kbd>
1068-<kbd>i</kbd>
1063+<kbd>g s</kbd>
1064+<kbd>g f</kbd>
1065+<kbd>g c</kbd>
1066+<kbd>g b</kbd>
1067+<kbd>g m</kbd>
1068+<kbd>g i</kbd>
1069+<kbd>g r</kbd>
1070+<kbd>g a</kbd>
10691071 </dt>
1070-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
1072+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
10711073 </div>
10721074 <div>
10731075 <dt>
@@ -1158,45 +1160,45 @@
11581160 <span class="k">go</span>
11591161 </a>
11601162 </li>
1161-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
1163+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
11621164 <a role="presentation" data-href="/long">
1163-<span class="lbl">summary</span>
1165+<span class="lbl">summary (g s)</span>
11641166 <span class="k">section</span>
11651167 </a>
11661168 </li>
1167-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
1169+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
11681170 <a role="presentation" data-href="/long/tree/main">
1169-<span class="lbl">files</span>
1171+<span class="lbl">files (g f)</span>
11701172 <span class="k">section</span>
11711173 </a>
11721174 </li>
1173-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
1175+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
11741176 <a role="presentation" data-href="/long/commits/main">
1175-<span class="lbl">commits</span>
1177+<span class="lbl">commits (g c)</span>
11761178 <span class="k">section</span>
11771179 </a>
11781180 </li>
1179-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
1181+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
11801182 <a role="presentation" data-href="/long/refs">
1181-<span class="lbl">branches &amp; tags</span>
1183+<span class="lbl">branches &amp; tags (g b)</span>
11821184 <span class="k">section</span>
11831185 </a>
11841186 </li>
1185-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
1187+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
11861188 <a role="presentation" data-href="/long/merge-requests">
1187-<span class="lbl">merge requests</span>
1189+<span class="lbl">merge requests (g m)</span>
11881190 <span class="k">section</span>
11891191 </a>
11901192 </li>
1191-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
1193+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
11921194 <a role="presentation" data-href="/long/releases">
1193-<span class="lbl">releases</span>
1195+<span class="lbl">releases (g r)</span>
11941196 <span class="k">section</span>
11951197 </a>
11961198 </li>
1197-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
1199+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
11981200 <a role="presentation" data-href="/long/actions">
1199-<span class="lbl">actions</span>
1201+<span class="lbl">actions (g a)</span>
12001202 <span class="k">section</span>
12011203 </a>
12021204 </li>
@@ -1243,7 +1245,9 @@
12431245 k-filter from:body -> ermite.focusFilter();
12441246 k-gh from:body -> ermite.go('.brand');
12451247 k-s from:body -> ermite.tab('s');
1246- k-t from:body -> ermite.tab('t');
1248+ k-f from:body -> ermite.tab('f');
1249+ k-b from:body -> ermite.tab('b');
1250+ k-a from:body -> ermite.tab('a');
12471251 k-c from:body -> ermite.tab('c');
12481252 k-r from:body -> ermite.tab('r');
12491253 k-m from:body -> ermite.tab('m');
@@ -1259,7 +1263,6 @@
12591263 k-permalink from:body -> ermite.permalink();
12601264 k-merge from:body -> window.ermiteMR?.openMerge();
12611265 k-read from:body -> window.ermiteMR?.markRead();
1262- k-gf from:body -> window.ermiteMR?.focusFiles();
12631266 k-keys from:body -> ermite.toggleKeys()">
12641267 </div>
12651268 </body>
M internal/app/testdata/web/commits-small.html +33 -30
@@ -70,31 +70,31 @@
7070 <nav class="tabs" aria-label="Sections">
7171 <ul>
7272 <li>
73-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
73+<a href="/diffs" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7474 </a>
7575 </li>
7676 <li>
77-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
77+<a href="/diffs/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
81+<a href="/diffs/commits/main" data-key="g c" aria-current="page">Commits <kbd aria-hidden="true">g c</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
85+<a href="/diffs/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8686 </a>
8787 </li>
8888 <li>
89-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
89+<a href="/diffs/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9090 </a>
9191 </li>
9292 <li>
93-<a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
93+<a href="/diffs/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9494 </a>
9595 </li>
9696 <li>
97-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
97+<a href="/diffs/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
9898 </a>
9999 </li>
100100 </ul>
@@ -405,14 +405,16 @@
405405 </div>
406406 <div>
407407 <dt>
408-<kbd>s</kbd>
409-<kbd>t</kbd>
410-<kbd>c</kbd>
411-<kbd>r</kbd>
412-<kbd>m</kbd>
413-<kbd>i</kbd>
408+<kbd>g s</kbd>
409+<kbd>g f</kbd>
410+<kbd>g c</kbd>
411+<kbd>g b</kbd>
412+<kbd>g m</kbd>
413+<kbd>g i</kbd>
414+<kbd>g r</kbd>
415+<kbd>g a</kbd>
414416 </dt>
415-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
417+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
416418 </div>
417419 <div>
418420 <dt>
@@ -511,45 +513,45 @@
511513 <span class="k">go</span>
512514 </a>
513515 </li>
514-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
516+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
515517 <a role="presentation" data-href="/diffs">
516-<span class="lbl">summary</span>
518+<span class="lbl">summary (g s)</span>
517519 <span class="k">section</span>
518520 </a>
519521 </li>
520-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
522+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
521523 <a role="presentation" data-href="/diffs/tree/main">
522-<span class="lbl">files</span>
524+<span class="lbl">files (g f)</span>
523525 <span class="k">section</span>
524526 </a>
525527 </li>
526-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
528+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
527529 <a role="presentation" data-href="/diffs/commits/main">
528-<span class="lbl">commits</span>
530+<span class="lbl">commits (g c)</span>
529531 <span class="k">section</span>
530532 </a>
531533 </li>
532-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
534+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
533535 <a role="presentation" data-href="/diffs/refs">
534-<span class="lbl">branches &amp; tags</span>
536+<span class="lbl">branches &amp; tags (g b)</span>
535537 <span class="k">section</span>
536538 </a>
537539 </li>
538-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
540+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
539541 <a role="presentation" data-href="/diffs/merge-requests">
540-<span class="lbl">merge requests</span>
542+<span class="lbl">merge requests (g m)</span>
541543 <span class="k">section</span>
542544 </a>
543545 </li>
544-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
546+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
545547 <a role="presentation" data-href="/diffs/releases">
546-<span class="lbl">releases</span>
548+<span class="lbl">releases (g r)</span>
547549 <span class="k">section</span>
548550 </a>
549551 </li>
550-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
552+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
551553 <a role="presentation" data-href="/diffs/actions">
552-<span class="lbl">actions</span>
554+<span class="lbl">actions (g a)</span>
553555 <span class="k">section</span>
554556 </a>
555557 </li>
@@ -608,7 +610,9 @@
608610 k-filter from:body -> ermite.focusFilter();
609611 k-gh from:body -> ermite.go('.brand');
610612 k-s from:body -> ermite.tab('s');
611- k-t from:body -> ermite.tab('t');
613+ k-f from:body -> ermite.tab('f');
614+ k-b from:body -> ermite.tab('b');
615+ k-a from:body -> ermite.tab('a');
612616 k-c from:body -> ermite.tab('c');
613617 k-r from:body -> ermite.tab('r');
614618 k-m from:body -> ermite.tab('m');
@@ -624,7 +628,6 @@
624628 k-permalink from:body -> ermite.permalink();
625629 k-merge from:body -> window.ermiteMR?.openMerge();
626630 k-read from:body -> window.ermiteMR?.markRead();
627- k-gf from:body -> window.ermiteMR?.focusFiles();
628631 k-keys from:body -> ermite.toggleKeys()">
629632 </div>
630633 </body>
M internal/app/testdata/web/empty.html +12 -9
@@ -173,14 +173,16 @@
173173 </div>
174174 <div>
175175 <dt>
176-<kbd>s</kbd>
177-<kbd>t</kbd>
178-<kbd>c</kbd>
179-<kbd>r</kbd>
180-<kbd>m</kbd>
181-<kbd>i</kbd>
176+<kbd>g s</kbd>
177+<kbd>g f</kbd>
178+<kbd>g c</kbd>
179+<kbd>g b</kbd>
180+<kbd>g m</kbd>
181+<kbd>g i</kbd>
182+<kbd>g r</kbd>
183+<kbd>g a</kbd>
182184 </dt>
183-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
185+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
184186 </div>
185187 <div>
186188 <dt>
@@ -281,7 +283,9 @@
281283 k-filter from:body -> ermite.focusFilter();
282284 k-gh from:body -> ermite.go('.brand');
283285 k-s from:body -> ermite.tab('s');
284- k-t from:body -> ermite.tab('t');
286+ k-f from:body -> ermite.tab('f');
287+ k-b from:body -> ermite.tab('b');
288+ k-a from:body -> ermite.tab('a');
285289 k-c from:body -> ermite.tab('c');
286290 k-r from:body -> ermite.tab('r');
287291 k-m from:body -> ermite.tab('m');
@@ -297,7 +301,6 @@
297301 k-permalink from:body -> ermite.permalink();
298302 k-merge from:body -> window.ermiteMR?.openMerge();
299303 k-read from:body -> window.ermiteMR?.markRead();
300- k-gf from:body -> window.ermiteMR?.focusFiles();
301304 k-keys from:body -> ermite.toggleKeys()">
302305 </div>
303306 </body>
M internal/app/testdata/web/home.html +12 -9
@@ -277,14 +277,16 @@
277277 </div>
278278 <div>
279279 <dt>
280-<kbd>s</kbd>
281-<kbd>t</kbd>
282-<kbd>c</kbd>
283-<kbd>r</kbd>
284-<kbd>m</kbd>
285-<kbd>i</kbd>
280+<kbd>g s</kbd>
281+<kbd>g f</kbd>
282+<kbd>g c</kbd>
283+<kbd>g b</kbd>
284+<kbd>g m</kbd>
285+<kbd>g i</kbd>
286+<kbd>g r</kbd>
287+<kbd>g a</kbd>
286288 </dt>
287-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
289+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
288290 </div>
289291 <div>
290292 <dt>
@@ -391,7 +393,9 @@
391393 k-filter from:body -> ermite.focusFilter();
392394 k-gh from:body -> ermite.go('.brand');
393395 k-s from:body -> ermite.tab('s');
394- k-t from:body -> ermite.tab('t');
396+ k-f from:body -> ermite.tab('f');
397+ k-b from:body -> ermite.tab('b');
398+ k-a from:body -> ermite.tab('a');
395399 k-c from:body -> ermite.tab('c');
396400 k-r from:body -> ermite.tab('r');
397401 k-m from:body -> ermite.tab('m');
@@ -407,7 +411,6 @@
407411 k-permalink from:body -> ermite.permalink();
408412 k-merge from:body -> window.ermiteMR?.openMerge();
409413 k-read from:body -> window.ermiteMR?.markRead();
410- k-gf from:body -> window.ermiteMR?.focusFiles();
411414 k-keys from:body -> ermite.toggleKeys()">
412415 </div>
413416 </body>
M internal/app/testdata/web/not-found-page.html +12 -9
@@ -162,14 +162,16 @@
162162 </div>
163163 <div>
164164 <dt>
165-<kbd>s</kbd>
166-<kbd>t</kbd>
167-<kbd>c</kbd>
168-<kbd>r</kbd>
169-<kbd>m</kbd>
170-<kbd>i</kbd>
165+<kbd>g s</kbd>
166+<kbd>g f</kbd>
167+<kbd>g c</kbd>
168+<kbd>g b</kbd>
169+<kbd>g m</kbd>
170+<kbd>g i</kbd>
171+<kbd>g r</kbd>
172+<kbd>g a</kbd>
171173 </dt>
172-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
174+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
173175 </div>
174176 <div>
175177 <dt>
@@ -264,7 +266,9 @@
264266 k-filter from:body -> ermite.focusFilter();
265267 k-gh from:body -> ermite.go('.brand');
266268 k-s from:body -> ermite.tab('s');
267- k-t from:body -> ermite.tab('t');
269+ k-f from:body -> ermite.tab('f');
270+ k-b from:body -> ermite.tab('b');
271+ k-a from:body -> ermite.tab('a');
268272 k-c from:body -> ermite.tab('c');
269273 k-r from:body -> ermite.tab('r');
270274 k-m from:body -> ermite.tab('m');
@@ -280,7 +284,6 @@
280284 k-permalink from:body -> ermite.permalink();
281285 k-merge from:body -> window.ermiteMR?.openMerge();
282286 k-read from:body -> window.ermiteMR?.markRead();
283- k-gf from:body -> window.ermiteMR?.focusFiles();
284287 k-keys from:body -> ermite.toggleKeys()">
285288 </div>
286289 </body>
M internal/app/testdata/web/not-found.html +12 -9
@@ -162,14 +162,16 @@
162162 </div>
163163 <div>
164164 <dt>
165-<kbd>s</kbd>
166-<kbd>t</kbd>
167-<kbd>c</kbd>
168-<kbd>r</kbd>
169-<kbd>m</kbd>
170-<kbd>i</kbd>
165+<kbd>g s</kbd>
166+<kbd>g f</kbd>
167+<kbd>g c</kbd>
168+<kbd>g b</kbd>
169+<kbd>g m</kbd>
170+<kbd>g i</kbd>
171+<kbd>g r</kbd>
172+<kbd>g a</kbd>
171173 </dt>
172-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
174+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
173175 </div>
174176 <div>
175177 <dt>
@@ -264,7 +266,9 @@
264266 k-filter from:body -> ermite.focusFilter();
265267 k-gh from:body -> ermite.go('.brand');
266268 k-s from:body -> ermite.tab('s');
267- k-t from:body -> ermite.tab('t');
269+ k-f from:body -> ermite.tab('f');
270+ k-b from:body -> ermite.tab('b');
271+ k-a from:body -> ermite.tab('a');
268272 k-c from:body -> ermite.tab('c');
269273 k-r from:body -> ermite.tab('r');
270274 k-m from:body -> ermite.tab('m');
@@ -280,7 +284,6 @@
280284 k-permalink from:body -> ermite.permalink();
281285 k-merge from:body -> window.ermiteMR?.openMerge();
282286 k-read from:body -> window.ermiteMR?.markRead();
283- k-gf from:body -> window.ermiteMR?.focusFiles();
284287 k-keys from:body -> ermite.toggleKeys()">
285288 </div>
286289 </body>
M internal/app/testdata/web/refs.html +33 -30
@@ -67,31 +67,31 @@
6767 <nav class="tabs" aria-label="Sections">
6868 <ul>
6969 <li>
70-<a href="/demo" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
70+<a href="/demo" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7171 </a>
7272 </li>
7373 <li>
74-<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
74+<a href="/demo/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
78+<a href="/demo/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/demo/refs" data-key="r" aria-keyshortcuts="r" aria-current="page">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
82+<a href="/demo/refs" data-key="g b" aria-current="page">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
86+<a href="/demo/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/demo/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
90+<a href="/demo/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
94+<a href="/demo/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -287,14 +287,16 @@
287287 </div>
288288 <div>
289289 <dt>
290-<kbd>s</kbd>
291-<kbd>t</kbd>
292-<kbd>c</kbd>
293-<kbd>r</kbd>
294-<kbd>m</kbd>
295-<kbd>i</kbd>
290+<kbd>g s</kbd>
291+<kbd>g f</kbd>
292+<kbd>g c</kbd>
293+<kbd>g b</kbd>
294+<kbd>g m</kbd>
295+<kbd>g i</kbd>
296+<kbd>g r</kbd>
297+<kbd>g a</kbd>
296298 </dt>
297-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
299+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
298300 </div>
299301 <div>
300302 <dt>
@@ -397,45 +399,45 @@
397399 <span class="k">go</span>
398400 </a>
399401 </li>
400-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
402+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
401403 <a role="presentation" data-href="/demo">
402-<span class="lbl">summary</span>
404+<span class="lbl">summary (g s)</span>
403405 <span class="k">section</span>
404406 </a>
405407 </li>
406-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
408+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
407409 <a role="presentation" data-href="/demo/tree/main">
408-<span class="lbl">files</span>
410+<span class="lbl">files (g f)</span>
409411 <span class="k">section</span>
410412 </a>
411413 </li>
412-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
414+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
413415 <a role="presentation" data-href="/demo/commits/main">
414-<span class="lbl">commits</span>
416+<span class="lbl">commits (g c)</span>
415417 <span class="k">section</span>
416418 </a>
417419 </li>
418-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
420+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
419421 <a role="presentation" data-href="/demo/refs">
420-<span class="lbl">branches &amp; tags</span>
422+<span class="lbl">branches &amp; tags (g b)</span>
421423 <span class="k">section</span>
422424 </a>
423425 </li>
424-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
426+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
425427 <a role="presentation" data-href="/demo/merge-requests">
426-<span class="lbl">merge requests</span>
428+<span class="lbl">merge requests (g m)</span>
427429 <span class="k">section</span>
428430 </a>
429431 </li>
430-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
432+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
431433 <a role="presentation" data-href="/demo/releases">
432-<span class="lbl">releases</span>
434+<span class="lbl">releases (g r)</span>
433435 <span class="k">section</span>
434436 </a>
435437 </li>
436-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
438+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
437439 <a role="presentation" data-href="/demo/actions">
438-<span class="lbl">actions</span>
440+<span class="lbl">actions (g a)</span>
439441 <span class="k">section</span>
440442 </a>
441443 </li>
@@ -500,7 +502,9 @@
500502 k-filter from:body -> ermite.focusFilter();
501503 k-gh from:body -> ermite.go('.brand');
502504 k-s from:body -> ermite.tab('s');
503- k-t from:body -> ermite.tab('t');
505+ k-f from:body -> ermite.tab('f');
506+ k-b from:body -> ermite.tab('b');
507+ k-a from:body -> ermite.tab('a');
504508 k-c from:body -> ermite.tab('c');
505509 k-r from:body -> ermite.tab('r');
506510 k-m from:body -> ermite.tab('m');
@@ -516,7 +520,6 @@
516520 k-permalink from:body -> ermite.permalink();
517521 k-merge from:body -> window.ermiteMR?.openMerge();
518522 k-read from:body -> window.ermiteMR?.markRead();
519- k-gf from:body -> window.ermiteMR?.focusFiles();
520523 k-keys from:body -> ermite.toggleKeys()">
521524 </div>
522525 </body>
M internal/app/testdata/web/summary.html +33 -30
@@ -64,31 +64,31 @@
6464 <nav class="tabs" aria-label="Sections">
6565 <ul>
6666 <li>
67-<a href="/demo" data-key="s" aria-keyshortcuts="s" aria-current="page">Summary <kbd aria-hidden="true">s</kbd>
67+<a href="/demo" data-key="g s" aria-current="page">Summary <kbd aria-hidden="true">g s</kbd>
6868 </a>
6969 </li>
7070 <li>
71-<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
71+<a href="/demo/tree/main" data-key="g f">Files <kbd aria-hidden="true">g f</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
75+<a href="/demo/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/demo/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
79+<a href="/demo/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
83+<a href="/demo/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/demo/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
87+<a href="/demo/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
91+<a href="/demo/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -313,14 +313,16 @@
313313 </div>
314314 <div>
315315 <dt>
316-<kbd>s</kbd>
317-<kbd>t</kbd>
318-<kbd>c</kbd>
319-<kbd>r</kbd>
320-<kbd>m</kbd>
321-<kbd>i</kbd>
316+<kbd>g s</kbd>
317+<kbd>g f</kbd>
318+<kbd>g c</kbd>
319+<kbd>g b</kbd>
320+<kbd>g m</kbd>
321+<kbd>g i</kbd>
322+<kbd>g r</kbd>
323+<kbd>g a</kbd>
322324 </dt>
323-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
325+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
324326 </div>
325327 <div>
326328 <dt>
@@ -423,45 +425,45 @@
423425 <span class="k">go</span>
424426 </a>
425427 </li>
426-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
428+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
427429 <a role="presentation" data-href="/demo">
428-<span class="lbl">summary</span>
430+<span class="lbl">summary (g s)</span>
429431 <span class="k">section</span>
430432 </a>
431433 </li>
432-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
434+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
433435 <a role="presentation" data-href="/demo/tree/main">
434-<span class="lbl">files</span>
436+<span class="lbl">files (g f)</span>
435437 <span class="k">section</span>
436438 </a>
437439 </li>
438-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
440+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
439441 <a role="presentation" data-href="/demo/commits/main">
440-<span class="lbl">commits</span>
442+<span class="lbl">commits (g c)</span>
441443 <span class="k">section</span>
442444 </a>
443445 </li>
444-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
446+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
445447 <a role="presentation" data-href="/demo/refs">
446-<span class="lbl">branches &amp; tags</span>
448+<span class="lbl">branches &amp; tags (g b)</span>
447449 <span class="k">section</span>
448450 </a>
449451 </li>
450-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
452+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
451453 <a role="presentation" data-href="/demo/merge-requests">
452-<span class="lbl">merge requests</span>
454+<span class="lbl">merge requests (g m)</span>
453455 <span class="k">section</span>
454456 </a>
455457 </li>
456-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
458+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
457459 <a role="presentation" data-href="/demo/releases">
458-<span class="lbl">releases</span>
460+<span class="lbl">releases (g r)</span>
459461 <span class="k">section</span>
460462 </a>
461463 </li>
462-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
464+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
463465 <a role="presentation" data-href="/demo/actions">
464-<span class="lbl">actions</span>
466+<span class="lbl">actions (g a)</span>
465467 <span class="k">section</span>
466468 </a>
467469 </li>
@@ -526,7 +528,9 @@
526528 k-filter from:body -> ermite.focusFilter();
527529 k-gh from:body -> ermite.go('.brand');
528530 k-s from:body -> ermite.tab('s');
529- k-t from:body -> ermite.tab('t');
531+ k-f from:body -> ermite.tab('f');
532+ k-b from:body -> ermite.tab('b');
533+ k-a from:body -> ermite.tab('a');
530534 k-c from:body -> ermite.tab('c');
531535 k-r from:body -> ermite.tab('r');
532536 k-m from:body -> ermite.tab('m');
@@ -542,7 +546,6 @@
542546 k-permalink from:body -> ermite.permalink();
543547 k-merge from:body -> window.ermiteMR?.openMerge();
544548 k-read from:body -> window.ermiteMR?.markRead();
545- k-gf from:body -> window.ermiteMR?.focusFiles();
546549 k-keys from:body -> ermite.toggleKeys()">
547550 </div>
548551 </body>
M internal/app/testdata/web/tree-nested.html +33 -30
@@ -76,31 +76,31 @@
7676 <nav class="tabs" aria-label="Sections">
7777 <ul>
7878 <li>
79-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
79+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
83+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
87+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
91+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
9292 </a>
9393 </li>
9494 <li>
95-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
95+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9696 </a>
9797 </li>
9898 <li>
99-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
99+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
100100 </a>
101101 </li>
102102 <li>
103-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
103+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
104104 </a>
105105 </li>
106106 </ul>
@@ -243,14 +243,16 @@
243243 </div>
244244 <div>
245245 <dt>
246-<kbd>s</kbd>
247-<kbd>t</kbd>
248-<kbd>c</kbd>
249-<kbd>r</kbd>
250-<kbd>m</kbd>
251-<kbd>i</kbd>
246+<kbd>g s</kbd>
247+<kbd>g f</kbd>
248+<kbd>g c</kbd>
249+<kbd>g b</kbd>
250+<kbd>g m</kbd>
251+<kbd>g i</kbd>
252+<kbd>g r</kbd>
253+<kbd>g a</kbd>
252254 </dt>
253-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
255+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
254256 </div>
255257 <div>
256258 <dt>
@@ -349,45 +351,45 @@
349351 <span class="k">go</span>
350352 </a>
351353 </li>
352-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
354+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
353355 <a role="presentation" data-href="/files">
354-<span class="lbl">summary</span>
356+<span class="lbl">summary (g s)</span>
355357 <span class="k">section</span>
356358 </a>
357359 </li>
358-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
360+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
359361 <a role="presentation" data-href="/files/tree/main">
360-<span class="lbl">files</span>
362+<span class="lbl">files (g f)</span>
361363 <span class="k">section</span>
362364 </a>
363365 </li>
364-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
366+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
365367 <a role="presentation" data-href="/files/commits/main">
366-<span class="lbl">commits</span>
368+<span class="lbl">commits (g c)</span>
367369 <span class="k">section</span>
368370 </a>
369371 </li>
370-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
372+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
371373 <a role="presentation" data-href="/files/refs">
372-<span class="lbl">branches &amp; tags</span>
374+<span class="lbl">branches &amp; tags (g b)</span>
373375 <span class="k">section</span>
374376 </a>
375377 </li>
376-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
378+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
377379 <a role="presentation" data-href="/files/merge-requests">
378-<span class="lbl">merge requests</span>
380+<span class="lbl">merge requests (g m)</span>
379381 <span class="k">section</span>
380382 </a>
381383 </li>
382-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
384+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
383385 <a role="presentation" data-href="/files/releases">
384-<span class="lbl">releases</span>
386+<span class="lbl">releases (g r)</span>
385387 <span class="k">section</span>
386388 </a>
387389 </li>
388-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
390+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
389391 <a role="presentation" data-href="/files/actions">
390-<span class="lbl">actions</span>
392+<span class="lbl">actions (g a)</span>
391393 <span class="k">section</span>
392394 </a>
393395 </li>
@@ -440,7 +442,9 @@
440442 k-filter from:body -> ermite.focusFilter();
441443 k-gh from:body -> ermite.go('.brand');
442444 k-s from:body -> ermite.tab('s');
443- k-t from:body -> ermite.tab('t');
445+ k-f from:body -> ermite.tab('f');
446+ k-b from:body -> ermite.tab('b');
447+ k-a from:body -> ermite.tab('a');
444448 k-c from:body -> ermite.tab('c');
445449 k-r from:body -> ermite.tab('r');
446450 k-m from:body -> ermite.tab('m');
@@ -456,7 +460,6 @@
456460 k-permalink from:body -> ermite.permalink();
457461 k-merge from:body -> window.ermiteMR?.openMerge();
458462 k-read from:body -> window.ermiteMR?.markRead();
459- k-gf from:body -> window.ermiteMR?.focusFiles();
460463 k-keys from:body -> ermite.toggleKeys()">
461464 </div>
462465 </body>
M internal/app/testdata/web/tree-root.html +33 -30
@@ -70,31 +70,31 @@
7070 <nav class="tabs" aria-label="Sections">
7171 <ul>
7272 <li>
73-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
73+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7474 </a>
7575 </li>
7676 <li>
77-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
77+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
81+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
85+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8686 </a>
8787 </li>
8888 <li>
89-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
89+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9090 </a>
9191 </li>
9292 <li>
93-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
93+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9494 </a>
9595 </li>
9696 <li>
97-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
97+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
9898 </a>
9999 </li>
100100 </ul>
@@ -464,14 +464,16 @@
464464 </div>
465465 <div>
466466 <dt>
467-<kbd>s</kbd>
468-<kbd>t</kbd>
469-<kbd>c</kbd>
470-<kbd>r</kbd>
471-<kbd>m</kbd>
472-<kbd>i</kbd>
467+<kbd>g s</kbd>
468+<kbd>g f</kbd>
469+<kbd>g c</kbd>
470+<kbd>g b</kbd>
471+<kbd>g m</kbd>
472+<kbd>g i</kbd>
473+<kbd>g r</kbd>
474+<kbd>g a</kbd>
473475 </dt>
474-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
476+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
475477 </div>
476478 <div>
477479 <dt>
@@ -570,45 +572,45 @@
570572 <span class="k">go</span>
571573 </a>
572574 </li>
573-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
575+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
574576 <a role="presentation" data-href="/files">
575-<span class="lbl">summary</span>
577+<span class="lbl">summary (g s)</span>
576578 <span class="k">section</span>
577579 </a>
578580 </li>
579-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
581+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
580582 <a role="presentation" data-href="/files/tree/main">
581-<span class="lbl">files</span>
583+<span class="lbl">files (g f)</span>
582584 <span class="k">section</span>
583585 </a>
584586 </li>
585-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
587+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
586588 <a role="presentation" data-href="/files/commits/main">
587-<span class="lbl">commits</span>
589+<span class="lbl">commits (g c)</span>
588590 <span class="k">section</span>
589591 </a>
590592 </li>
591-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
593+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
592594 <a role="presentation" data-href="/files/refs">
593-<span class="lbl">branches &amp; tags</span>
595+<span class="lbl">branches &amp; tags (g b)</span>
594596 <span class="k">section</span>
595597 </a>
596598 </li>
597-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
599+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
598600 <a role="presentation" data-href="/files/merge-requests">
599-<span class="lbl">merge requests</span>
601+<span class="lbl">merge requests (g m)</span>
600602 <span class="k">section</span>
601603 </a>
602604 </li>
603-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
605+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
604606 <a role="presentation" data-href="/files/releases">
605-<span class="lbl">releases</span>
607+<span class="lbl">releases (g r)</span>
606608 <span class="k">section</span>
607609 </a>
608610 </li>
609-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
611+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
610612 <a role="presentation" data-href="/files/actions">
611-<span class="lbl">actions</span>
613+<span class="lbl">actions (g a)</span>
612614 <span class="k">section</span>
613615 </a>
614616 </li>
@@ -661,7 +663,9 @@
661663 k-filter from:body -> ermite.focusFilter();
662664 k-gh from:body -> ermite.go('.brand');
663665 k-s from:body -> ermite.tab('s');
664- k-t from:body -> ermite.tab('t');
666+ k-f from:body -> ermite.tab('f');
667+ k-b from:body -> ermite.tab('b');
668+ k-a from:body -> ermite.tab('a');
665669 k-c from:body -> ermite.tab('c');
666670 k-r from:body -> ermite.tab('r');
667671 k-m from:body -> ermite.tab('m');
@@ -677,7 +681,6 @@
677681 k-permalink from:body -> ermite.permalink();
678682 k-merge from:body -> window.ermiteMR?.openMerge();
679683 k-read from:body -> window.ermiteMR?.markRead();
680- k-gf from:body -> window.ermiteMR?.focusFiles();
681684 k-keys from:body -> ermite.toggleKeys()">
682685 </div>
683686 </body>
M internal/app/testdata/web/tree-slash-ref.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -240,14 +240,16 @@
240240 </div>
241241 <div>
242242 <dt>
243-<kbd>s</kbd>
244-<kbd>t</kbd>
245-<kbd>c</kbd>
246-<kbd>r</kbd>
247-<kbd>m</kbd>
248-<kbd>i</kbd>
243+<kbd>g s</kbd>
244+<kbd>g f</kbd>
245+<kbd>g c</kbd>
246+<kbd>g b</kbd>
247+<kbd>g m</kbd>
248+<kbd>g i</kbd>
249+<kbd>g r</kbd>
250+<kbd>g a</kbd>
249251 </dt>
250-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
252+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
251253 </div>
252254 <div>
253255 <dt>
@@ -346,45 +348,45 @@
346348 <span class="k">go</span>
347349 </a>
348350 </li>
349-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
351+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
350352 <a role="presentation" data-href="/files">
351-<span class="lbl">summary</span>
353+<span class="lbl">summary (g s)</span>
352354 <span class="k">section</span>
353355 </a>
354356 </li>
355-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
357+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
356358 <a role="presentation" data-href="/files/tree/main">
357-<span class="lbl">files</span>
359+<span class="lbl">files (g f)</span>
358360 <span class="k">section</span>
359361 </a>
360362 </li>
361-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
363+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
362364 <a role="presentation" data-href="/files/commits/main">
363-<span class="lbl">commits</span>
365+<span class="lbl">commits (g c)</span>
364366 <span class="k">section</span>
365367 </a>
366368 </li>
367-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
369+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
368370 <a role="presentation" data-href="/files/refs">
369-<span class="lbl">branches &amp; tags</span>
371+<span class="lbl">branches &amp; tags (g b)</span>
370372 <span class="k">section</span>
371373 </a>
372374 </li>
373-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
375+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
374376 <a role="presentation" data-href="/files/merge-requests">
375-<span class="lbl">merge requests</span>
377+<span class="lbl">merge requests (g m)</span>
376378 <span class="k">section</span>
377379 </a>
378380 </li>
379-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
381+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
380382 <a role="presentation" data-href="/files/releases">
381-<span class="lbl">releases</span>
383+<span class="lbl">releases (g r)</span>
382384 <span class="k">section</span>
383385 </a>
384386 </li>
385-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
387+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
386388 <a role="presentation" data-href="/files/actions">
387-<span class="lbl">actions</span>
389+<span class="lbl">actions (g a)</span>
388390 <span class="k">section</span>
389391 </a>
390392 </li>
@@ -437,7 +439,9 @@
437439 k-filter from:body -> ermite.focusFilter();
438440 k-gh from:body -> ermite.go('.brand');
439441 k-s from:body -> ermite.tab('s');
440- k-t from:body -> ermite.tab('t');
442+ k-f from:body -> ermite.tab('f');
443+ k-b from:body -> ermite.tab('b');
444+ k-a from:body -> ermite.tab('a');
441445 k-c from:body -> ermite.tab('c');
442446 k-r from:body -> ermite.tab('r');
443447 k-m from:body -> ermite.tab('m');
@@ -453,7 +457,6 @@
453457 k-permalink from:body -> ermite.permalink();
454458 k-merge from:body -> window.ermiteMR?.openMerge();
455459 k-read from:body -> window.ermiteMR?.markRead();
456- k-gf from:body -> window.ermiteMR?.focusFiles();
457460 k-keys from:body -> ermite.toggleKeys()">
458461 </div>
459462 </body>
M internal/app/testdata/web/tree-subdir.html +33 -30
@@ -73,31 +73,31 @@
7373 <nav class="tabs" aria-label="Sections">
7474 <ul>
7575 <li>
76-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
76+<a href="/files" data-key="g s">Summary <kbd aria-hidden="true">g s</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
80+<a href="/files/tree/main" data-key="g f" aria-current="page">Files <kbd aria-hidden="true">g f</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
84+<a href="/files/commits/main" data-key="g c">Commits <kbd aria-hidden="true">g c</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
88+<a href="/files/refs" data-key="g b">Branches &amp; tags <kbd aria-hidden="true">g b</kbd>
8989 </a>
9090 </li>
9191 <li>
92-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
92+<a href="/files/merge-requests" data-key="g m">Merge requests <kbd aria-hidden="true">g m</kbd>
9393 </a>
9494 </li>
9595 <li>
96-<a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd>
96+<a href="/files/releases" data-key="g r">Releases <kbd aria-hidden="true">g r</kbd>
9797 </a>
9898 </li>
9999 <li>
100-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
100+<a href="/files/actions" data-key="g a">Actions <kbd aria-hidden="true">g a</kbd>
101101 </a>
102102 </li>
103103 </ul>
@@ -256,14 +256,16 @@
256256 </div>
257257 <div>
258258 <dt>
259-<kbd>s</kbd>
260-<kbd>t</kbd>
261-<kbd>c</kbd>
262-<kbd>r</kbd>
263-<kbd>m</kbd>
264-<kbd>i</kbd>
259+<kbd>g s</kbd>
260+<kbd>g f</kbd>
261+<kbd>g c</kbd>
262+<kbd>g b</kbd>
263+<kbd>g m</kbd>
264+<kbd>g i</kbd>
265+<kbd>g r</kbd>
266+<kbd>g a</kbd>
265267 </dt>
266-<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd>
268+<dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues, releases, actions (when available)</dd>
267269 </div>
268270 <div>
269271 <dt>
@@ -362,45 +364,45 @@
362364 <span class="k">go</span>
363365 </a>
364366 </li>
365-<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
367+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary (g s)">
366368 <a role="presentation" data-href="/files">
367-<span class="lbl">summary</span>
369+<span class="lbl">summary (g s)</span>
368370 <span class="k">section</span>
369371 </a>
370372 </li>
371-<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
373+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files (g f)">
372374 <a role="presentation" data-href="/files/tree/main">
373-<span class="lbl">files</span>
375+<span class="lbl">files (g f)</span>
374376 <span class="k">section</span>
375377 </a>
376378 </li>
377-<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
379+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits (g c)">
378380 <a role="presentation" data-href="/files/commits/main">
379-<span class="lbl">commits</span>
381+<span class="lbl">commits (g c)</span>
380382 <span class="k">section</span>
381383 </a>
382384 </li>
383-<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
385+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags (g b)">
384386 <a role="presentation" data-href="/files/refs">
385-<span class="lbl">branches &amp; tags</span>
387+<span class="lbl">branches &amp; tags (g b)</span>
386388 <span class="k">section</span>
387389 </a>
388390 </li>
389-<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
391+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests (g m)">
390392 <a role="presentation" data-href="/files/merge-requests">
391-<span class="lbl">merge requests</span>
393+<span class="lbl">merge requests (g m)</span>
392394 <span class="k">section</span>
393395 </a>
394396 </li>
395-<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
397+<li id="cmd-o6" role="option" aria-selected="false" data-k="section releases (g r)">
396398 <a role="presentation" data-href="/files/releases">
397-<span class="lbl">releases</span>
399+<span class="lbl">releases (g r)</span>
398400 <span class="k">section</span>
399401 </a>
400402 </li>
401-<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
403+<li id="cmd-o7" role="option" aria-selected="false" data-k="section actions (g a)">
402404 <a role="presentation" data-href="/files/actions">
403-<span class="lbl">actions</span>
405+<span class="lbl">actions (g a)</span>
404406 <span class="k">section</span>
405407 </a>
406408 </li>
@@ -453,7 +455,9 @@
453455 k-filter from:body -> ermite.focusFilter();
454456 k-gh from:body -> ermite.go('.brand');
455457 k-s from:body -> ermite.tab('s');
456- k-t from:body -> ermite.tab('t');
458+ k-f from:body -> ermite.tab('f');
459+ k-b from:body -> ermite.tab('b');
460+ k-a from:body -> ermite.tab('a');
457461 k-c from:body -> ermite.tab('c');
458462 k-r from:body -> ermite.tab('r');
459463 k-m from:body -> ermite.tab('m');
@@ -469,7 +473,6 @@
469473 k-permalink from:body -> ermite.permalink();
470474 k-merge from:body -> window.ermiteMR?.openMerge();
471475 k-read from:body -> window.ermiteMR?.markRead();
472- k-gf from:body -> window.ermiteMR?.focusFiles();
473476 k-keys from:body -> ermite.toggleKeys()">
474477 </div>
475478 </body>
M internal/app/web_issue_test.go +1 -1
@@ -28,7 +28,7 @@ func TestWebIssuePagesEndToEnd(t *testing.T) {
2828 // Off by default: no page, no tab, for the owner as well.
2929 assert.Equal(t, 404, b.get("/proj/issues", as(token)).status)
3030 assert.Equal(t, 404, b.get("/proj/issues/new", as(token)).status)
31- assert.NotContains(t, b.get("/proj", as(token)).body, `data-key="i"`)
31+ assert.NotContains(t, b.get("/proj", as(token)).body, `data-key="g i"`)
3232
3333 mode := domain.IssuesOwner
3434 _, err := r.forge.Repos.Update(r.ctx, domain.Root(), r.name("proj"), ports.UpdateRepoParams{Issues: &mode})
M internal/app/web_mr_test.go +1 -1
@@ -93,7 +93,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) {
9393 require.Equal(t, 200, list.status)
9494 assert.Contains(t, list.body, "no open merge requests")
9595 assert.Contains(t, list.body, `href="/proj/compare/feat/search"`)
96- assert.Contains(t, list.body, `data-key="m"`, "the section of the nav")
96+ assert.Contains(t, list.body, `data-key="g m"`, "the section of the nav")
9797 assert.NotContains(t, strings.ToLower(list.body), "proposal")
9898
9999 // Open one from the compare page.
M internal/ports/mocks/ReleaseStore.go +66 -0
@@ -311,6 +311,72 @@ func (_c *ReleaseStore_GetAssetByName_Call) RunAndReturn(run func(ctx context.Co
311311 return _c
312312 }
313313
314+// GetLatestRelease provides a mock function for the type ReleaseStore
315+func (_mock *ReleaseStore) GetLatestRelease(ctx context.Context, repo domain.RepoName) (domain.Release, error) {
316+ ret := _mock.Called(ctx, repo)
317+
318+ if len(ret) == 0 {
319+ panic("no return value specified for GetLatestRelease")
320+ }
321+
322+ var r0 domain.Release
323+ var r1 error
324+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (domain.Release, error)); ok {
325+ return returnFunc(ctx, repo)
326+ }
327+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) domain.Release); ok {
328+ r0 = returnFunc(ctx, repo)
329+ } else {
330+ r0 = ret.Get(0).(domain.Release)
331+ }
332+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
333+ r1 = returnFunc(ctx, repo)
334+ } else {
335+ r1 = ret.Error(1)
336+ }
337+ return r0, r1
338+}
339+
340+// ReleaseStore_GetLatestRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestRelease'
341+type ReleaseStore_GetLatestRelease_Call struct {
342+ *mock.Call
343+}
344+
345+// GetLatestRelease is a helper method to define mock.On call
346+// - ctx context.Context
347+// - repo domain.RepoName
348+func (_e *ReleaseStore_Expecter) GetLatestRelease(ctx any, repo any) *ReleaseStore_GetLatestRelease_Call {
349+ return &ReleaseStore_GetLatestRelease_Call{Call: _e.mock.On("GetLatestRelease", ctx, repo)}
350+}
351+
352+func (_c *ReleaseStore_GetLatestRelease_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseStore_GetLatestRelease_Call {
353+ _c.Call.Run(func(args mock.Arguments) {
354+ var arg0 context.Context
355+ if args[0] != nil {
356+ arg0 = args[0].(context.Context)
357+ }
358+ var arg1 domain.RepoName
359+ if args[1] != nil {
360+ arg1 = args[1].(domain.RepoName)
361+ }
362+ run(
363+ arg0,
364+ arg1,
365+ )
366+ })
367+ return _c
368+}
369+
370+func (_c *ReleaseStore_GetLatestRelease_Call) Return(release domain.Release, err error) *ReleaseStore_GetLatestRelease_Call {
371+ _c.Call.Return(release, err)
372+ return _c
373+}
374+
375+func (_c *ReleaseStore_GetLatestRelease_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) (domain.Release, error)) *ReleaseStore_GetLatestRelease_Call {
376+ _c.Call.Return(run)
377+ return _c
378+}
379+
314380 // GetRelease provides a mock function for the type ReleaseStore
315381 func (_mock *ReleaseStore) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {
316382 ret := _mock.Called(ctx, repo, id)
M internal/ports/release.go +3 -0
@@ -13,6 +13,8 @@ import (
1313 // Used includes both uploading and uploaded assets.
1414 type ReleaseWrite struct {
1515 Repo domain.RepoName
16+ SetLatest bool // false leaves the latest index unchanged
17+ LatestID int64 // zero means no eligible release when SetLatest
1618 Used int64
1719 UsageVersion uint64
1820 PutReleases, DeleteReleases []domain.Release
@@ -21,6 +23,7 @@ type ReleaseWrite struct {
2123
2224 // ReleaseStore keeps metadata, never contents. *kvstore.Releases implements it.
2325 type ReleaseStore interface {
26+ GetLatestRelease(ctx context.Context, repo domain.RepoName) (domain.Release, error)
2427 NextReleaseID(ctx context.Context) (int64, error)
2528 ReleaseUsage(ctx context.Context, repo domain.RepoName) (used int64, version uint64, err error)
2629 GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error)
M internal/usecase/releases.go +25 -9
@@ -92,6 +92,30 @@ func (r *Releases) updateReleases(ctx context.Context, repo domain.RepoName, fn
9292 if err = fn(&w); err != nil {
9393 return err
9494 }
95+ if len(w.PutReleases) > 0 || len(w.DeleteReleases) > 0 {
96+ w.SetLatest = true
97+ all, err := r.store.ListReleases(ctx, repo)
98+ if err != nil {
99+ return err
100+ }
101+ byID := make(map[int64]domain.Release, len(all)+len(w.PutReleases))
102+ for _, rel := range all {
103+ byID[rel.ID] = rel
104+ }
105+ for _, rel := range w.DeleteReleases {
106+ delete(byID, rel.ID)
107+ }
108+ for _, rel := range w.PutReleases {
109+ byID[rel.ID] = rel
110+ }
111+ all = all[:0]
112+ for _, rel := range byID {
113+ all = append(all, rel)
114+ }
115+ if latest, ok := domain.LatestRelease(all); ok {
116+ w.LatestID = latest.ID
117+ }
118+ }
95119 err = r.store.ApplyReleases(ctx, w)
96120 if errors.Is(err, domain.ErrConflict) {
97121 continue
@@ -245,15 +269,7 @@ func (r *Releases) GetRelease(ctx context.Context, p domain.Principal, name doma
245269 case ref.Tag != "":
246270 rel, err = r.store.GetReleaseByTag(ctx, name, ref.Tag)
247271 default:
248- var all []domain.Release
249- all, err = r.store.ListReleases(ctx, name)
250- if err == nil {
251- var ok bool
252- rel, ok = domain.LatestRelease(all)
253- if !ok {
254- err = domain.ErrNotFound
255- }
256- }
272+ rel, err = r.store.GetLatestRelease(ctx, name)
257273 }
258274 if err == nil && rel.Draft && !releaseWriter(p, name) {
259275 err = domain.ErrNotFound
M internal/usecase/releases_integration_test.go +59 -0
@@ -263,6 +263,7 @@ func TestReleaseWriteRetriesUsageConflict(t *testing.T) {
263263 reader.EXPECT().ResolveRevision(ctx, repo, rev).Return(domain.ResolvedRevision{}, domain.ErrNotFound)
264264 store.EXPECT().GetReleaseByTag(ctx, repo, "v1").Return(domain.Release{}, domain.ErrNotFound)
265265 store.EXPECT().NextReleaseID(ctx).Return(int64(1), nil)
266+ store.EXPECT().ListReleases(ctx, repo).Return(nil, nil).Twice()
266267 store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(1), nil).Once()
267268 store.EXPECT().ApplyReleases(ctx, mock.MatchedBy(func(w ports.ReleaseWrite) bool { return w.UsageVersion == 1 && len(w.PutReleases) == 1 })).Return(domain.ErrConflict).Once()
268269 store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(2), nil).Once()
@@ -334,3 +335,61 @@ func TestReleaseUpdateTargetNeverMovesExistingTag(t *testing.T) {
334335 require.NoError(t, err)
335336 require.Equal(t, oidA, got.TargetCommitish)
336337 }
338+
339+func TestReleaseLatestIndexTransitions(t *testing.T) {
340+ r := newReleaseRig(t, ports.ReleaseLimits{})
341+ latest := func(id int64) {
342+ t.Helper()
343+ got, err := r.svc.GetRelease(r.ctx, domain.Anonymous(), r.repo, ports.ReleaseRef{Latest: true})
344+ if id == 0 {
345+ require.ErrorIs(t, err, domain.ErrNotFound)
346+ return
347+ }
348+ require.NoError(t, err)
349+ require.Equal(t, id, got.ID)
350+ }
351+ update := func(id int64, p ports.UpdateReleaseParams) {
352+ t.Helper()
353+ _, err := r.svc.UpdateRelease(r.ctx, domain.Root(), r.repo, id, p)
354+ require.NoError(t, err)
355+ }
356+ yes, no := true, false
357+ draft := r.create(t, "draft", true)
358+ latest(0)
359+ first := r.create(t, "v1", false)
360+ latest(first.ID)
361+ update(draft.ID, ports.UpdateReleaseParams{Draft: &no})
362+ latest(first.ID) // equal creation times: higher ID wins
363+ second := r.create(t, "v2", false)
364+ latest(second.ID)
365+ update(second.ID, ports.UpdateReleaseParams{Prerelease: &yes})
366+ latest(first.ID)
367+ update(second.ID, ports.UpdateReleaseParams{Prerelease: &no})
368+ latest(second.ID)
369+ update(first.ID, ports.UpdateReleaseParams{MakeLatest: &yes})
370+ latest(first.ID)
371+ tag := "renamed"
372+ r.resolve(t, tag, nil)
373+ update(first.ID, ports.UpdateReleaseParams{Tag: &tag})
374+ latest(first.ID)
375+ require.NoError(t, r.svc.DeleteRelease(r.ctx, domain.Root(), r.repo, first.ID))
376+ latest(second.ID)
377+ update(second.ID, ports.UpdateReleaseParams{Draft: &yes})
378+ latest(draft.ID)
379+ update(draft.ID, ports.UpdateReleaseParams{Draft: &yes})
380+ latest(0)
381+}
382+
383+// No ListReleases expectation: latest resolution must use only point reads.
384+func TestReleaseLatestUsesIndex(t *testing.T) {
385+ ctx := t.Context()
386+ repo := name(t, "indexed")
387+ store := mocks.NewReleaseStore(t)
388+ meta := mocks.NewRepoMetaStore(t)
389+ meta.EXPECT().Get(ctx, repo).Return(domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}, nil)
390+ store.EXPECT().GetLatestRelease(ctx, repo).Return(domain.Release{ID: 42, Repo: repo}, nil).Once()
391+ svc := usecase.NewReleases(store, nil, meta, nil, nil, nil, nil, nil, ports.ReleaseLimits{})
392+ got, err := svc.GetRelease(ctx, domain.Anonymous(), repo, ports.ReleaseRef{Latest: true})
393+ require.NoError(t, err)
394+ require.EqualValues(t, 42, got.ID)
395+}
A internal/usecase/releases_write_test.go +27 -0
@@ -0,0 +1,27 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package usecase
3+
4+import (
5+ "testing"
6+
7+ "bnema.dev/ermite/internal/domain"
8+ "bnema.dev/ermite/internal/ports"
9+ "bnema.dev/ermite/internal/ports/mocks"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+// No ListReleases expectation: asset-only changes must not scan releases.
14+func TestAssetOnlyWriteDoesNotListReleases(t *testing.T) {
15+ ctx := t.Context()
16+ repo, err := domain.ParseRepoName("assets")
17+ require.NoError(t, err)
18+ store := mocks.NewReleaseStore(t)
19+ asset := domain.ReleaseAsset{ID: 1, Repo: repo}
20+ store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(2), nil).Once()
21+ store.EXPECT().ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: 2, PutAssets: []domain.ReleaseAsset{asset}}).Return(nil).Once()
22+ svc := &Releases{store: store}
23+ require.NoError(t, svc.updateReleases(ctx, repo, func(w *ports.ReleaseWrite) error {
24+ w.PutAssets = append(w.PutAssets, asset)
25+ return nil
26+ }))
27+}

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 41

↑↓ moveenter openesc close