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
139 - [Issues](docs/issues.md)139 - [Issues](docs/issues.md)
140 - [Commit signatures](docs/commit-signatures.md): SSH and OpenPGP badges, trusted keys140 - [Commit signatures](docs/commit-signatures.md): SSH and OpenPGP badges, trusted keys
141 - [Web sign-in](docs/web-sign-in.md): passkeys and one-time links141 - [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
142 - [CI](docs/ci.md): runners, trust, concurrency, reusable workflows143 - [CI](docs/ci.md): runners, trust, concurrency, reusable workflows
143 - [CI events](docs/ci-events.md)144 - [CI events](docs/ci-events.md)
144 - [OIDC](docs/ci-oidc.md)145 - [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
50 50
51 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.51 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.
52 52
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.
54 54
55 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.55 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.
56 56
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
142 `N+` when the branch list is capped: the reader's existing count operation counts all reachable142 `N+` when the branch list is capped: the reader's existing count operation counts all reachable
143 ancestors, not the source-minus-target range.143 ancestors, not the source-minus-target range.
144 144
145-The rail keeps Files (including its `g f` shortcut) and Verdicts. Merge settings145+The rail keeps Files (including its `l` shortcut) and Verdicts. Merge settings
146 are read-only information in the More (⋯) menu, available to every viewer.146 are read-only information in the More (⋯) menu, available to every viewer.
147 Signing-key information is also there when a merge box offers signed modes;147 Signing-key information is also there when a merge box offers signed modes;
148 merge actions remain restricted to their existing permissions.148 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) {
364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
365 _, err := kvstore.Open(ctx, dir)365 _, err := kvstore.Open(ctx, dir)
366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
367- assert.Equal(t, uint32(6), uint32(kvstore.SchemaVersion))367+ assert.Equal(t, uint32(7), uint32(kvstore.SchemaVersion))
368 }368 }
369 369
370 // A reopen racing the delete of the repository either lands before the delete370 // 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 {
36 func relNameKey(a domain.ReleaseAsset) []byte {36 func relNameKey(a domain.ReleaseAsset) []byte {
37 return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)37 return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)
38 }38 }
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+}
40 func encodeRelease(r domain.Release) []byte {59 func encodeRelease(r domain.Release) []byte {
41 return mustMarshal(&storagepb.Release{60 return mustMarshal(&storagepb.Release{
42 Id: r.ID,61 Id: r.ID,
@@ -285,6 +304,13 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
285 b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)304 b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)
286 }305 }
287 b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))306 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+ }
288 snap := c.db.Snapshot(ctx)314 snap := c.db.Snapshot(ctx)
289 for _, r := range w.DeleteReleases {315 for _, r := range w.DeleteReleases {
290 if r.Repo != w.Repo || r.Version == 0 {316 if r.Repo != w.Repo || r.Version == 0 {
@@ -388,7 +414,7 @@ func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) erro
388 414
389 // releaseKeys collects all repository-owned release keys, including global indexes.415 // releaseKeys collects all repository-owned release keys, including global indexes.
390 func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {416 func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
391- keys := [][]byte{relUseKey(repo)}417+ keys := [][]byte{relUseKey(repo), relLatestKey(repo)}
392 for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {418 for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {
393 for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {419 for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {
394 keys = append(keys, slices.Clone(key))420 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 (
43 // holding all eleven previous scopes, preserving their owner rights. Version 643 // holding all eleven previous scopes, preserving their owner rights. Version 6
44 // adds releases and release:write; older builds cannot clean up their metadata44 // adds releases and release:write; older builds cannot clean up their metadata
45 // or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).45 // or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).
46-const SchemaVersion = 646+// Version 7 adds the latest-release index; older writers would leave it stale.
47+const SchemaVersion = 7
47 48
48 const schemaKey = "meta/schema"49 const schemaKey = "meta/schema"
49 50
@@ -76,6 +77,10 @@ func Open(ctx context.Context, dir string) (*Store, error) {
76 _ = db.Close(context.WithoutCancel(ctx))77 _ = db.Close(context.WithoutCancel(ctx))
77 return nil, err78 return nil, err
78 }79 }
80+ if err := s.buildReleaseLatestIndex(ctx); err != nil {
81+ _ = db.Close(context.WithoutCancel(ctx))
82+ return nil, err
83+ }
79 return s, nil84 return s, nil
80 }85 }
81 86
M internal/adapters/web/fragments.go +1 -1
@@ -167,7 +167,7 @@ func (l *liveStream) gone(ctx context.Context, name string) ([]fragment, error)
167 func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {167 func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragment, error) {
168 switch ev := we.Event.(type) {168 switch ev := we.Event.(type) {
169 case domain.ReleasePublished, domain.ReleaseUpdated, domain.ReleaseDeleted, domain.ReleaseUnpublished:169 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" {
171 return gap, nil // notes, assets, latest or visibility changed: reload171 return gap, nil // notes, assets, latest or visibility changed: reload
172 }172 }
173 return nil, nil173 return nil, nil
M internal/adapters/web/fragments_test.go +3 -3
@@ -101,8 +101,8 @@ func TestMRPushedHeadSignal(t *testing.T) {
101 }101 }
102 }102 }
103 103
104-// Release lifecycle events invalidate the release pages without putting notes104+// Release lifecycle events invalidate release pages and the summary's release
105-// or asset metadata in SSE payloads. Other views need no release fragments.105+// fact without putting notes or asset metadata in SSE payloads.
106 func TestReleaseLiveUpdates(t *testing.T) {106 func TestReleaseLiveUpdates(t *testing.T) {
107 for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} {107 for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} {
108 we := ports.WatchEvent{Event: ev}108 we := ports.WatchEvent{Event: ev}
@@ -111,7 +111,7 @@ func TestReleaseLiveUpdates(t *testing.T) {
111 st := &liveStream{repo: "proj", view: view}111 st := &liveStream{repo: "proj", view: view}
112 got, err := st.render(t.Context(), we)112 got, err := st.render(t.Context(), we)
113 require.NoError(t, err)113 require.NoError(t, err)
114- if view == "release" || view == "releases" {114+ if view == "release" || view == "releases" || view == "summary" {
115 assert.Equal(t, gap, got)115 assert.Equal(t, gap, got)
116 } else {116 } else {
117 assert.Empty(t, got)117 assert.Empty(t, got)
M internal/adapters/web/issues_test.go +3 -3
@@ -100,7 +100,7 @@ func TestIssueList(t *testing.T) {
100 assert.Contains(t, body, "crash &lt;b&gt;on&lt;/b&gt; start")100 assert.Contains(t, body, "crash &lt;b&gt;on&lt;/b&gt; start")
101 assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page")101 assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page")
102 assert.Contains(t, body, `href="/proj/issues?state=closed"`, "closed tab")102 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")
104 assert.NotContains(t, body, "New issue", "no form for an anonymous visitor")104 assert.NotContains(t, body, "New issue", "no form for an anonymous visitor")
105 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))105 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
106 106
@@ -169,7 +169,7 @@ func TestIssueNotFound(t *testing.T) {
169 }169 }
170 summary := off.do("GET", "/proj", false, nil)170 summary := off.do("GET", "/proj", false, nil)
171 require.Equal(t, 200, summary.Code)171 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"`)
173 173
174 // The mode is unset in the settings: off as well.174 // The mode is unset in the settings: off as well.
175 unset := newIssueRig(t, "", false)175 unset := newIssueRig(t, "", false)
@@ -257,7 +257,7 @@ func TestIssueRoutesAbsentWithoutService(t *testing.T) {
257 }257 }
258 rec := httptest.NewRecorder()258 rec := httptest.NewRecorder()
259 srv.ServeHTTP(rec, httptest.NewRequest("GET", "/proj", nil))259 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"`)
261 }261 }
262 262
263 func TestIssueNumberRedirects(t *testing.T) {263 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() {
23 k-filter from:body -> ermite.focusFilter();23 k-filter from:body -> ermite.focusFilter();
24 k-gh from:body -> ermite.go('.brand');24 k-gh from:body -> ermite.go('.brand');
25 k-s from:body -> ermite.tab('s');25 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');
27 k-c from:body -> ermite.tab('c');29 k-c from:body -> ermite.tab('c');
28 k-r from:body -> ermite.tab('r');30 k-r from:body -> ermite.tab('r');
29 k-m from:body -> ermite.tab('m');31 k-m from:body -> ermite.tab('m');
@@ -39,7 +41,6 @@ templ Keys() {
39 k-permalink from:body -> ermite.permalink();41 k-permalink from:body -> ermite.permalink();
40 k-merge from:body -> window.ermiteMR?.openMerge();42 k-merge from:body -> window.ermiteMR?.openMerge();
41 k-read from:body -> window.ermiteMR?.markRead();43 k-read from:body -> window.ermiteMR?.markRead();
42- k-gf from:body -> window.ermiteMR?.focusFiles();
43 k-keys from:body -> ermite.toggleKeys()"44 k-keys from:body -> ermite.toggleKeys()"
44 ></div>45 ></div>
45 }46 }
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) {
430 templ mrRail(p MRPage) {430 templ mrRail(p MRPage) {
431 <section aria-labelledby="r-files" class="r-files">431 <section aria-labelledby="r-files" class="r-files">
432 <h2 id="r-files">432 <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>
434 <button type="button" class="btn icon fold" data-js aria-expanded="true" title="fold the list of files" hx-on:click="ermiteMR.foldFiles()">434 <button type="button" class="btn icon fold" data-js aria-expanded="true" title="fold the list of files" hx-on:click="ermiteMR.foldFiles()">
435 <span class="ico i-chevron" aria-hidden="true"></span><span class="sr">fold the list of files</span>435 <span class="ico i-chevron" aria-hidden="true"></span><span class="sr">fold the list of files</span>
436 </button>436 </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) {
157 assert.Contains(t, footer, "more commits")157 assert.Contains(t, footer, "more commits")
158 assert.Contains(t, footer, "1/2 failed")158 assert.Contains(t, footer, "1/2 failed")
159 rail := renderToString(t, mrRail(p))159 rail := renderToString(t, mrRail(p))
160- assert.Contains(t, rail, "g f")160+ assert.Contains(t, rail, `<kbd aria-hidden="true">l</kbd>`)
161 for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {161 for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {
162 assert.NotContains(t, rail, text)162 assert.NotContains(t, rail, text)
163 }163 }
M internal/adapters/web/overlays.templ +2 -2
@@ -26,7 +26,7 @@ templ OverlayHelp(mr bool) {
26 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>26 <div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div>
27 <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>27 <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>
28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>28 <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>
30 if SignedIn(ctx) {30 if SignedIn(ctx) {
31 <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>31 <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
32 }32 }
@@ -37,7 +37,7 @@ templ OverlayHelp(mr bool) {
37 <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>37 <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>
38 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>38 <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div>
39 if mr {39 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>
41 <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>41 <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>
42 <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div>42 <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div>
43 }43 }
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) {
115 if p.Latest != nil {115 if p.Latest != nil {
116 @latestTagFact(*p.Latest)116 @latestTagFact(*p.Latest)
117 }117 }
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+ }
118 </dl>126 </dl>
119 </section>127 </section>
120 if len(p.Recent) > 0 {128 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) {
40 40
41 templ ReleaseDetail(l Layout, p ReleaseDetailPage) {41 templ ReleaseDetail(l Layout, p ReleaseDetailPage) {
42 @Shell(l, nil) {42 @Shell(l, nil) {
43- <h1>{ p.Title }</h1>43+ <section class="mr-card">
44- <p>44+ <header class="show mr-h">
45- <span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)45+ <h1>{ p.Title }</h1>
46- if p.SHA != "" {46+ <dl class="kv show-kv">
47- <a href={ templ.SafeURL(p.CommitHref) }><code>{ p.SHA }</code></a>47+ <dt>tag</dt>
48- }48+ <dd><span class="pill">{ p.Tag }</span> @ReleaseBadges(p.ReleaseRow)</dd>
49- <span class="dim">{ p.Published }</span>49+ if p.SHA != "" {
50- </p>50+ <dt>commit</dt>
51- <div class="md">@templ.Raw(string(p.Notes))</div>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>
52 <h2>Assets</h2>59 <h2>Assets</h2>
53 if len(p.Assets) == 0 {60 if len(p.Assets) == 0 {
54 <p class="dim">no assets.</p>61 <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 {
38 t.Helper()38 t.Helper()
39 repo := repoName(t, "proj")39 repo := repoName(t, "proj")
40 f := &releaseFixture{svc: mocks.NewReleaseService(t), data: "0123456789"}40 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}
42 f.draft = domain.Release{ID: 2, Repo: repo, Tag: "unpublished", Title: "Secret draft", Draft: true, MakeLatest: true}42 f.draft = domain.Release{ID: 2, Repo: repo, Tag: "unpublished", Title: "Secret draft", Draft: true, MakeLatest: true}
43 f.asset = domain.ReleaseAsset{ID: 7, ReleaseID: 1, Repo: repo, Name: "build.zip", Size: 10, ContentType: "application/zip", Digest: "sha256:" + strings.Repeat("b", 64)}43 f.asset = domain.ReleaseAsset{ID: 7, ReleaseID: 1, Repo: repo, Name: "build.zip", Size: 10, ContentType: "application/zip", Digest: "sha256:" + strings.Repeat("b", 64)}
44 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) {44 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) {
120 assert.Contains(t, body, "2026-10-12")120 assert.Contains(t, body, "2026-10-12")
121 assert.Contains(t, body, `<td>1</td>`)121 assert.Contains(t, body, `<td>1</td>`)
122 assert.Contains(t, body, `?page=next%2B%2F%3D`)122 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"`)
124 assert.Contains(t, body, "view=releases")125 assert.Contains(t, body, "view=releases")
125 }126 }
126 owner := f.request("GET", "/proj/releases?page=cursor%2B%2F%3D", "tok", "")127 owner := f.request("GET", "/proj/releases?page=cursor%2B%2F%3D", "tok", "")
@@ -137,6 +138,15 @@ func TestReleasePages(t *testing.T) {
137 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"} {138 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"} {
138 assert.Contains(t, body, text)139 assert.Contains(t, body, text)
139 }140 }
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">`)
140 assert.NotContains(t, body, "<script>alert")150 assert.NotContains(t, body, "<script>alert")
141 assert.NotContains(t, body, `href="javascript:`)151 assert.NotContains(t, body, `href="javascript:`)
142 assert.Contains(t, body, `href="/proj/blob/stable/v1/guide.md"`)152 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
144 }144 }
145 l.Sections = []Section{145 l.Sections = []Section{
146 mk("s", "Summary", repoHref(d.name()), "summary"),146 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"),
148 mk("c", "Commits", repoHref(d.name(), "commits", def), "commits"),148 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"),
150 }150 }
151 if s.cfg.MergeRequests != nil {151 if s.cfg.MergeRequests != nil {
152 mrs := mk("m", "Merge requests", mrBase(d.name()), "merge-requests")152 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
157 l.Sections = append(l.Sections, mk("i", "Issues", issuesBase(d.name()), "issues"))157 l.Sections = append(l.Sections, mk("i", "Issues", issuesBase(d.name()), "issues"))
158 }158 }
159 if s.cfg.Releases != nil {159 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"))
161 }161 }
162 l.Sections = s.withActions(l.Sections, d.name(), section)162 l.Sections = s.withActions(l.Sections, d.name(), section)
163 l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}}163 l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}}
164 164
165 l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"})165 l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"})
166 for _, sec := range l.Sections {166 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})
168 }168 }
169 if l.Ref != "" {169 if l.Ref != "" {
170 for _, b := range d.branches {170 for _, b := range d.branches {
M internal/adapters/web/shell.templ +2 -3
@@ -172,8 +172,7 @@ templ Header(l Layout) {
172 <li>172 <li>
173 <a173 <a
174 href={ templ.SafeURL(s.Href) }174 href={ templ.SafeURL(s.Href) }
175- data-key={ s.Key }175+ data-key={ "g " + s.Key }
176- aria-keyshortcuts={ s.Key }
177 if s.Current {176 if s.Current {
178 aria-current="page"177 aria-current="page"
179 }178 }
@@ -182,7 +181,7 @@ templ Header(l Layout) {
182 if s.Count != "" {181 if s.Count != "" {
183 <span class="count">{ s.Count }<span class="sr">{ " open" }</span></span>182 <span class="count">{ s.Count }<span class="sr">{ " open" }</span></span>
184 }183 }
185- <kbd aria-hidden="true">{ s.Key }</kbd>184+ <kbd aria-hidden="true">{ "g " + s.Key }</kbd>
186 </a>185 </a>
187 </li>186 </li>
188 }187 }
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 @@
112 $(sel)?.click();112 $(sel)?.click();
113 },113 },
114 tab(key) {114 tab(key) {
115- $(`.tabs [data-key=${key}]`)?.click();115+ $(`.tabs [data-key="g ${key}"]`)?.click();
116 },116 },
117 117
118 // The preference "single-key shortcuts" (ermiteKeys): the checkbox of the118 // The preference "single-key shortcuts" (ermiteKeys): the checkbox of the
@@ -617,11 +617,11 @@
617 617
618 // 1. Keys.618 // 1. Keys.
619 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',619 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',
621 // the merge request page (mr.js): the merge menu, mark read621 // the merge request page (mr.js): the merge menu, mark read
622 M: 'merge', R: 'read' };622 M: 'merge', R: 'read' };
623- // "g t" is the key "t": the tabs have one event each.623+ // Section keys are the second letter of the g sequence.
624- const goto = { h: 'gh', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', g: 'top', f: 'gf' };624+ const goto = { h: 'gh', s: 's', f: 'f', c: 'c', b: 'b', m: 'm', i: 'i', r: 'r', a: 'a', g: 'top' };
625 let pending = 0;625 let pending = 0;
626 let pendingTimer = 0;626 let pendingTimer = 0;
627 addEventListener('keydown', (e) => {627 addEventListener('keydown', (e) => {
@@ -647,6 +647,15 @@
647 // behind it does not react (scripted clicks would go through inert).647 // behind it does not react (scripted clicks would go through inert).
648 if ($('dialog[open]') && !['?', 'b', ':', 'f', 'Enter'].includes(e.key)) { pending = 0; return; }648 if ($('dialog[open]') && !['?', 'b', ':', 'f', 'Enter'].includes(e.key)) { pending = 0; return; }
649 if (e.key === 'Enter' && e.target.closest('a, button, summary')) return;649 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+ }
650 if (pending) {659 if (pending) {
651 const name = goto[e.key];660 const name = goto[e.key];
652 pending = 0;661 pending = 0;
M internal/adapters/web/static/js/mr.js +2 -2
@@ -8,7 +8,7 @@
8 // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter8 // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter
9 // submits (the browser does it: the focus is on the button), Esc closes.9 // submits (the browser does it: the focus is on the button), Esc closes.
10 // R marks the head read. Nothing merges on one key.10 // 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 / k11+// 3. l puts the focus in the list of files (opening it when folded); j / k
12 // move in it, Enter opens the file, Esc goes back to the diff. The button12 // move in it, Enter opens the file, Esc goes back to the diff. The button
13 // of its header folds the list, and the choice is remembered.13 // of its header folds the list, and the choice is remembered.
14 //14 //
@@ -186,7 +186,7 @@ window.ermiteMR = (() => {
186 return true;186 return true;
187 }187 }
188 188
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.
190 const links = () => $$('.r-files .files > li:not([hidden]) > a');190 const links = () => $$('.r-files .files > li:not([hidden]) > a');
191 function focusFiles() {191 function focusFiles() {
192 foldFiles(false);192 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,
68 68
69 p.Facts = s.facts(d, now)69 p.Facts = s.facts(d, now)
70 p.Latest = s.latest(ctx, d)70 p.Latest = s.latest(ctx, d)
71+ p.Release = s.summaryRelease(ctx, d.repo.Name, now)
71 p.Actions = s.recentActions(ctx, d.repo.Name, now)72 p.Actions = s.recentActions(ctx, d.repo.Name, now)
72 73
73 if rd, err := s.readme(ctx, d, ref); err != nil {74 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
272 }273 }
273 return a, nil274 return a, nil
274 }275 }
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 {
211 Outline []Heading211 Outline []Heading
212 AllHref string212 AllHref string
213 // Latest is the newest tag, nil when the repository has none.213 // Latest is the newest tag, nil when the repository has none.
214- Latest *LatestTag214+ Latest *LatestTag
215+ Release *LatestRelease
215 // Actions is the recent runs of the rail, nil when the viewer cannot read216 // Actions is the recent runs of the rail, nil when the viewer cannot read
216 // CI (or the server runs none).217 // CI (or the server runs none).
217 Actions *RecentActions218 Actions *RecentActions
218 }219 }
219 220
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+
220 // RecentActions is the "Recent actions" block of the summary rail: the newest226 // RecentActions is the "Recent actions" block of the summary rail: the newest
221 // runs. With no run the block is in the page but hidden, so that the first227 // runs. With no run the block is in the page but hidden, so that the first
222 // run reaches it live.228 // run reaches it live.
M internal/app/testdata/web/blob-binary.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -228,14 +228,16 @@
228 </div>228 </div>
229 <div>229 <div>
230 <dt>230 <dt>
231-<kbd>s</kbd>231+<kbd>g s</kbd>
232-<kbd>t</kbd>232+<kbd>g f</kbd>
233-<kbd>c</kbd>233+<kbd>g c</kbd>
234-<kbd>r</kbd>234+<kbd>g b</kbd>
235-<kbd>m</kbd>235+<kbd>g m</kbd>
236-<kbd>i</kbd>236+<kbd>g i</kbd>
237+<kbd>g r</kbd>
238+<kbd>g a</kbd>
237 </dt>239 </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>
239 </div>241 </div>
240 <div>242 <div>
241 <dt>243 <dt>
@@ -334,45 +336,45 @@
334 <span class="k">go</span>336 <span class="k">go</span>
335 </a>337 </a>
336 </li>338 </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)">
338 <a role="presentation" data-href="/files">340 <a role="presentation" data-href="/files">
339-<span class="lbl">summary</span>341+<span class="lbl">summary (g s)</span>
340 <span class="k">section</span>342 <span class="k">section</span>
341 </a>343 </a>
342 </li>344 </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)">
344 <a role="presentation" data-href="/files/tree/main">346 <a role="presentation" data-href="/files/tree/main">
345-<span class="lbl">files</span>347+<span class="lbl">files (g f)</span>
346 <span class="k">section</span>348 <span class="k">section</span>
347 </a>349 </a>
348 </li>350 </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)">
350 <a role="presentation" data-href="/files/commits/main">352 <a role="presentation" data-href="/files/commits/main">
351-<span class="lbl">commits</span>353+<span class="lbl">commits (g c)</span>
352 <span class="k">section</span>354 <span class="k">section</span>
353 </a>355 </a>
354 </li>356 </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)">
356 <a role="presentation" data-href="/files/refs">358 <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>
358 <span class="k">section</span>360 <span class="k">section</span>
359 </a>361 </a>
360 </li>362 </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)">
362 <a role="presentation" data-href="/files/merge-requests">364 <a role="presentation" data-href="/files/merge-requests">
363-<span class="lbl">merge requests</span>365+<span class="lbl">merge requests (g m)</span>
364 <span class="k">section</span>366 <span class="k">section</span>
365 </a>367 </a>
366 </li>368 </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)">
368 <a role="presentation" data-href="/files/releases">370 <a role="presentation" data-href="/files/releases">
369-<span class="lbl">releases</span>371+<span class="lbl">releases (g r)</span>
370 <span class="k">section</span>372 <span class="k">section</span>
371 </a>373 </a>
372 </li>374 </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)">
374 <a role="presentation" data-href="/files/actions">376 <a role="presentation" data-href="/files/actions">
375-<span class="lbl">actions</span>377+<span class="lbl">actions (g a)</span>
376 <span class="k">section</span>378 <span class="k">section</span>
377 </a>379 </a>
378 </li>380 </li>
@@ -425,7 +427,9 @@
425 k-filter from:body -> ermite.focusFilter();427 k-filter from:body -> ermite.focusFilter();
426 k-gh from:body -> ermite.go('.brand');428 k-gh from:body -> ermite.go('.brand');
427 k-s from:body -> ermite.tab('s');429 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');
429 k-c from:body -> ermite.tab('c');433 k-c from:body -> ermite.tab('c');
430 k-r from:body -> ermite.tab('r');434 k-r from:body -> ermite.tab('r');
431 k-m from:body -> ermite.tab('m');435 k-m from:body -> ermite.tab('m');
@@ -441,7 +445,6 @@
441 k-permalink from:body -> ermite.permalink();445 k-permalink from:body -> ermite.permalink();
442 k-merge from:body -> window.ermiteMR?.openMerge();446 k-merge from:body -> window.ermiteMR?.openMerge();
443 k-read from:body -> window.ermiteMR?.markRead();447 k-read from:body -> window.ermiteMR?.markRead();
444- k-gf from:body -> window.ermiteMR?.focusFiles();
445 k-keys from:body -> ermite.toggleKeys()">448 k-keys from:body -> ermite.toggleKeys()">
446 </div>449 </div>
447 </body>450 </body>
M internal/app/testdata/web/blob-code.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -298,14 +298,16 @@
298 </div>298 </div>
299 <div>299 <div>
300 <dt>300 <dt>
301-<kbd>s</kbd>301+<kbd>g s</kbd>
302-<kbd>t</kbd>302+<kbd>g f</kbd>
303-<kbd>c</kbd>303+<kbd>g c</kbd>
304-<kbd>r</kbd>304+<kbd>g b</kbd>
305-<kbd>m</kbd>305+<kbd>g m</kbd>
306-<kbd>i</kbd>306+<kbd>g i</kbd>
307+<kbd>g r</kbd>
308+<kbd>g a</kbd>
307 </dt>309 </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>
309 </div>311 </div>
310 <div>312 <div>
311 <dt>313 <dt>
@@ -404,45 +406,45 @@
404 <span class="k">go</span>406 <span class="k">go</span>
405 </a>407 </a>
406 </li>408 </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)">
408 <a role="presentation" data-href="/files">410 <a role="presentation" data-href="/files">
409-<span class="lbl">summary</span>411+<span class="lbl">summary (g s)</span>
410 <span class="k">section</span>412 <span class="k">section</span>
411 </a>413 </a>
412 </li>414 </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)">
414 <a role="presentation" data-href="/files/tree/main">416 <a role="presentation" data-href="/files/tree/main">
415-<span class="lbl">files</span>417+<span class="lbl">files (g f)</span>
416 <span class="k">section</span>418 <span class="k">section</span>
417 </a>419 </a>
418 </li>420 </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)">
420 <a role="presentation" data-href="/files/commits/main">422 <a role="presentation" data-href="/files/commits/main">
421-<span class="lbl">commits</span>423+<span class="lbl">commits (g c)</span>
422 <span class="k">section</span>424 <span class="k">section</span>
423 </a>425 </a>
424 </li>426 </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)">
426 <a role="presentation" data-href="/files/refs">428 <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>
428 <span class="k">section</span>430 <span class="k">section</span>
429 </a>431 </a>
430 </li>432 </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)">
432 <a role="presentation" data-href="/files/merge-requests">434 <a role="presentation" data-href="/files/merge-requests">
433-<span class="lbl">merge requests</span>435+<span class="lbl">merge requests (g m)</span>
434 <span class="k">section</span>436 <span class="k">section</span>
435 </a>437 </a>
436 </li>438 </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)">
438 <a role="presentation" data-href="/files/releases">440 <a role="presentation" data-href="/files/releases">
439-<span class="lbl">releases</span>441+<span class="lbl">releases (g r)</span>
440 <span class="k">section</span>442 <span class="k">section</span>
441 </a>443 </a>
442 </li>444 </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)">
444 <a role="presentation" data-href="/files/actions">446 <a role="presentation" data-href="/files/actions">
445-<span class="lbl">actions</span>447+<span class="lbl">actions (g a)</span>
446 <span class="k">section</span>448 <span class="k">section</span>
447 </a>449 </a>
448 </li>450 </li>
@@ -495,7 +497,9 @@
495 k-filter from:body -> ermite.focusFilter();497 k-filter from:body -> ermite.focusFilter();
496 k-gh from:body -> ermite.go('.brand');498 k-gh from:body -> ermite.go('.brand');
497 k-s from:body -> ermite.tab('s');499 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');
499 k-c from:body -> ermite.tab('c');503 k-c from:body -> ermite.tab('c');
500 k-r from:body -> ermite.tab('r');504 k-r from:body -> ermite.tab('r');
501 k-m from:body -> ermite.tab('m');505 k-m from:body -> ermite.tab('m');
@@ -511,7 +515,6 @@
511 k-permalink from:body -> ermite.permalink();515 k-permalink from:body -> ermite.permalink();
512 k-merge from:body -> window.ermiteMR?.openMerge();516 k-merge from:body -> window.ermiteMR?.openMerge();
513 k-read from:body -> window.ermiteMR?.markRead();517 k-read from:body -> window.ermiteMR?.markRead();
514- k-gf from:body -> window.ermiteMR?.focusFiles();
515 k-keys from:body -> ermite.toggleKeys()">518 k-keys from:body -> ermite.toggleKeys()">
516 </div>519 </div>
517 </body>520 </body>
M internal/app/testdata/web/blob-empty.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -228,14 +228,16 @@
228 </div>228 </div>
229 <div>229 <div>
230 <dt>230 <dt>
231-<kbd>s</kbd>231+<kbd>g s</kbd>
232-<kbd>t</kbd>232+<kbd>g f</kbd>
233-<kbd>c</kbd>233+<kbd>g c</kbd>
234-<kbd>r</kbd>234+<kbd>g b</kbd>
235-<kbd>m</kbd>235+<kbd>g m</kbd>
236-<kbd>i</kbd>236+<kbd>g i</kbd>
237+<kbd>g r</kbd>
238+<kbd>g a</kbd>
237 </dt>239 </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>
239 </div>241 </div>
240 <div>242 <div>
241 <dt>243 <dt>
@@ -334,45 +336,45 @@
334 <span class="k">go</span>336 <span class="k">go</span>
335 </a>337 </a>
336 </li>338 </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)">
338 <a role="presentation" data-href="/files">340 <a role="presentation" data-href="/files">
339-<span class="lbl">summary</span>341+<span class="lbl">summary (g s)</span>
340 <span class="k">section</span>342 <span class="k">section</span>
341 </a>343 </a>
342 </li>344 </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)">
344 <a role="presentation" data-href="/files/tree/main">346 <a role="presentation" data-href="/files/tree/main">
345-<span class="lbl">files</span>347+<span class="lbl">files (g f)</span>
346 <span class="k">section</span>348 <span class="k">section</span>
347 </a>349 </a>
348 </li>350 </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)">
350 <a role="presentation" data-href="/files/commits/main">352 <a role="presentation" data-href="/files/commits/main">
351-<span class="lbl">commits</span>353+<span class="lbl">commits (g c)</span>
352 <span class="k">section</span>354 <span class="k">section</span>
353 </a>355 </a>
354 </li>356 </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)">
356 <a role="presentation" data-href="/files/refs">358 <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>
358 <span class="k">section</span>360 <span class="k">section</span>
359 </a>361 </a>
360 </li>362 </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)">
362 <a role="presentation" data-href="/files/merge-requests">364 <a role="presentation" data-href="/files/merge-requests">
363-<span class="lbl">merge requests</span>365+<span class="lbl">merge requests (g m)</span>
364 <span class="k">section</span>366 <span class="k">section</span>
365 </a>367 </a>
366 </li>368 </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)">
368 <a role="presentation" data-href="/files/releases">370 <a role="presentation" data-href="/files/releases">
369-<span class="lbl">releases</span>371+<span class="lbl">releases (g r)</span>
370 <span class="k">section</span>372 <span class="k">section</span>
371 </a>373 </a>
372 </li>374 </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)">
374 <a role="presentation" data-href="/files/actions">376 <a role="presentation" data-href="/files/actions">
375-<span class="lbl">actions</span>377+<span class="lbl">actions (g a)</span>
376 <span class="k">section</span>378 <span class="k">section</span>
377 </a>379 </a>
378 </li>380 </li>
@@ -425,7 +427,9 @@
425 k-filter from:body -> ermite.focusFilter();427 k-filter from:body -> ermite.focusFilter();
426 k-gh from:body -> ermite.go('.brand');428 k-gh from:body -> ermite.go('.brand');
427 k-s from:body -> ermite.tab('s');429 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');
429 k-c from:body -> ermite.tab('c');433 k-c from:body -> ermite.tab('c');
430 k-r from:body -> ermite.tab('r');434 k-r from:body -> ermite.tab('r');
431 k-m from:body -> ermite.tab('m');435 k-m from:body -> ermite.tab('m');
@@ -441,7 +445,6 @@
441 k-permalink from:body -> ermite.permalink();445 k-permalink from:body -> ermite.permalink();
442 k-merge from:body -> window.ermiteMR?.openMerge();446 k-merge from:body -> window.ermiteMR?.openMerge();
443 k-read from:body -> window.ermiteMR?.markRead();447 k-read from:body -> window.ermiteMR?.markRead();
444- k-gf from:body -> window.ermiteMR?.focusFiles();
445 k-keys from:body -> ermite.toggleKeys()">448 k-keys from:body -> ermite.toggleKeys()">
446 </div>449 </div>
447 </body>450 </body>
M internal/app/testdata/web/blob-image.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -230,14 +230,16 @@
230 </div>230 </div>
231 <div>231 <div>
232 <dt>232 <dt>
233-<kbd>s</kbd>233+<kbd>g s</kbd>
234-<kbd>t</kbd>234+<kbd>g f</kbd>
235-<kbd>c</kbd>235+<kbd>g c</kbd>
236-<kbd>r</kbd>236+<kbd>g b</kbd>
237-<kbd>m</kbd>237+<kbd>g m</kbd>
238-<kbd>i</kbd>238+<kbd>g i</kbd>
239+<kbd>g r</kbd>
240+<kbd>g a</kbd>
239 </dt>241 </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>
241 </div>243 </div>
242 <div>244 <div>
243 <dt>245 <dt>
@@ -336,45 +338,45 @@
336 <span class="k">go</span>338 <span class="k">go</span>
337 </a>339 </a>
338 </li>340 </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)">
340 <a role="presentation" data-href="/files">342 <a role="presentation" data-href="/files">
341-<span class="lbl">summary</span>343+<span class="lbl">summary (g s)</span>
342 <span class="k">section</span>344 <span class="k">section</span>
343 </a>345 </a>
344 </li>346 </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)">
346 <a role="presentation" data-href="/files/tree/main">348 <a role="presentation" data-href="/files/tree/main">
347-<span class="lbl">files</span>349+<span class="lbl">files (g f)</span>
348 <span class="k">section</span>350 <span class="k">section</span>
349 </a>351 </a>
350 </li>352 </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)">
352 <a role="presentation" data-href="/files/commits/main">354 <a role="presentation" data-href="/files/commits/main">
353-<span class="lbl">commits</span>355+<span class="lbl">commits (g c)</span>
354 <span class="k">section</span>356 <span class="k">section</span>
355 </a>357 </a>
356 </li>358 </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)">
358 <a role="presentation" data-href="/files/refs">360 <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>
360 <span class="k">section</span>362 <span class="k">section</span>
361 </a>363 </a>
362 </li>364 </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)">
364 <a role="presentation" data-href="/files/merge-requests">366 <a role="presentation" data-href="/files/merge-requests">
365-<span class="lbl">merge requests</span>367+<span class="lbl">merge requests (g m)</span>
366 <span class="k">section</span>368 <span class="k">section</span>
367 </a>369 </a>
368 </li>370 </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)">
370 <a role="presentation" data-href="/files/releases">372 <a role="presentation" data-href="/files/releases">
371-<span class="lbl">releases</span>373+<span class="lbl">releases (g r)</span>
372 <span class="k">section</span>374 <span class="k">section</span>
373 </a>375 </a>
374 </li>376 </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)">
376 <a role="presentation" data-href="/files/actions">378 <a role="presentation" data-href="/files/actions">
377-<span class="lbl">actions</span>379+<span class="lbl">actions (g a)</span>
378 <span class="k">section</span>380 <span class="k">section</span>
379 </a>381 </a>
380 </li>382 </li>
@@ -427,7 +429,9 @@
427 k-filter from:body -> ermite.focusFilter();429 k-filter from:body -> ermite.focusFilter();
428 k-gh from:body -> ermite.go('.brand');430 k-gh from:body -> ermite.go('.brand');
429 k-s from:body -> ermite.tab('s');431 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');
431 k-c from:body -> ermite.tab('c');435 k-c from:body -> ermite.tab('c');
432 k-r from:body -> ermite.tab('r');436 k-r from:body -> ermite.tab('r');
433 k-m from:body -> ermite.tab('m');437 k-m from:body -> ermite.tab('m');
@@ -443,7 +447,6 @@
443 k-permalink from:body -> ermite.permalink();447 k-permalink from:body -> ermite.permalink();
444 k-merge from:body -> window.ermiteMR?.openMerge();448 k-merge from:body -> window.ermiteMR?.openMerge();
445 k-read from:body -> window.ermiteMR?.markRead();449 k-read from:body -> window.ermiteMR?.markRead();
446- k-gf from:body -> window.ermiteMR?.focusFiles();
447 k-keys from:body -> ermite.toggleKeys()">450 k-keys from:body -> ermite.toggleKeys()">
448 </div>451 </div>
449 </body>452 </body>
M internal/app/testdata/web/blob-markdown.html +33 -30
@@ -76,31 +76,31 @@
76 <nav class="tabs" aria-label="Sections">76 <nav class="tabs" aria-label="Sections">
77 <ul>77 <ul>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 <li>102 <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>
104 </a>104 </a>
105 </li>105 </li>
106 </ul>106 </ul>
@@ -295,14 +295,16 @@
295 </div>295 </div>
296 <div>296 <div>
297 <dt>297 <dt>
298-<kbd>s</kbd>298+<kbd>g s</kbd>
299-<kbd>t</kbd>299+<kbd>g f</kbd>
300-<kbd>c</kbd>300+<kbd>g c</kbd>
301-<kbd>r</kbd>301+<kbd>g b</kbd>
302-<kbd>m</kbd>302+<kbd>g m</kbd>
303-<kbd>i</kbd>303+<kbd>g i</kbd>
304+<kbd>g r</kbd>
305+<kbd>g a</kbd>
304 </dt>306 </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>
306 </div>308 </div>
307 <div>309 <div>
308 <dt>310 <dt>
@@ -401,45 +403,45 @@
401 <span class="k">go</span>403 <span class="k">go</span>
402 </a>404 </a>
403 </li>405 </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)">
405 <a role="presentation" data-href="/files">407 <a role="presentation" data-href="/files">
406-<span class="lbl">summary</span>408+<span class="lbl">summary (g s)</span>
407 <span class="k">section</span>409 <span class="k">section</span>
408 </a>410 </a>
409 </li>411 </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)">
411 <a role="presentation" data-href="/files/tree/main">413 <a role="presentation" data-href="/files/tree/main">
412-<span class="lbl">files</span>414+<span class="lbl">files (g f)</span>
413 <span class="k">section</span>415 <span class="k">section</span>
414 </a>416 </a>
415 </li>417 </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)">
417 <a role="presentation" data-href="/files/commits/main">419 <a role="presentation" data-href="/files/commits/main">
418-<span class="lbl">commits</span>420+<span class="lbl">commits (g c)</span>
419 <span class="k">section</span>421 <span class="k">section</span>
420 </a>422 </a>
421 </li>423 </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)">
423 <a role="presentation" data-href="/files/refs">425 <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>
425 <span class="k">section</span>427 <span class="k">section</span>
426 </a>428 </a>
427 </li>429 </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)">
429 <a role="presentation" data-href="/files/merge-requests">431 <a role="presentation" data-href="/files/merge-requests">
430-<span class="lbl">merge requests</span>432+<span class="lbl">merge requests (g m)</span>
431 <span class="k">section</span>433 <span class="k">section</span>
432 </a>434 </a>
433 </li>435 </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)">
435 <a role="presentation" data-href="/files/releases">437 <a role="presentation" data-href="/files/releases">
436-<span class="lbl">releases</span>438+<span class="lbl">releases (g r)</span>
437 <span class="k">section</span>439 <span class="k">section</span>
438 </a>440 </a>
439 </li>441 </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)">
441 <a role="presentation" data-href="/files/actions">443 <a role="presentation" data-href="/files/actions">
442-<span class="lbl">actions</span>444+<span class="lbl">actions (g a)</span>
443 <span class="k">section</span>445 <span class="k">section</span>
444 </a>446 </a>
445 </li>447 </li>
@@ -492,7 +494,9 @@
492 k-filter from:body -> ermite.focusFilter();494 k-filter from:body -> ermite.focusFilter();
493 k-gh from:body -> ermite.go('.brand');495 k-gh from:body -> ermite.go('.brand');
494 k-s from:body -> ermite.tab('s');496 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');
496 k-c from:body -> ermite.tab('c');500 k-c from:body -> ermite.tab('c');
497 k-r from:body -> ermite.tab('r');501 k-r from:body -> ermite.tab('r');
498 k-m from:body -> ermite.tab('m');502 k-m from:body -> ermite.tab('m');
@@ -508,7 +512,6 @@
508 k-permalink from:body -> ermite.permalink();512 k-permalink from:body -> ermite.permalink();
509 k-merge from:body -> window.ermiteMR?.openMerge();513 k-merge from:body -> window.ermiteMR?.openMerge();
510 k-read from:body -> window.ermiteMR?.markRead();514 k-read from:body -> window.ermiteMR?.markRead();
511- k-gf from:body -> window.ermiteMR?.focusFiles();
512 k-keys from:body -> ermite.toggleKeys()">515 k-keys from:body -> ermite.toggleKeys()">
513 </div>516 </div>
514 </body>517 </body>
M internal/app/testdata/web/blob-svg.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -253,14 +253,16 @@
253 </div>253 </div>
254 <div>254 <div>
255 <dt>255 <dt>
256-<kbd>s</kbd>256+<kbd>g s</kbd>
257-<kbd>t</kbd>257+<kbd>g f</kbd>
258-<kbd>c</kbd>258+<kbd>g c</kbd>
259-<kbd>r</kbd>259+<kbd>g b</kbd>
260-<kbd>m</kbd>260+<kbd>g m</kbd>
261-<kbd>i</kbd>261+<kbd>g i</kbd>
262+<kbd>g r</kbd>
263+<kbd>g a</kbd>
262 </dt>264 </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>
264 </div>266 </div>
265 <div>267 <div>
266 <dt>268 <dt>
@@ -359,45 +361,45 @@
359 <span class="k">go</span>361 <span class="k">go</span>
360 </a>362 </a>
361 </li>363 </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)">
363 <a role="presentation" data-href="/files">365 <a role="presentation" data-href="/files">
364-<span class="lbl">summary</span>366+<span class="lbl">summary (g s)</span>
365 <span class="k">section</span>367 <span class="k">section</span>
366 </a>368 </a>
367 </li>369 </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)">
369 <a role="presentation" data-href="/files/tree/main">371 <a role="presentation" data-href="/files/tree/main">
370-<span class="lbl">files</span>372+<span class="lbl">files (g f)</span>
371 <span class="k">section</span>373 <span class="k">section</span>
372 </a>374 </a>
373 </li>375 </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)">
375 <a role="presentation" data-href="/files/commits/main">377 <a role="presentation" data-href="/files/commits/main">
376-<span class="lbl">commits</span>378+<span class="lbl">commits (g c)</span>
377 <span class="k">section</span>379 <span class="k">section</span>
378 </a>380 </a>
379 </li>381 </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)">
381 <a role="presentation" data-href="/files/refs">383 <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>
383 <span class="k">section</span>385 <span class="k">section</span>
384 </a>386 </a>
385 </li>387 </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)">
387 <a role="presentation" data-href="/files/merge-requests">389 <a role="presentation" data-href="/files/merge-requests">
388-<span class="lbl">merge requests</span>390+<span class="lbl">merge requests (g m)</span>
389 <span class="k">section</span>391 <span class="k">section</span>
390 </a>392 </a>
391 </li>393 </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)">
393 <a role="presentation" data-href="/files/releases">395 <a role="presentation" data-href="/files/releases">
394-<span class="lbl">releases</span>396+<span class="lbl">releases (g r)</span>
395 <span class="k">section</span>397 <span class="k">section</span>
396 </a>398 </a>
397 </li>399 </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)">
399 <a role="presentation" data-href="/files/actions">401 <a role="presentation" data-href="/files/actions">
400-<span class="lbl">actions</span>402+<span class="lbl">actions (g a)</span>
401 <span class="k">section</span>403 <span class="k">section</span>
402 </a>404 </a>
403 </li>405 </li>
@@ -450,7 +452,9 @@
450 k-filter from:body -> ermite.focusFilter();452 k-filter from:body -> ermite.focusFilter();
451 k-gh from:body -> ermite.go('.brand');453 k-gh from:body -> ermite.go('.brand');
452 k-s from:body -> ermite.tab('s');454 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');
454 k-c from:body -> ermite.tab('c');458 k-c from:body -> ermite.tab('c');
455 k-r from:body -> ermite.tab('r');459 k-r from:body -> ermite.tab('r');
456 k-m from:body -> ermite.tab('m');460 k-m from:body -> ermite.tab('m');
@@ -466,7 +470,6 @@
466 k-permalink from:body -> ermite.permalink();470 k-permalink from:body -> ermite.permalink();
467 k-merge from:body -> window.ermiteMR?.openMerge();471 k-merge from:body -> window.ermiteMR?.openMerge();
468 k-read from:body -> window.ermiteMR?.markRead();472 k-read from:body -> window.ermiteMR?.markRead();
469- k-gf from:body -> window.ermiteMR?.focusFiles();
470 k-keys from:body -> ermite.toggleKeys()">473 k-keys from:body -> ermite.toggleKeys()">
471 </div>474 </div>
472 </body>475 </body>
M internal/app/testdata/web/blob-text.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -248,14 +248,16 @@
248 </div>248 </div>
249 <div>249 <div>
250 <dt>250 <dt>
251-<kbd>s</kbd>251+<kbd>g s</kbd>
252-<kbd>t</kbd>252+<kbd>g f</kbd>
253-<kbd>c</kbd>253+<kbd>g c</kbd>
254-<kbd>r</kbd>254+<kbd>g b</kbd>
255-<kbd>m</kbd>255+<kbd>g m</kbd>
256-<kbd>i</kbd>256+<kbd>g i</kbd>
257+<kbd>g r</kbd>
258+<kbd>g a</kbd>
257 </dt>259 </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>
259 </div>261 </div>
260 <div>262 <div>
261 <dt>263 <dt>
@@ -354,45 +356,45 @@
354 <span class="k">go</span>356 <span class="k">go</span>
355 </a>357 </a>
356 </li>358 </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)">
358 <a role="presentation" data-href="/files">360 <a role="presentation" data-href="/files">
359-<span class="lbl">summary</span>361+<span class="lbl">summary (g s)</span>
360 <span class="k">section</span>362 <span class="k">section</span>
361 </a>363 </a>
362 </li>364 </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)">
364 <a role="presentation" data-href="/files/tree/main">366 <a role="presentation" data-href="/files/tree/main">
365-<span class="lbl">files</span>367+<span class="lbl">files (g f)</span>
366 <span class="k">section</span>368 <span class="k">section</span>
367 </a>369 </a>
368 </li>370 </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)">
370 <a role="presentation" data-href="/files/commits/main">372 <a role="presentation" data-href="/files/commits/main">
371-<span class="lbl">commits</span>373+<span class="lbl">commits (g c)</span>
372 <span class="k">section</span>374 <span class="k">section</span>
373 </a>375 </a>
374 </li>376 </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)">
376 <a role="presentation" data-href="/files/refs">378 <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>
378 <span class="k">section</span>380 <span class="k">section</span>
379 </a>381 </a>
380 </li>382 </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)">
382 <a role="presentation" data-href="/files/merge-requests">384 <a role="presentation" data-href="/files/merge-requests">
383-<span class="lbl">merge requests</span>385+<span class="lbl">merge requests (g m)</span>
384 <span class="k">section</span>386 <span class="k">section</span>
385 </a>387 </a>
386 </li>388 </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)">
388 <a role="presentation" data-href="/files/releases">390 <a role="presentation" data-href="/files/releases">
389-<span class="lbl">releases</span>391+<span class="lbl">releases (g r)</span>
390 <span class="k">section</span>392 <span class="k">section</span>
391 </a>393 </a>
392 </li>394 </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)">
394 <a role="presentation" data-href="/files/actions">396 <a role="presentation" data-href="/files/actions">
395-<span class="lbl">actions</span>397+<span class="lbl">actions (g a)</span>
396 <span class="k">section</span>398 <span class="k">section</span>
397 </a>399 </a>
398 </li>400 </li>
@@ -445,7 +447,9 @@
445 k-filter from:body -> ermite.focusFilter();447 k-filter from:body -> ermite.focusFilter();
446 k-gh from:body -> ermite.go('.brand');448 k-gh from:body -> ermite.go('.brand');
447 k-s from:body -> ermite.tab('s');449 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');
449 k-c from:body -> ermite.tab('c');453 k-c from:body -> ermite.tab('c');
450 k-r from:body -> ermite.tab('r');454 k-r from:body -> ermite.tab('r');
451 k-m from:body -> ermite.tab('m');455 k-m from:body -> ermite.tab('m');
@@ -461,7 +465,6 @@
461 k-permalink from:body -> ermite.permalink();465 k-permalink from:body -> ermite.permalink();
462 k-merge from:body -> window.ermiteMR?.openMerge();466 k-merge from:body -> window.ermiteMR?.openMerge();
463 k-read from:body -> window.ermiteMR?.markRead();467 k-read from:body -> window.ermiteMR?.markRead();
464- k-gf from:body -> window.ermiteMR?.focusFiles();
465 k-keys from:body -> ermite.toggleKeys()">468 k-keys from:body -> ermite.toggleKeys()">
466 </div>469 </div>
467 </body>470 </body>
M internal/app/testdata/web/blob-too-large.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -228,14 +228,16 @@
228 </div>228 </div>
229 <div>229 <div>
230 <dt>230 <dt>
231-<kbd>s</kbd>231+<kbd>g s</kbd>
232-<kbd>t</kbd>232+<kbd>g f</kbd>
233-<kbd>c</kbd>233+<kbd>g c</kbd>
234-<kbd>r</kbd>234+<kbd>g b</kbd>
235-<kbd>m</kbd>235+<kbd>g m</kbd>
236-<kbd>i</kbd>236+<kbd>g i</kbd>
237+<kbd>g r</kbd>
238+<kbd>g a</kbd>
237 </dt>239 </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>
239 </div>241 </div>
240 <div>242 <div>
241 <dt>243 <dt>
@@ -334,45 +336,45 @@
334 <span class="k">go</span>336 <span class="k">go</span>
335 </a>337 </a>
336 </li>338 </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)">
338 <a role="presentation" data-href="/files">340 <a role="presentation" data-href="/files">
339-<span class="lbl">summary</span>341+<span class="lbl">summary (g s)</span>
340 <span class="k">section</span>342 <span class="k">section</span>
341 </a>343 </a>
342 </li>344 </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)">
344 <a role="presentation" data-href="/files/tree/main">346 <a role="presentation" data-href="/files/tree/main">
345-<span class="lbl">files</span>347+<span class="lbl">files (g f)</span>
346 <span class="k">section</span>348 <span class="k">section</span>
347 </a>349 </a>
348 </li>350 </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)">
350 <a role="presentation" data-href="/files/commits/main">352 <a role="presentation" data-href="/files/commits/main">
351-<span class="lbl">commits</span>353+<span class="lbl">commits (g c)</span>
352 <span class="k">section</span>354 <span class="k">section</span>
353 </a>355 </a>
354 </li>356 </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)">
356 <a role="presentation" data-href="/files/refs">358 <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>
358 <span class="k">section</span>360 <span class="k">section</span>
359 </a>361 </a>
360 </li>362 </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)">
362 <a role="presentation" data-href="/files/merge-requests">364 <a role="presentation" data-href="/files/merge-requests">
363-<span class="lbl">merge requests</span>365+<span class="lbl">merge requests (g m)</span>
364 <span class="k">section</span>366 <span class="k">section</span>
365 </a>367 </a>
366 </li>368 </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)">
368 <a role="presentation" data-href="/files/releases">370 <a role="presentation" data-href="/files/releases">
369-<span class="lbl">releases</span>371+<span class="lbl">releases (g r)</span>
370 <span class="k">section</span>372 <span class="k">section</span>
371 </a>373 </a>
372 </li>374 </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)">
374 <a role="presentation" data-href="/files/actions">376 <a role="presentation" data-href="/files/actions">
375-<span class="lbl">actions</span>377+<span class="lbl">actions (g a)</span>
376 <span class="k">section</span>378 <span class="k">section</span>
377 </a>379 </a>
378 </li>380 </li>
@@ -425,7 +427,9 @@
425 k-filter from:body -> ermite.focusFilter();427 k-filter from:body -> ermite.focusFilter();
426 k-gh from:body -> ermite.go('.brand');428 k-gh from:body -> ermite.go('.brand');
427 k-s from:body -> ermite.tab('s');429 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');
429 k-c from:body -> ermite.tab('c');433 k-c from:body -> ermite.tab('c');
430 k-r from:body -> ermite.tab('r');434 k-r from:body -> ermite.tab('r');
431 k-m from:body -> ermite.tab('m');435 k-m from:body -> ermite.tab('m');
@@ -441,7 +445,6 @@
441 k-permalink from:body -> ermite.permalink();445 k-permalink from:body -> ermite.permalink();
442 k-merge from:body -> window.ermiteMR?.openMerge();446 k-merge from:body -> window.ermiteMR?.openMerge();
443 k-read from:body -> window.ermiteMR?.markRead();447 k-read from:body -> window.ermiteMR?.markRead();
444- k-gf from:body -> window.ermiteMR?.focusFiles();
445 k-keys from:body -> ermite.toggleKeys()">448 k-keys from:body -> ermite.toggleKeys()">
446 </div>449 </div>
447 </body>450 </body>
M internal/app/testdata/web/blob-trojan.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -295,14 +295,16 @@
295 </div>295 </div>
296 <div>296 <div>
297 <dt>297 <dt>
298-<kbd>s</kbd>298+<kbd>g s</kbd>
299-<kbd>t</kbd>299+<kbd>g f</kbd>
300-<kbd>c</kbd>300+<kbd>g c</kbd>
301-<kbd>r</kbd>301+<kbd>g b</kbd>
302-<kbd>m</kbd>302+<kbd>g m</kbd>
303-<kbd>i</kbd>303+<kbd>g i</kbd>
304+<kbd>g r</kbd>
305+<kbd>g a</kbd>
304 </dt>306 </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>
306 </div>308 </div>
307 <div>309 <div>
308 <dt>310 <dt>
@@ -401,45 +403,45 @@
401 <span class="k">go</span>403 <span class="k">go</span>
402 </a>404 </a>
403 </li>405 </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)">
405 <a role="presentation" data-href="/files">407 <a role="presentation" data-href="/files">
406-<span class="lbl">summary</span>408+<span class="lbl">summary (g s)</span>
407 <span class="k">section</span>409 <span class="k">section</span>
408 </a>410 </a>
409 </li>411 </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)">
411 <a role="presentation" data-href="/files/tree/main">413 <a role="presentation" data-href="/files/tree/main">
412-<span class="lbl">files</span>414+<span class="lbl">files (g f)</span>
413 <span class="k">section</span>415 <span class="k">section</span>
414 </a>416 </a>
415 </li>417 </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)">
417 <a role="presentation" data-href="/files/commits/main">419 <a role="presentation" data-href="/files/commits/main">
418-<span class="lbl">commits</span>420+<span class="lbl">commits (g c)</span>
419 <span class="k">section</span>421 <span class="k">section</span>
420 </a>422 </a>
421 </li>423 </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)">
423 <a role="presentation" data-href="/files/refs">425 <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>
425 <span class="k">section</span>427 <span class="k">section</span>
426 </a>428 </a>
427 </li>429 </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)">
429 <a role="presentation" data-href="/files/merge-requests">431 <a role="presentation" data-href="/files/merge-requests">
430-<span class="lbl">merge requests</span>432+<span class="lbl">merge requests (g m)</span>
431 <span class="k">section</span>433 <span class="k">section</span>
432 </a>434 </a>
433 </li>435 </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)">
435 <a role="presentation" data-href="/files/releases">437 <a role="presentation" data-href="/files/releases">
436-<span class="lbl">releases</span>438+<span class="lbl">releases (g r)</span>
437 <span class="k">section</span>439 <span class="k">section</span>
438 </a>440 </a>
439 </li>441 </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)">
441 <a role="presentation" data-href="/files/actions">443 <a role="presentation" data-href="/files/actions">
442-<span class="lbl">actions</span>444+<span class="lbl">actions (g a)</span>
443 <span class="k">section</span>445 <span class="k">section</span>
444 </a>446 </a>
445 </li>447 </li>
@@ -492,7 +494,9 @@
492 k-filter from:body -> ermite.focusFilter();494 k-filter from:body -> ermite.focusFilter();
493 k-gh from:body -> ermite.go('.brand');495 k-gh from:body -> ermite.go('.brand');
494 k-s from:body -> ermite.tab('s');496 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');
496 k-c from:body -> ermite.tab('c');500 k-c from:body -> ermite.tab('c');
497 k-r from:body -> ermite.tab('r');501 k-r from:body -> ermite.tab('r');
498 k-m from:body -> ermite.tab('m');502 k-m from:body -> ermite.tab('m');
@@ -508,7 +512,6 @@
508 k-permalink from:body -> ermite.permalink();512 k-permalink from:body -> ermite.permalink();
509 k-merge from:body -> window.ermiteMR?.openMerge();513 k-merge from:body -> window.ermiteMR?.openMerge();
510 k-read from:body -> window.ermiteMR?.markRead();514 k-read from:body -> window.ermiteMR?.markRead();
511- k-gf from:body -> window.ermiteMR?.focusFiles();
512 k-keys from:body -> ermite.toggleKeys()">515 k-keys from:body -> ermite.toggleKeys()">
513 </div>516 </div>
514 </body>517 </body>
M internal/app/testdata/web/commit-big.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -280,14 +280,16 @@
280 </div>280 </div>
281 <div>281 <div>
282 <dt>282 <dt>
283-<kbd>s</kbd>283+<kbd>g s</kbd>
284-<kbd>t</kbd>284+<kbd>g f</kbd>
285-<kbd>c</kbd>285+<kbd>g c</kbd>
286-<kbd>r</kbd>286+<kbd>g b</kbd>
287-<kbd>m</kbd>287+<kbd>g m</kbd>
288-<kbd>i</kbd>288+<kbd>g i</kbd>
289+<kbd>g r</kbd>
290+<kbd>g a</kbd>
289 </dt>291 </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>
291 </div>293 </div>
292 <div>294 <div>
293 <dt>295 <dt>
@@ -386,45 +388,45 @@
386 <span class="k">go</span>388 <span class="k">go</span>
387 </a>389 </a>
388 </li>390 </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)">
390 <a role="presentation" data-href="/diffs">392 <a role="presentation" data-href="/diffs">
391-<span class="lbl">summary</span>393+<span class="lbl">summary (g s)</span>
392 <span class="k">section</span>394 <span class="k">section</span>
393 </a>395 </a>
394 </li>396 </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)">
396 <a role="presentation" data-href="/diffs/tree/main">398 <a role="presentation" data-href="/diffs/tree/main">
397-<span class="lbl">files</span>399+<span class="lbl">files (g f)</span>
398 <span class="k">section</span>400 <span class="k">section</span>
399 </a>401 </a>
400 </li>402 </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)">
402 <a role="presentation" data-href="/diffs/commits/main">404 <a role="presentation" data-href="/diffs/commits/main">
403-<span class="lbl">commits</span>405+<span class="lbl">commits (g c)</span>
404 <span class="k">section</span>406 <span class="k">section</span>
405 </a>407 </a>
406 </li>408 </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)">
408 <a role="presentation" data-href="/diffs/refs">410 <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>
410 <span class="k">section</span>412 <span class="k">section</span>
411 </a>413 </a>
412 </li>414 </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)">
414 <a role="presentation" data-href="/diffs/merge-requests">416 <a role="presentation" data-href="/diffs/merge-requests">
415-<span class="lbl">merge requests</span>417+<span class="lbl">merge requests (g m)</span>
416 <span class="k">section</span>418 <span class="k">section</span>
417 </a>419 </a>
418 </li>420 </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)">
420 <a role="presentation" data-href="/diffs/releases">422 <a role="presentation" data-href="/diffs/releases">
421-<span class="lbl">releases</span>423+<span class="lbl">releases (g r)</span>
422 <span class="k">section</span>424 <span class="k">section</span>
423 </a>425 </a>
424 </li>426 </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)">
426 <a role="presentation" data-href="/diffs/actions">428 <a role="presentation" data-href="/diffs/actions">
427-<span class="lbl">actions</span>429+<span class="lbl">actions (g a)</span>
428 <span class="k">section</span>430 <span class="k">section</span>
429 </a>431 </a>
430 </li>432 </li>
@@ -477,7 +479,9 @@
477 k-filter from:body -> ermite.focusFilter();479 k-filter from:body -> ermite.focusFilter();
478 k-gh from:body -> ermite.go('.brand');480 k-gh from:body -> ermite.go('.brand');
479 k-s from:body -> ermite.tab('s');481 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');
481 k-c from:body -> ermite.tab('c');485 k-c from:body -> ermite.tab('c');
482 k-r from:body -> ermite.tab('r');486 k-r from:body -> ermite.tab('r');
483 k-m from:body -> ermite.tab('m');487 k-m from:body -> ermite.tab('m');
@@ -493,7 +497,6 @@
493 k-permalink from:body -> ermite.permalink();497 k-permalink from:body -> ermite.permalink();
494 k-merge from:body -> window.ermiteMR?.openMerge();498 k-merge from:body -> window.ermiteMR?.openMerge();
495 k-read from:body -> window.ermiteMR?.markRead();499 k-read from:body -> window.ermiteMR?.markRead();
496- k-gf from:body -> window.ermiteMR?.focusFiles();
497 k-keys from:body -> ermite.toggleKeys()">500 k-keys from:body -> ermite.toggleKeys()">
498 </div>501 </div>
499 </body>502 </body>
M internal/app/testdata/web/commit-binary.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -271,14 +271,16 @@
271 </div>271 </div>
272 <div>272 <div>
273 <dt>273 <dt>
274-<kbd>s</kbd>274+<kbd>g s</kbd>
275-<kbd>t</kbd>275+<kbd>g f</kbd>
276-<kbd>c</kbd>276+<kbd>g c</kbd>
277-<kbd>r</kbd>277+<kbd>g b</kbd>
278-<kbd>m</kbd>278+<kbd>g m</kbd>
279-<kbd>i</kbd>279+<kbd>g i</kbd>
280+<kbd>g r</kbd>
281+<kbd>g a</kbd>
280 </dt>282 </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>
282 </div>284 </div>
283 <div>285 <div>
284 <dt>286 <dt>
@@ -377,45 +379,45 @@
377 <span class="k">go</span>379 <span class="k">go</span>
378 </a>380 </a>
379 </li>381 </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)">
381 <a role="presentation" data-href="/diffs">383 <a role="presentation" data-href="/diffs">
382-<span class="lbl">summary</span>384+<span class="lbl">summary (g s)</span>
383 <span class="k">section</span>385 <span class="k">section</span>
384 </a>386 </a>
385 </li>387 </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)">
387 <a role="presentation" data-href="/diffs/tree/main">389 <a role="presentation" data-href="/diffs/tree/main">
388-<span class="lbl">files</span>390+<span class="lbl">files (g f)</span>
389 <span class="k">section</span>391 <span class="k">section</span>
390 </a>392 </a>
391 </li>393 </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)">
393 <a role="presentation" data-href="/diffs/commits/main">395 <a role="presentation" data-href="/diffs/commits/main">
394-<span class="lbl">commits</span>396+<span class="lbl">commits (g c)</span>
395 <span class="k">section</span>397 <span class="k">section</span>
396 </a>398 </a>
397 </li>399 </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)">
399 <a role="presentation" data-href="/diffs/refs">401 <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>
401 <span class="k">section</span>403 <span class="k">section</span>
402 </a>404 </a>
403 </li>405 </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)">
405 <a role="presentation" data-href="/diffs/merge-requests">407 <a role="presentation" data-href="/diffs/merge-requests">
406-<span class="lbl">merge requests</span>408+<span class="lbl">merge requests (g m)</span>
407 <span class="k">section</span>409 <span class="k">section</span>
408 </a>410 </a>
409 </li>411 </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)">
411 <a role="presentation" data-href="/diffs/releases">413 <a role="presentation" data-href="/diffs/releases">
412-<span class="lbl">releases</span>414+<span class="lbl">releases (g r)</span>
413 <span class="k">section</span>415 <span class="k">section</span>
414 </a>416 </a>
415 </li>417 </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)">
417 <a role="presentation" data-href="/diffs/actions">419 <a role="presentation" data-href="/diffs/actions">
418-<span class="lbl">actions</span>420+<span class="lbl">actions (g a)</span>
419 <span class="k">section</span>421 <span class="k">section</span>
420 </a>422 </a>
421 </li>423 </li>
@@ -468,7 +470,9 @@
468 k-filter from:body -> ermite.focusFilter();470 k-filter from:body -> ermite.focusFilter();
469 k-gh from:body -> ermite.go('.brand');471 k-gh from:body -> ermite.go('.brand');
470 k-s from:body -> ermite.tab('s');472 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');
472 k-c from:body -> ermite.tab('c');476 k-c from:body -> ermite.tab('c');
473 k-r from:body -> ermite.tab('r');477 k-r from:body -> ermite.tab('r');
474 k-m from:body -> ermite.tab('m');478 k-m from:body -> ermite.tab('m');
@@ -484,7 +488,6 @@
484 k-permalink from:body -> ermite.permalink();488 k-permalink from:body -> ermite.permalink();
485 k-merge from:body -> window.ermiteMR?.openMerge();489 k-merge from:body -> window.ermiteMR?.openMerge();
486 k-read from:body -> window.ermiteMR?.markRead();490 k-read from:body -> window.ermiteMR?.markRead();
487- k-gf from:body -> window.ermiteMR?.focusFiles();
488 k-keys from:body -> ermite.toggleKeys()">491 k-keys from:body -> ermite.toggleKeys()">
489 </div>492 </div>
490 </body>493 </body>
M internal/app/testdata/web/commit-merge.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -313,14 +313,16 @@
313 </div>313 </div>
314 <div>314 <div>
315 <dt>315 <dt>
316-<kbd>s</kbd>316+<kbd>g s</kbd>
317-<kbd>t</kbd>317+<kbd>g f</kbd>
318-<kbd>c</kbd>318+<kbd>g c</kbd>
319-<kbd>r</kbd>319+<kbd>g b</kbd>
320-<kbd>m</kbd>320+<kbd>g m</kbd>
321-<kbd>i</kbd>321+<kbd>g i</kbd>
322+<kbd>g r</kbd>
323+<kbd>g a</kbd>
322 </dt>324 </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>
324 </div>326 </div>
325 <div>327 <div>
326 <dt>328 <dt>
@@ -419,45 +421,45 @@
419 <span class="k">go</span>421 <span class="k">go</span>
420 </a>422 </a>
421 </li>423 </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)">
423 <a role="presentation" data-href="/diffs">425 <a role="presentation" data-href="/diffs">
424-<span class="lbl">summary</span>426+<span class="lbl">summary (g s)</span>
425 <span class="k">section</span>427 <span class="k">section</span>
426 </a>428 </a>
427 </li>429 </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)">
429 <a role="presentation" data-href="/diffs/tree/main">431 <a role="presentation" data-href="/diffs/tree/main">
430-<span class="lbl">files</span>432+<span class="lbl">files (g f)</span>
431 <span class="k">section</span>433 <span class="k">section</span>
432 </a>434 </a>
433 </li>435 </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)">
435 <a role="presentation" data-href="/diffs/commits/main">437 <a role="presentation" data-href="/diffs/commits/main">
436-<span class="lbl">commits</span>438+<span class="lbl">commits (g c)</span>
437 <span class="k">section</span>439 <span class="k">section</span>
438 </a>440 </a>
439 </li>441 </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)">
441 <a role="presentation" data-href="/diffs/refs">443 <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>
443 <span class="k">section</span>445 <span class="k">section</span>
444 </a>446 </a>
445 </li>447 </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)">
447 <a role="presentation" data-href="/diffs/merge-requests">449 <a role="presentation" data-href="/diffs/merge-requests">
448-<span class="lbl">merge requests</span>450+<span class="lbl">merge requests (g m)</span>
449 <span class="k">section</span>451 <span class="k">section</span>
450 </a>452 </a>
451 </li>453 </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)">
453 <a role="presentation" data-href="/diffs/releases">455 <a role="presentation" data-href="/diffs/releases">
454-<span class="lbl">releases</span>456+<span class="lbl">releases (g r)</span>
455 <span class="k">section</span>457 <span class="k">section</span>
456 </a>458 </a>
457 </li>459 </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)">
459 <a role="presentation" data-href="/diffs/actions">461 <a role="presentation" data-href="/diffs/actions">
460-<span class="lbl">actions</span>462+<span class="lbl">actions (g a)</span>
461 <span class="k">section</span>463 <span class="k">section</span>
462 </a>464 </a>
463 </li>465 </li>
@@ -510,7 +512,9 @@
510 k-filter from:body -> ermite.focusFilter();512 k-filter from:body -> ermite.focusFilter();
511 k-gh from:body -> ermite.go('.brand');513 k-gh from:body -> ermite.go('.brand');
512 k-s from:body -> ermite.tab('s');514 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');
514 k-c from:body -> ermite.tab('c');518 k-c from:body -> ermite.tab('c');
515 k-r from:body -> ermite.tab('r');519 k-r from:body -> ermite.tab('r');
516 k-m from:body -> ermite.tab('m');520 k-m from:body -> ermite.tab('m');
@@ -526,7 +530,6 @@
526 k-permalink from:body -> ermite.permalink();530 k-permalink from:body -> ermite.permalink();
527 k-merge from:body -> window.ermiteMR?.openMerge();531 k-merge from:body -> window.ermiteMR?.openMerge();
528 k-read from:body -> window.ermiteMR?.markRead();532 k-read from:body -> window.ermiteMR?.markRead();
529- k-gf from:body -> window.ermiteMR?.focusFiles();
530 k-keys from:body -> ermite.toggleKeys()">533 k-keys from:body -> ermite.toggleKeys()">
531 </div>534 </div>
532 </body>535 </body>
M internal/app/testdata/web/commit-modify-split.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -436,14 +436,16 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
436 </div>436 </div>
437 <div>437 <div>
438 <dt>438 <dt>
439-<kbd>s</kbd>439+<kbd>g s</kbd>
440-<kbd>t</kbd>440+<kbd>g f</kbd>
441-<kbd>c</kbd>441+<kbd>g c</kbd>
442-<kbd>r</kbd>442+<kbd>g b</kbd>
443-<kbd>m</kbd>443+<kbd>g m</kbd>
444-<kbd>i</kbd>444+<kbd>g i</kbd>
445+<kbd>g r</kbd>
446+<kbd>g a</kbd>
445 </dt>447 </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>
447 </div>449 </div>
448 <div>450 <div>
449 <dt>451 <dt>
@@ -542,45 +544,45 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
542 <span class="k">go</span>544 <span class="k">go</span>
543 </a>545 </a>
544 </li>546 </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)">
546 <a role="presentation" data-href="/diffs">548 <a role="presentation" data-href="/diffs">
547-<span class="lbl">summary</span>549+<span class="lbl">summary (g s)</span>
548 <span class="k">section</span>550 <span class="k">section</span>
549 </a>551 </a>
550 </li>552 </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)">
552 <a role="presentation" data-href="/diffs/tree/main">554 <a role="presentation" data-href="/diffs/tree/main">
553-<span class="lbl">files</span>555+<span class="lbl">files (g f)</span>
554 <span class="k">section</span>556 <span class="k">section</span>
555 </a>557 </a>
556 </li>558 </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)">
558 <a role="presentation" data-href="/diffs/commits/main">560 <a role="presentation" data-href="/diffs/commits/main">
559-<span class="lbl">commits</span>561+<span class="lbl">commits (g c)</span>
560 <span class="k">section</span>562 <span class="k">section</span>
561 </a>563 </a>
562 </li>564 </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)">
564 <a role="presentation" data-href="/diffs/refs">566 <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>
566 <span class="k">section</span>568 <span class="k">section</span>
567 </a>569 </a>
568 </li>570 </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)">
570 <a role="presentation" data-href="/diffs/merge-requests">572 <a role="presentation" data-href="/diffs/merge-requests">
571-<span class="lbl">merge requests</span>573+<span class="lbl">merge requests (g m)</span>
572 <span class="k">section</span>574 <span class="k">section</span>
573 </a>575 </a>
574 </li>576 </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)">
576 <a role="presentation" data-href="/diffs/releases">578 <a role="presentation" data-href="/diffs/releases">
577-<span class="lbl">releases</span>579+<span class="lbl">releases (g r)</span>
578 <span class="k">section</span>580 <span class="k">section</span>
579 </a>581 </a>
580 </li>582 </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)">
582 <a role="presentation" data-href="/diffs/actions">584 <a role="presentation" data-href="/diffs/actions">
583-<span class="lbl">actions</span>585+<span class="lbl">actions (g a)</span>
584 <span class="k">section</span>586 <span class="k">section</span>
585 </a>587 </a>
586 </li>588 </li>
@@ -633,7 +635,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
633 k-filter from:body -> ermite.focusFilter();635 k-filter from:body -> ermite.focusFilter();
634 k-gh from:body -> ermite.go('.brand');636 k-gh from:body -> ermite.go('.brand');
635 k-s from:body -> ermite.tab('s');637 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');
637 k-c from:body -> ermite.tab('c');641 k-c from:body -> ermite.tab('c');
638 k-r from:body -> ermite.tab('r');642 k-r from:body -> ermite.tab('r');
639 k-m from:body -> ermite.tab('m');643 k-m from:body -> ermite.tab('m');
@@ -649,7 +653,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
649 k-permalink from:body -> ermite.permalink();653 k-permalink from:body -> ermite.permalink();
650 k-merge from:body -> window.ermiteMR?.openMerge();654 k-merge from:body -> window.ermiteMR?.openMerge();
651 k-read from:body -> window.ermiteMR?.markRead();655 k-read from:body -> window.ermiteMR?.markRead();
652- k-gf from:body -> window.ermiteMR?.focusFiles();
653 k-keys from:body -> ermite.toggleKeys()">656 k-keys from:body -> ermite.toggleKeys()">
654 </div>657 </div>
655 </body>658 </body>
M internal/app/testdata/web/commit-modify.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -436,14 +436,16 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
436 </div>436 </div>
437 <div>437 <div>
438 <dt>438 <dt>
439-<kbd>s</kbd>439+<kbd>g s</kbd>
440-<kbd>t</kbd>440+<kbd>g f</kbd>
441-<kbd>c</kbd>441+<kbd>g c</kbd>
442-<kbd>r</kbd>442+<kbd>g b</kbd>
443-<kbd>m</kbd>443+<kbd>g m</kbd>
444-<kbd>i</kbd>444+<kbd>g i</kbd>
445+<kbd>g r</kbd>
446+<kbd>g a</kbd>
445 </dt>447 </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>
447 </div>449 </div>
448 <div>450 <div>
449 <dt>451 <dt>
@@ -542,45 +544,45 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
542 <span class="k">go</span>544 <span class="k">go</span>
543 </a>545 </a>
544 </li>546 </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)">
546 <a role="presentation" data-href="/diffs">548 <a role="presentation" data-href="/diffs">
547-<span class="lbl">summary</span>549+<span class="lbl">summary (g s)</span>
548 <span class="k">section</span>550 <span class="k">section</span>
549 </a>551 </a>
550 </li>552 </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)">
552 <a role="presentation" data-href="/diffs/tree/main">554 <a role="presentation" data-href="/diffs/tree/main">
553-<span class="lbl">files</span>555+<span class="lbl">files (g f)</span>
554 <span class="k">section</span>556 <span class="k">section</span>
555 </a>557 </a>
556 </li>558 </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)">
558 <a role="presentation" data-href="/diffs/commits/main">560 <a role="presentation" data-href="/diffs/commits/main">
559-<span class="lbl">commits</span>561+<span class="lbl">commits (g c)</span>
560 <span class="k">section</span>562 <span class="k">section</span>
561 </a>563 </a>
562 </li>564 </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)">
564 <a role="presentation" data-href="/diffs/refs">566 <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>
566 <span class="k">section</span>568 <span class="k">section</span>
567 </a>569 </a>
568 </li>570 </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)">
570 <a role="presentation" data-href="/diffs/merge-requests">572 <a role="presentation" data-href="/diffs/merge-requests">
571-<span class="lbl">merge requests</span>573+<span class="lbl">merge requests (g m)</span>
572 <span class="k">section</span>574 <span class="k">section</span>
573 </a>575 </a>
574 </li>576 </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)">
576 <a role="presentation" data-href="/diffs/releases">578 <a role="presentation" data-href="/diffs/releases">
577-<span class="lbl">releases</span>579+<span class="lbl">releases (g r)</span>
578 <span class="k">section</span>580 <span class="k">section</span>
579 </a>581 </a>
580 </li>582 </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)">
582 <a role="presentation" data-href="/diffs/actions">584 <a role="presentation" data-href="/diffs/actions">
583-<span class="lbl">actions</span>585+<span class="lbl">actions (g a)</span>
584 <span class="k">section</span>586 <span class="k">section</span>
585 </a>587 </a>
586 </li>588 </li>
@@ -633,7 +635,9 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
633 k-filter from:body -> ermite.focusFilter();635 k-filter from:body -> ermite.focusFilter();
634 k-gh from:body -> ermite.go('.brand');636 k-gh from:body -> ermite.go('.brand');
635 k-s from:body -> ermite.tab('s');637 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');
637 k-c from:body -> ermite.tab('c');641 k-c from:body -> ermite.tab('c');
638 k-r from:body -> ermite.tab('r');642 k-r from:body -> ermite.tab('r');
639 k-m from:body -> ermite.tab('m');643 k-m from:body -> ermite.tab('m');
@@ -649,7 +653,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
649 k-permalink from:body -> ermite.permalink();653 k-permalink from:body -> ermite.permalink();
650 k-merge from:body -> window.ermiteMR?.openMerge();654 k-merge from:body -> window.ermiteMR?.openMerge();
651 k-read from:body -> window.ermiteMR?.markRead();655 k-read from:body -> window.ermiteMR?.markRead();
652- k-gf from:body -> window.ermiteMR?.focusFiles();
653 k-keys from:body -> ermite.toggleKeys()">656 k-keys from:body -> ermite.toggleKeys()">
654 </div>657 </div>
655 </body>658 </body>
M internal/app/testdata/web/commit-rename.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -340,14 +340,16 @@
340 </div>340 </div>
341 <div>341 <div>
342 <dt>342 <dt>
343-<kbd>s</kbd>343+<kbd>g s</kbd>
344-<kbd>t</kbd>344+<kbd>g f</kbd>
345-<kbd>c</kbd>345+<kbd>g c</kbd>
346-<kbd>r</kbd>346+<kbd>g b</kbd>
347-<kbd>m</kbd>347+<kbd>g m</kbd>
348-<kbd>i</kbd>348+<kbd>g i</kbd>
349+<kbd>g r</kbd>
350+<kbd>g a</kbd>
349 </dt>351 </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>
351 </div>353 </div>
352 <div>354 <div>
353 <dt>355 <dt>
@@ -446,45 +448,45 @@
446 <span class="k">go</span>448 <span class="k">go</span>
447 </a>449 </a>
448 </li>450 </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)">
450 <a role="presentation" data-href="/diffs">452 <a role="presentation" data-href="/diffs">
451-<span class="lbl">summary</span>453+<span class="lbl">summary (g s)</span>
452 <span class="k">section</span>454 <span class="k">section</span>
453 </a>455 </a>
454 </li>456 </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)">
456 <a role="presentation" data-href="/diffs/tree/main">458 <a role="presentation" data-href="/diffs/tree/main">
457-<span class="lbl">files</span>459+<span class="lbl">files (g f)</span>
458 <span class="k">section</span>460 <span class="k">section</span>
459 </a>461 </a>
460 </li>462 </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)">
462 <a role="presentation" data-href="/diffs/commits/main">464 <a role="presentation" data-href="/diffs/commits/main">
463-<span class="lbl">commits</span>465+<span class="lbl">commits (g c)</span>
464 <span class="k">section</span>466 <span class="k">section</span>
465 </a>467 </a>
466 </li>468 </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)">
468 <a role="presentation" data-href="/diffs/refs">470 <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>
470 <span class="k">section</span>472 <span class="k">section</span>
471 </a>473 </a>
472 </li>474 </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)">
474 <a role="presentation" data-href="/diffs/merge-requests">476 <a role="presentation" data-href="/diffs/merge-requests">
475-<span class="lbl">merge requests</span>477+<span class="lbl">merge requests (g m)</span>
476 <span class="k">section</span>478 <span class="k">section</span>
477 </a>479 </a>
478 </li>480 </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)">
480 <a role="presentation" data-href="/diffs/releases">482 <a role="presentation" data-href="/diffs/releases">
481-<span class="lbl">releases</span>483+<span class="lbl">releases (g r)</span>
482 <span class="k">section</span>484 <span class="k">section</span>
483 </a>485 </a>
484 </li>486 </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)">
486 <a role="presentation" data-href="/diffs/actions">488 <a role="presentation" data-href="/diffs/actions">
487-<span class="lbl">actions</span>489+<span class="lbl">actions (g a)</span>
488 <span class="k">section</span>490 <span class="k">section</span>
489 </a>491 </a>
490 </li>492 </li>
@@ -537,7 +539,9 @@
537 k-filter from:body -> ermite.focusFilter();539 k-filter from:body -> ermite.focusFilter();
538 k-gh from:body -> ermite.go('.brand');540 k-gh from:body -> ermite.go('.brand');
539 k-s from:body -> ermite.tab('s');541 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');
541 k-c from:body -> ermite.tab('c');545 k-c from:body -> ermite.tab('c');
542 k-r from:body -> ermite.tab('r');546 k-r from:body -> ermite.tab('r');
543 k-m from:body -> ermite.tab('m');547 k-m from:body -> ermite.tab('m');
@@ -553,7 +557,6 @@
553 k-permalink from:body -> ermite.permalink();557 k-permalink from:body -> ermite.permalink();
554 k-merge from:body -> window.ermiteMR?.openMerge();558 k-merge from:body -> window.ermiteMR?.openMerge();
555 k-read from:body -> window.ermiteMR?.markRead();559 k-read from:body -> window.ermiteMR?.markRead();
556- k-gf from:body -> window.ermiteMR?.focusFiles();
557 k-keys from:body -> ermite.toggleKeys()">560 k-keys from:body -> ermite.toggleKeys()">
558 </div>561 </div>
559 </body>562 </body>
M internal/app/testdata/web/commit-root.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -659,14 +659,16 @@
659 </div>659 </div>
660 <div>660 <div>
661 <dt>661 <dt>
662-<kbd>s</kbd>662+<kbd>g s</kbd>
663-<kbd>t</kbd>663+<kbd>g f</kbd>
664-<kbd>c</kbd>664+<kbd>g c</kbd>
665-<kbd>r</kbd>665+<kbd>g b</kbd>
666-<kbd>m</kbd>666+<kbd>g m</kbd>
667-<kbd>i</kbd>667+<kbd>g i</kbd>
668+<kbd>g r</kbd>
669+<kbd>g a</kbd>
668 </dt>670 </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>
670 </div>672 </div>
671 <div>673 <div>
672 <dt>674 <dt>
@@ -765,45 +767,45 @@
765 <span class="k">go</span>767 <span class="k">go</span>
766 </a>768 </a>
767 </li>769 </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)">
769 <a role="presentation" data-href="/diffs">771 <a role="presentation" data-href="/diffs">
770-<span class="lbl">summary</span>772+<span class="lbl">summary (g s)</span>
771 <span class="k">section</span>773 <span class="k">section</span>
772 </a>774 </a>
773 </li>775 </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)">
775 <a role="presentation" data-href="/diffs/tree/main">777 <a role="presentation" data-href="/diffs/tree/main">
776-<span class="lbl">files</span>778+<span class="lbl">files (g f)</span>
777 <span class="k">section</span>779 <span class="k">section</span>
778 </a>780 </a>
779 </li>781 </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)">
781 <a role="presentation" data-href="/diffs/commits/main">783 <a role="presentation" data-href="/diffs/commits/main">
782-<span class="lbl">commits</span>784+<span class="lbl">commits (g c)</span>
783 <span class="k">section</span>785 <span class="k">section</span>
784 </a>786 </a>
785 </li>787 </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)">
787 <a role="presentation" data-href="/diffs/refs">789 <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>
789 <span class="k">section</span>791 <span class="k">section</span>
790 </a>792 </a>
791 </li>793 </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)">
793 <a role="presentation" data-href="/diffs/merge-requests">795 <a role="presentation" data-href="/diffs/merge-requests">
794-<span class="lbl">merge requests</span>796+<span class="lbl">merge requests (g m)</span>
795 <span class="k">section</span>797 <span class="k">section</span>
796 </a>798 </a>
797 </li>799 </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)">
799 <a role="presentation" data-href="/diffs/releases">801 <a role="presentation" data-href="/diffs/releases">
800-<span class="lbl">releases</span>802+<span class="lbl">releases (g r)</span>
801 <span class="k">section</span>803 <span class="k">section</span>
802 </a>804 </a>
803 </li>805 </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)">
805 <a role="presentation" data-href="/diffs/actions">807 <a role="presentation" data-href="/diffs/actions">
806-<span class="lbl">actions</span>808+<span class="lbl">actions (g a)</span>
807 <span class="k">section</span>809 <span class="k">section</span>
808 </a>810 </a>
809 </li>811 </li>
@@ -856,7 +858,9 @@
856 k-filter from:body -> ermite.focusFilter();858 k-filter from:body -> ermite.focusFilter();
857 k-gh from:body -> ermite.go('.brand');859 k-gh from:body -> ermite.go('.brand');
858 k-s from:body -> ermite.tab('s');860 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');
860 k-c from:body -> ermite.tab('c');864 k-c from:body -> ermite.tab('c');
861 k-r from:body -> ermite.tab('r');865 k-r from:body -> ermite.tab('r');
862 k-m from:body -> ermite.tab('m');866 k-m from:body -> ermite.tab('m');
@@ -872,7 +876,6 @@
872 k-permalink from:body -> ermite.permalink();876 k-permalink from:body -> ermite.permalink();
873 k-merge from:body -> window.ermiteMR?.openMerge();877 k-merge from:body -> window.ermiteMR?.openMerge();
874 k-read from:body -> window.ermiteMR?.markRead();878 k-read from:body -> window.ermiteMR?.markRead();
875- k-gf from:body -> window.ermiteMR?.focusFiles();
876 k-keys from:body -> ermite.toggleKeys()">879 k-keys from:body -> ermite.toggleKeys()">
877 </div>880 </div>
878 </body>881 </body>
M internal/app/testdata/web/commit-trojan.html +33 -30
@@ -72,31 +72,31 @@
72 <nav class="tabs" aria-label="Sections">72 <nav class="tabs" aria-label="Sections">
73 <ul>73 <ul>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 </ul>102 </ul>
@@ -488,14 +488,16 @@
488 </div>488 </div>
489 <div>489 <div>
490 <dt>490 <dt>
491-<kbd>s</kbd>491+<kbd>g s</kbd>
492-<kbd>t</kbd>492+<kbd>g f</kbd>
493-<kbd>c</kbd>493+<kbd>g c</kbd>
494-<kbd>r</kbd>494+<kbd>g b</kbd>
495-<kbd>m</kbd>495+<kbd>g m</kbd>
496-<kbd>i</kbd>496+<kbd>g i</kbd>
497+<kbd>g r</kbd>
498+<kbd>g a</kbd>
497 </dt>499 </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>
499 </div>501 </div>
500 <div>502 <div>
501 <dt>503 <dt>
@@ -594,45 +596,45 @@
594 <span class="k">go</span>596 <span class="k">go</span>
595 </a>597 </a>
596 </li>598 </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)">
598 <a role="presentation" data-href="/diffs">600 <a role="presentation" data-href="/diffs">
599-<span class="lbl">summary</span>601+<span class="lbl">summary (g s)</span>
600 <span class="k">section</span>602 <span class="k">section</span>
601 </a>603 </a>
602 </li>604 </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)">
604 <a role="presentation" data-href="/diffs/tree/main">606 <a role="presentation" data-href="/diffs/tree/main">
605-<span class="lbl">files</span>607+<span class="lbl">files (g f)</span>
606 <span class="k">section</span>608 <span class="k">section</span>
607 </a>609 </a>
608 </li>610 </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)">
610 <a role="presentation" data-href="/diffs/commits/main">612 <a role="presentation" data-href="/diffs/commits/main">
611-<span class="lbl">commits</span>613+<span class="lbl">commits (g c)</span>
612 <span class="k">section</span>614 <span class="k">section</span>
613 </a>615 </a>
614 </li>616 </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)">
616 <a role="presentation" data-href="/diffs/refs">618 <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>
618 <span class="k">section</span>620 <span class="k">section</span>
619 </a>621 </a>
620 </li>622 </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)">
622 <a role="presentation" data-href="/diffs/merge-requests">624 <a role="presentation" data-href="/diffs/merge-requests">
623-<span class="lbl">merge requests</span>625+<span class="lbl">merge requests (g m)</span>
624 <span class="k">section</span>626 <span class="k">section</span>
625 </a>627 </a>
626 </li>628 </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)">
628 <a role="presentation" data-href="/diffs/releases">630 <a role="presentation" data-href="/diffs/releases">
629-<span class="lbl">releases</span>631+<span class="lbl">releases (g r)</span>
630 <span class="k">section</span>632 <span class="k">section</span>
631 </a>633 </a>
632 </li>634 </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)">
634 <a role="presentation" data-href="/diffs/actions">636 <a role="presentation" data-href="/diffs/actions">
635-<span class="lbl">actions</span>637+<span class="lbl">actions (g a)</span>
636 <span class="k">section</span>638 <span class="k">section</span>
637 </a>639 </a>
638 </li>640 </li>
@@ -685,7 +687,9 @@
685 k-filter from:body -> ermite.focusFilter();687 k-filter from:body -> ermite.focusFilter();
686 k-gh from:body -> ermite.go('.brand');688 k-gh from:body -> ermite.go('.brand');
687 k-s from:body -> ermite.tab('s');689 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');
689 k-c from:body -> ermite.tab('c');693 k-c from:body -> ermite.tab('c');
690 k-r from:body -> ermite.tab('r');694 k-r from:body -> ermite.tab('r');
691 k-m from:body -> ermite.tab('m');695 k-m from:body -> ermite.tab('m');
@@ -701,7 +705,6 @@
701 k-permalink from:body -> ermite.permalink();705 k-permalink from:body -> ermite.permalink();
702 k-merge from:body -> window.ermiteMR?.openMerge();706 k-merge from:body -> window.ermiteMR?.openMerge();
703 k-read from:body -> window.ermiteMR?.markRead();707 k-read from:body -> window.ermiteMR?.markRead();
704- k-gf from:body -> window.ermiteMR?.focusFiles();
705 k-keys from:body -> ermite.toggleKeys()">708 k-keys from:body -> ermite.toggleKeys()">
706 </div>709 </div>
707 </body>710 </body>
M internal/app/testdata/web/commits-page2.html +33 -30
@@ -70,31 +70,31 @@
70 <nav class="tabs" aria-label="Sections">70 <nav class="tabs" aria-label="Sections">
71 <ul>71 <ul>
72 <li>72 <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>
74 </a>74 </a>
75 </li>75 </li>
76 <li>76 <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>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <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>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <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>
86 </a>86 </a>
87 </li>87 </li>
88 <li>88 <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>
90 </a>90 </a>
91 </li>91 </li>
92 <li>92 <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>
94 </a>94 </a>
95 </li>95 </li>
96 <li>96 <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>
98 </a>98 </a>
99 </li>99 </li>
100 </ul>100 </ul>
@@ -255,14 +255,16 @@
255 </div>255 </div>
256 <div>256 <div>
257 <dt>257 <dt>
258-<kbd>s</kbd>258+<kbd>g s</kbd>
259-<kbd>t</kbd>259+<kbd>g f</kbd>
260-<kbd>c</kbd>260+<kbd>g c</kbd>
261-<kbd>r</kbd>261+<kbd>g b</kbd>
262-<kbd>m</kbd>262+<kbd>g m</kbd>
263-<kbd>i</kbd>263+<kbd>g i</kbd>
264+<kbd>g r</kbd>
265+<kbd>g a</kbd>
264 </dt>266 </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>
266 </div>268 </div>
267 <div>269 <div>
268 <dt>270 <dt>
@@ -353,45 +355,45 @@
353 <span class="k">go</span>355 <span class="k">go</span>
354 </a>356 </a>
355 </li>357 </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)">
357 <a role="presentation" data-href="/long">359 <a role="presentation" data-href="/long">
358-<span class="lbl">summary</span>360+<span class="lbl">summary (g s)</span>
359 <span class="k">section</span>361 <span class="k">section</span>
360 </a>362 </a>
361 </li>363 </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)">
363 <a role="presentation" data-href="/long/tree/main">365 <a role="presentation" data-href="/long/tree/main">
364-<span class="lbl">files</span>366+<span class="lbl">files (g f)</span>
365 <span class="k">section</span>367 <span class="k">section</span>
366 </a>368 </a>
367 </li>369 </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)">
369 <a role="presentation" data-href="/long/commits/main">371 <a role="presentation" data-href="/long/commits/main">
370-<span class="lbl">commits</span>372+<span class="lbl">commits (g c)</span>
371 <span class="k">section</span>373 <span class="k">section</span>
372 </a>374 </a>
373 </li>375 </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)">
375 <a role="presentation" data-href="/long/refs">377 <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>
377 <span class="k">section</span>379 <span class="k">section</span>
378 </a>380 </a>
379 </li>381 </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)">
381 <a role="presentation" data-href="/long/merge-requests">383 <a role="presentation" data-href="/long/merge-requests">
382-<span class="lbl">merge requests</span>384+<span class="lbl">merge requests (g m)</span>
383 <span class="k">section</span>385 <span class="k">section</span>
384 </a>386 </a>
385 </li>387 </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)">
387 <a role="presentation" data-href="/long/releases">389 <a role="presentation" data-href="/long/releases">
388-<span class="lbl">releases</span>390+<span class="lbl">releases (g r)</span>
389 <span class="k">section</span>391 <span class="k">section</span>
390 </a>392 </a>
391 </li>393 </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)">
393 <a role="presentation" data-href="/long/actions">395 <a role="presentation" data-href="/long/actions">
394-<span class="lbl">actions</span>396+<span class="lbl">actions (g a)</span>
395 <span class="k">section</span>397 <span class="k">section</span>
396 </a>398 </a>
397 </li>399 </li>
@@ -438,7 +440,9 @@
438 k-filter from:body -> ermite.focusFilter();440 k-filter from:body -> ermite.focusFilter();
439 k-gh from:body -> ermite.go('.brand');441 k-gh from:body -> ermite.go('.brand');
440 k-s from:body -> ermite.tab('s');442 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');
442 k-c from:body -> ermite.tab('c');446 k-c from:body -> ermite.tab('c');
443 k-r from:body -> ermite.tab('r');447 k-r from:body -> ermite.tab('r');
444 k-m from:body -> ermite.tab('m');448 k-m from:body -> ermite.tab('m');
@@ -454,7 +458,6 @@
454 k-permalink from:body -> ermite.permalink();458 k-permalink from:body -> ermite.permalink();
455 k-merge from:body -> window.ermiteMR?.openMerge();459 k-merge from:body -> window.ermiteMR?.openMerge();
456 k-read from:body -> window.ermiteMR?.markRead();460 k-read from:body -> window.ermiteMR?.markRead();
457- k-gf from:body -> window.ermiteMR?.focusFiles();
458 k-keys from:body -> ermite.toggleKeys()">461 k-keys from:body -> ermite.toggleKeys()">
459 </div>462 </div>
460 </body>463 </body>
M internal/app/testdata/web/commits-path.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -270,14 +270,16 @@
270 </div>270 </div>
271 <div>271 <div>
272 <dt>272 <dt>
273-<kbd>s</kbd>273+<kbd>g s</kbd>
274-<kbd>t</kbd>274+<kbd>g f</kbd>
275-<kbd>c</kbd>275+<kbd>g c</kbd>
276-<kbd>r</kbd>276+<kbd>g b</kbd>
277-<kbd>m</kbd>277+<kbd>g m</kbd>
278-<kbd>i</kbd>278+<kbd>g i</kbd>
279+<kbd>g r</kbd>
280+<kbd>g a</kbd>
279 </dt>281 </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>
281 </div>283 </div>
282 <div>284 <div>
283 <dt>285 <dt>
@@ -368,45 +370,45 @@
368 <span class="k">go</span>370 <span class="k">go</span>
369 </a>371 </a>
370 </li>372 </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)">
372 <a role="presentation" data-href="/long">374 <a role="presentation" data-href="/long">
373-<span class="lbl">summary</span>375+<span class="lbl">summary (g s)</span>
374 <span class="k">section</span>376 <span class="k">section</span>
375 </a>377 </a>
376 </li>378 </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)">
378 <a role="presentation" data-href="/long/tree/main">380 <a role="presentation" data-href="/long/tree/main">
379-<span class="lbl">files</span>381+<span class="lbl">files (g f)</span>
380 <span class="k">section</span>382 <span class="k">section</span>
381 </a>383 </a>
382 </li>384 </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)">
384 <a role="presentation" data-href="/long/commits/main">386 <a role="presentation" data-href="/long/commits/main">
385-<span class="lbl">commits</span>387+<span class="lbl">commits (g c)</span>
386 <span class="k">section</span>388 <span class="k">section</span>
387 </a>389 </a>
388 </li>390 </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)">
390 <a role="presentation" data-href="/long/refs">392 <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>
392 <span class="k">section</span>394 <span class="k">section</span>
393 </a>395 </a>
394 </li>396 </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)">
396 <a role="presentation" data-href="/long/merge-requests">398 <a role="presentation" data-href="/long/merge-requests">
397-<span class="lbl">merge requests</span>399+<span class="lbl">merge requests (g m)</span>
398 <span class="k">section</span>400 <span class="k">section</span>
399 </a>401 </a>
400 </li>402 </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)">
402 <a role="presentation" data-href="/long/releases">404 <a role="presentation" data-href="/long/releases">
403-<span class="lbl">releases</span>405+<span class="lbl">releases (g r)</span>
404 <span class="k">section</span>406 <span class="k">section</span>
405 </a>407 </a>
406 </li>408 </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)">
408 <a role="presentation" data-href="/long/actions">410 <a role="presentation" data-href="/long/actions">
409-<span class="lbl">actions</span>411+<span class="lbl">actions (g a)</span>
410 <span class="k">section</span>412 <span class="k">section</span>
411 </a>413 </a>
412 </li>414 </li>
@@ -453,7 +455,9 @@
453 k-filter from:body -> ermite.focusFilter();455 k-filter from:body -> ermite.focusFilter();
454 k-gh from:body -> ermite.go('.brand');456 k-gh from:body -> ermite.go('.brand');
455 k-s from:body -> ermite.tab('s');457 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');
457 k-c from:body -> ermite.tab('c');461 k-c from:body -> ermite.tab('c');
458 k-r from:body -> ermite.tab('r');462 k-r from:body -> ermite.tab('r');
459 k-m from:body -> ermite.tab('m');463 k-m from:body -> ermite.tab('m');
@@ -469,7 +473,6 @@
469 k-permalink from:body -> ermite.permalink();473 k-permalink from:body -> ermite.permalink();
470 k-merge from:body -> window.ermiteMR?.openMerge();474 k-merge from:body -> window.ermiteMR?.openMerge();
471 k-read from:body -> window.ermiteMR?.markRead();475 k-read from:body -> window.ermiteMR?.markRead();
472- k-gf from:body -> window.ermiteMR?.focusFiles();
473 k-keys from:body -> ermite.toggleKeys()">476 k-keys from:body -> ermite.toggleKeys()">
474 </div>477 </div>
475 </body>478 </body>
M internal/app/testdata/web/commits-root.html +33 -30
@@ -70,31 +70,31 @@
70 <nav class="tabs" aria-label="Sections">70 <nav class="tabs" aria-label="Sections">
71 <ul>71 <ul>
72 <li>72 <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>
74 </a>74 </a>
75 </li>75 </li>
76 <li>76 <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>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <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>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <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>
86 </a>86 </a>
87 </li>87 </li>
88 <li>88 <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>
90 </a>90 </a>
91 </li>91 </li>
92 <li>92 <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>
94 </a>94 </a>
95 </li>95 </li>
96 <li>96 <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>
98 </a>98 </a>
99 </li>99 </li>
100 </ul>100 </ul>
@@ -1060,14 +1060,16 @@
1060 </div>1060 </div>
1061 <div>1061 <div>
1062 <dt>1062 <dt>
1063-<kbd>s</kbd>1063+<kbd>g s</kbd>
1064-<kbd>t</kbd>1064+<kbd>g f</kbd>
1065-<kbd>c</kbd>1065+<kbd>g c</kbd>
1066-<kbd>r</kbd>1066+<kbd>g b</kbd>
1067-<kbd>m</kbd>1067+<kbd>g m</kbd>
1068-<kbd>i</kbd>1068+<kbd>g i</kbd>
1069+<kbd>g r</kbd>
1070+<kbd>g a</kbd>
1069 </dt>1071 </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>
1071 </div>1073 </div>
1072 <div>1074 <div>
1073 <dt>1075 <dt>
@@ -1158,45 +1160,45 @@
1158 <span class="k">go</span>1160 <span class="k">go</span>
1159 </a>1161 </a>
1160 </li>1162 </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)">
1162 <a role="presentation" data-href="/long">1164 <a role="presentation" data-href="/long">
1163-<span class="lbl">summary</span>1165+<span class="lbl">summary (g s)</span>
1164 <span class="k">section</span>1166 <span class="k">section</span>
1165 </a>1167 </a>
1166 </li>1168 </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)">
1168 <a role="presentation" data-href="/long/tree/main">1170 <a role="presentation" data-href="/long/tree/main">
1169-<span class="lbl">files</span>1171+<span class="lbl">files (g f)</span>
1170 <span class="k">section</span>1172 <span class="k">section</span>
1171 </a>1173 </a>
1172 </li>1174 </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)">
1174 <a role="presentation" data-href="/long/commits/main">1176 <a role="presentation" data-href="/long/commits/main">
1175-<span class="lbl">commits</span>1177+<span class="lbl">commits (g c)</span>
1176 <span class="k">section</span>1178 <span class="k">section</span>
1177 </a>1179 </a>
1178 </li>1180 </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)">
1180 <a role="presentation" data-href="/long/refs">1182 <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>
1182 <span class="k">section</span>1184 <span class="k">section</span>
1183 </a>1185 </a>
1184 </li>1186 </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)">
1186 <a role="presentation" data-href="/long/merge-requests">1188 <a role="presentation" data-href="/long/merge-requests">
1187-<span class="lbl">merge requests</span>1189+<span class="lbl">merge requests (g m)</span>
1188 <span class="k">section</span>1190 <span class="k">section</span>
1189 </a>1191 </a>
1190 </li>1192 </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)">
1192 <a role="presentation" data-href="/long/releases">1194 <a role="presentation" data-href="/long/releases">
1193-<span class="lbl">releases</span>1195+<span class="lbl">releases (g r)</span>
1194 <span class="k">section</span>1196 <span class="k">section</span>
1195 </a>1197 </a>
1196 </li>1198 </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)">
1198 <a role="presentation" data-href="/long/actions">1200 <a role="presentation" data-href="/long/actions">
1199-<span class="lbl">actions</span>1201+<span class="lbl">actions (g a)</span>
1200 <span class="k">section</span>1202 <span class="k">section</span>
1201 </a>1203 </a>
1202 </li>1204 </li>
@@ -1243,7 +1245,9 @@
1243 k-filter from:body -> ermite.focusFilter();1245 k-filter from:body -> ermite.focusFilter();
1244 k-gh from:body -> ermite.go('.brand');1246 k-gh from:body -> ermite.go('.brand');
1245 k-s from:body -> ermite.tab('s');1247 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');
1247 k-c from:body -> ermite.tab('c');1251 k-c from:body -> ermite.tab('c');
1248 k-r from:body -> ermite.tab('r');1252 k-r from:body -> ermite.tab('r');
1249 k-m from:body -> ermite.tab('m');1253 k-m from:body -> ermite.tab('m');
@@ -1259,7 +1263,6 @@
1259 k-permalink from:body -> ermite.permalink();1263 k-permalink from:body -> ermite.permalink();
1260 k-merge from:body -> window.ermiteMR?.openMerge();1264 k-merge from:body -> window.ermiteMR?.openMerge();
1261 k-read from:body -> window.ermiteMR?.markRead();1265 k-read from:body -> window.ermiteMR?.markRead();
1262- k-gf from:body -> window.ermiteMR?.focusFiles();
1263 k-keys from:body -> ermite.toggleKeys()">1266 k-keys from:body -> ermite.toggleKeys()">
1264 </div>1267 </div>
1265 </body>1268 </body>
M internal/app/testdata/web/commits-small.html +33 -30
@@ -70,31 +70,31 @@
70 <nav class="tabs" aria-label="Sections">70 <nav class="tabs" aria-label="Sections">
71 <ul>71 <ul>
72 <li>72 <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>
74 </a>74 </a>
75 </li>75 </li>
76 <li>76 <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>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <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>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <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>
86 </a>86 </a>
87 </li>87 </li>
88 <li>88 <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>
90 </a>90 </a>
91 </li>91 </li>
92 <li>92 <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>
94 </a>94 </a>
95 </li>95 </li>
96 <li>96 <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>
98 </a>98 </a>
99 </li>99 </li>
100 </ul>100 </ul>
@@ -405,14 +405,16 @@
405 </div>405 </div>
406 <div>406 <div>
407 <dt>407 <dt>
408-<kbd>s</kbd>408+<kbd>g s</kbd>
409-<kbd>t</kbd>409+<kbd>g f</kbd>
410-<kbd>c</kbd>410+<kbd>g c</kbd>
411-<kbd>r</kbd>411+<kbd>g b</kbd>
412-<kbd>m</kbd>412+<kbd>g m</kbd>
413-<kbd>i</kbd>413+<kbd>g i</kbd>
414+<kbd>g r</kbd>
415+<kbd>g a</kbd>
414 </dt>416 </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>
416 </div>418 </div>
417 <div>419 <div>
418 <dt>420 <dt>
@@ -511,45 +513,45 @@
511 <span class="k">go</span>513 <span class="k">go</span>
512 </a>514 </a>
513 </li>515 </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)">
515 <a role="presentation" data-href="/diffs">517 <a role="presentation" data-href="/diffs">
516-<span class="lbl">summary</span>518+<span class="lbl">summary (g s)</span>
517 <span class="k">section</span>519 <span class="k">section</span>
518 </a>520 </a>
519 </li>521 </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)">
521 <a role="presentation" data-href="/diffs/tree/main">523 <a role="presentation" data-href="/diffs/tree/main">
522-<span class="lbl">files</span>524+<span class="lbl">files (g f)</span>
523 <span class="k">section</span>525 <span class="k">section</span>
524 </a>526 </a>
525 </li>527 </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)">
527 <a role="presentation" data-href="/diffs/commits/main">529 <a role="presentation" data-href="/diffs/commits/main">
528-<span class="lbl">commits</span>530+<span class="lbl">commits (g c)</span>
529 <span class="k">section</span>531 <span class="k">section</span>
530 </a>532 </a>
531 </li>533 </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)">
533 <a role="presentation" data-href="/diffs/refs">535 <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>
535 <span class="k">section</span>537 <span class="k">section</span>
536 </a>538 </a>
537 </li>539 </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)">
539 <a role="presentation" data-href="/diffs/merge-requests">541 <a role="presentation" data-href="/diffs/merge-requests">
540-<span class="lbl">merge requests</span>542+<span class="lbl">merge requests (g m)</span>
541 <span class="k">section</span>543 <span class="k">section</span>
542 </a>544 </a>
543 </li>545 </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)">
545 <a role="presentation" data-href="/diffs/releases">547 <a role="presentation" data-href="/diffs/releases">
546-<span class="lbl">releases</span>548+<span class="lbl">releases (g r)</span>
547 <span class="k">section</span>549 <span class="k">section</span>
548 </a>550 </a>
549 </li>551 </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)">
551 <a role="presentation" data-href="/diffs/actions">553 <a role="presentation" data-href="/diffs/actions">
552-<span class="lbl">actions</span>554+<span class="lbl">actions (g a)</span>
553 <span class="k">section</span>555 <span class="k">section</span>
554 </a>556 </a>
555 </li>557 </li>
@@ -608,7 +610,9 @@
608 k-filter from:body -> ermite.focusFilter();610 k-filter from:body -> ermite.focusFilter();
609 k-gh from:body -> ermite.go('.brand');611 k-gh from:body -> ermite.go('.brand');
610 k-s from:body -> ermite.tab('s');612 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');
612 k-c from:body -> ermite.tab('c');616 k-c from:body -> ermite.tab('c');
613 k-r from:body -> ermite.tab('r');617 k-r from:body -> ermite.tab('r');
614 k-m from:body -> ermite.tab('m');618 k-m from:body -> ermite.tab('m');
@@ -624,7 +628,6 @@
624 k-permalink from:body -> ermite.permalink();628 k-permalink from:body -> ermite.permalink();
625 k-merge from:body -> window.ermiteMR?.openMerge();629 k-merge from:body -> window.ermiteMR?.openMerge();
626 k-read from:body -> window.ermiteMR?.markRead();630 k-read from:body -> window.ermiteMR?.markRead();
627- k-gf from:body -> window.ermiteMR?.focusFiles();
628 k-keys from:body -> ermite.toggleKeys()">631 k-keys from:body -> ermite.toggleKeys()">
629 </div>632 </div>
630 </body>633 </body>
M internal/app/testdata/web/empty.html +12 -9
@@ -173,14 +173,16 @@
173 </div>173 </div>
174 <div>174 <div>
175 <dt>175 <dt>
176-<kbd>s</kbd>176+<kbd>g s</kbd>
177-<kbd>t</kbd>177+<kbd>g f</kbd>
178-<kbd>c</kbd>178+<kbd>g c</kbd>
179-<kbd>r</kbd>179+<kbd>g b</kbd>
180-<kbd>m</kbd>180+<kbd>g m</kbd>
181-<kbd>i</kbd>181+<kbd>g i</kbd>
182+<kbd>g r</kbd>
183+<kbd>g a</kbd>
182 </dt>184 </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>
184 </div>186 </div>
185 <div>187 <div>
186 <dt>188 <dt>
@@ -281,7 +283,9 @@
281 k-filter from:body -> ermite.focusFilter();283 k-filter from:body -> ermite.focusFilter();
282 k-gh from:body -> ermite.go('.brand');284 k-gh from:body -> ermite.go('.brand');
283 k-s from:body -> ermite.tab('s');285 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');
285 k-c from:body -> ermite.tab('c');289 k-c from:body -> ermite.tab('c');
286 k-r from:body -> ermite.tab('r');290 k-r from:body -> ermite.tab('r');
287 k-m from:body -> ermite.tab('m');291 k-m from:body -> ermite.tab('m');
@@ -297,7 +301,6 @@
297 k-permalink from:body -> ermite.permalink();301 k-permalink from:body -> ermite.permalink();
298 k-merge from:body -> window.ermiteMR?.openMerge();302 k-merge from:body -> window.ermiteMR?.openMerge();
299 k-read from:body -> window.ermiteMR?.markRead();303 k-read from:body -> window.ermiteMR?.markRead();
300- k-gf from:body -> window.ermiteMR?.focusFiles();
301 k-keys from:body -> ermite.toggleKeys()">304 k-keys from:body -> ermite.toggleKeys()">
302 </div>305 </div>
303 </body>306 </body>
M internal/app/testdata/web/home.html +12 -9
@@ -277,14 +277,16 @@
277 </div>277 </div>
278 <div>278 <div>
279 <dt>279 <dt>
280-<kbd>s</kbd>280+<kbd>g s</kbd>
281-<kbd>t</kbd>281+<kbd>g f</kbd>
282-<kbd>c</kbd>282+<kbd>g c</kbd>
283-<kbd>r</kbd>283+<kbd>g b</kbd>
284-<kbd>m</kbd>284+<kbd>g m</kbd>
285-<kbd>i</kbd>285+<kbd>g i</kbd>
286+<kbd>g r</kbd>
287+<kbd>g a</kbd>
286 </dt>288 </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>
288 </div>290 </div>
289 <div>291 <div>
290 <dt>292 <dt>
@@ -391,7 +393,9 @@
391 k-filter from:body -> ermite.focusFilter();393 k-filter from:body -> ermite.focusFilter();
392 k-gh from:body -> ermite.go('.brand');394 k-gh from:body -> ermite.go('.brand');
393 k-s from:body -> ermite.tab('s');395 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');
395 k-c from:body -> ermite.tab('c');399 k-c from:body -> ermite.tab('c');
396 k-r from:body -> ermite.tab('r');400 k-r from:body -> ermite.tab('r');
397 k-m from:body -> ermite.tab('m');401 k-m from:body -> ermite.tab('m');
@@ -407,7 +411,6 @@
407 k-permalink from:body -> ermite.permalink();411 k-permalink from:body -> ermite.permalink();
408 k-merge from:body -> window.ermiteMR?.openMerge();412 k-merge from:body -> window.ermiteMR?.openMerge();
409 k-read from:body -> window.ermiteMR?.markRead();413 k-read from:body -> window.ermiteMR?.markRead();
410- k-gf from:body -> window.ermiteMR?.focusFiles();
411 k-keys from:body -> ermite.toggleKeys()">414 k-keys from:body -> ermite.toggleKeys()">
412 </div>415 </div>
413 </body>416 </body>
M internal/app/testdata/web/not-found-page.html +12 -9
@@ -162,14 +162,16 @@
162 </div>162 </div>
163 <div>163 <div>
164 <dt>164 <dt>
165-<kbd>s</kbd>165+<kbd>g s</kbd>
166-<kbd>t</kbd>166+<kbd>g f</kbd>
167-<kbd>c</kbd>167+<kbd>g c</kbd>
168-<kbd>r</kbd>168+<kbd>g b</kbd>
169-<kbd>m</kbd>169+<kbd>g m</kbd>
170-<kbd>i</kbd>170+<kbd>g i</kbd>
171+<kbd>g r</kbd>
172+<kbd>g a</kbd>
171 </dt>173 </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>
173 </div>175 </div>
174 <div>176 <div>
175 <dt>177 <dt>
@@ -264,7 +266,9 @@
264 k-filter from:body -> ermite.focusFilter();266 k-filter from:body -> ermite.focusFilter();
265 k-gh from:body -> ermite.go('.brand');267 k-gh from:body -> ermite.go('.brand');
266 k-s from:body -> ermite.tab('s');268 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');
268 k-c from:body -> ermite.tab('c');272 k-c from:body -> ermite.tab('c');
269 k-r from:body -> ermite.tab('r');273 k-r from:body -> ermite.tab('r');
270 k-m from:body -> ermite.tab('m');274 k-m from:body -> ermite.tab('m');
@@ -280,7 +284,6 @@
280 k-permalink from:body -> ermite.permalink();284 k-permalink from:body -> ermite.permalink();
281 k-merge from:body -> window.ermiteMR?.openMerge();285 k-merge from:body -> window.ermiteMR?.openMerge();
282 k-read from:body -> window.ermiteMR?.markRead();286 k-read from:body -> window.ermiteMR?.markRead();
283- k-gf from:body -> window.ermiteMR?.focusFiles();
284 k-keys from:body -> ermite.toggleKeys()">287 k-keys from:body -> ermite.toggleKeys()">
285 </div>288 </div>
286 </body>289 </body>
M internal/app/testdata/web/not-found.html +12 -9
@@ -162,14 +162,16 @@
162 </div>162 </div>
163 <div>163 <div>
164 <dt>164 <dt>
165-<kbd>s</kbd>165+<kbd>g s</kbd>
166-<kbd>t</kbd>166+<kbd>g f</kbd>
167-<kbd>c</kbd>167+<kbd>g c</kbd>
168-<kbd>r</kbd>168+<kbd>g b</kbd>
169-<kbd>m</kbd>169+<kbd>g m</kbd>
170-<kbd>i</kbd>170+<kbd>g i</kbd>
171+<kbd>g r</kbd>
172+<kbd>g a</kbd>
171 </dt>173 </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>
173 </div>175 </div>
174 <div>176 <div>
175 <dt>177 <dt>
@@ -264,7 +266,9 @@
264 k-filter from:body -> ermite.focusFilter();266 k-filter from:body -> ermite.focusFilter();
265 k-gh from:body -> ermite.go('.brand');267 k-gh from:body -> ermite.go('.brand');
266 k-s from:body -> ermite.tab('s');268 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');
268 k-c from:body -> ermite.tab('c');272 k-c from:body -> ermite.tab('c');
269 k-r from:body -> ermite.tab('r');273 k-r from:body -> ermite.tab('r');
270 k-m from:body -> ermite.tab('m');274 k-m from:body -> ermite.tab('m');
@@ -280,7 +284,6 @@
280 k-permalink from:body -> ermite.permalink();284 k-permalink from:body -> ermite.permalink();
281 k-merge from:body -> window.ermiteMR?.openMerge();285 k-merge from:body -> window.ermiteMR?.openMerge();
282 k-read from:body -> window.ermiteMR?.markRead();286 k-read from:body -> window.ermiteMR?.markRead();
283- k-gf from:body -> window.ermiteMR?.focusFiles();
284 k-keys from:body -> ermite.toggleKeys()">287 k-keys from:body -> ermite.toggleKeys()">
285 </div>288 </div>
286 </body>289 </body>
M internal/app/testdata/web/refs.html +33 -30
@@ -67,31 +67,31 @@
67 <nav class="tabs" aria-label="Sections">67 <nav class="tabs" aria-label="Sections">
68 <ul>68 <ul>
69 <li>69 <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>
71 </a>71 </a>
72 </li>72 </li>
73 <li>73 <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>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <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>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <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>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <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>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <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>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <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>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
@@ -287,14 +287,16 @@
287 </div>287 </div>
288 <div>288 <div>
289 <dt>289 <dt>
290-<kbd>s</kbd>290+<kbd>g s</kbd>
291-<kbd>t</kbd>291+<kbd>g f</kbd>
292-<kbd>c</kbd>292+<kbd>g c</kbd>
293-<kbd>r</kbd>293+<kbd>g b</kbd>
294-<kbd>m</kbd>294+<kbd>g m</kbd>
295-<kbd>i</kbd>295+<kbd>g i</kbd>
296+<kbd>g r</kbd>
297+<kbd>g a</kbd>
296 </dt>298 </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>
298 </div>300 </div>
299 <div>301 <div>
300 <dt>302 <dt>
@@ -397,45 +399,45 @@
397 <span class="k">go</span>399 <span class="k">go</span>
398 </a>400 </a>
399 </li>401 </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)">
401 <a role="presentation" data-href="/demo">403 <a role="presentation" data-href="/demo">
402-<span class="lbl">summary</span>404+<span class="lbl">summary (g s)</span>
403 <span class="k">section</span>405 <span class="k">section</span>
404 </a>406 </a>
405 </li>407 </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)">
407 <a role="presentation" data-href="/demo/tree/main">409 <a role="presentation" data-href="/demo/tree/main">
408-<span class="lbl">files</span>410+<span class="lbl">files (g f)</span>
409 <span class="k">section</span>411 <span class="k">section</span>
410 </a>412 </a>
411 </li>413 </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)">
413 <a role="presentation" data-href="/demo/commits/main">415 <a role="presentation" data-href="/demo/commits/main">
414-<span class="lbl">commits</span>416+<span class="lbl">commits (g c)</span>
415 <span class="k">section</span>417 <span class="k">section</span>
416 </a>418 </a>
417 </li>419 </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)">
419 <a role="presentation" data-href="/demo/refs">421 <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>
421 <span class="k">section</span>423 <span class="k">section</span>
422 </a>424 </a>
423 </li>425 </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)">
425 <a role="presentation" data-href="/demo/merge-requests">427 <a role="presentation" data-href="/demo/merge-requests">
426-<span class="lbl">merge requests</span>428+<span class="lbl">merge requests (g m)</span>
427 <span class="k">section</span>429 <span class="k">section</span>
428 </a>430 </a>
429 </li>431 </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)">
431 <a role="presentation" data-href="/demo/releases">433 <a role="presentation" data-href="/demo/releases">
432-<span class="lbl">releases</span>434+<span class="lbl">releases (g r)</span>
433 <span class="k">section</span>435 <span class="k">section</span>
434 </a>436 </a>
435 </li>437 </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)">
437 <a role="presentation" data-href="/demo/actions">439 <a role="presentation" data-href="/demo/actions">
438-<span class="lbl">actions</span>440+<span class="lbl">actions (g a)</span>
439 <span class="k">section</span>441 <span class="k">section</span>
440 </a>442 </a>
441 </li>443 </li>
@@ -500,7 +502,9 @@
500 k-filter from:body -> ermite.focusFilter();502 k-filter from:body -> ermite.focusFilter();
501 k-gh from:body -> ermite.go('.brand');503 k-gh from:body -> ermite.go('.brand');
502 k-s from:body -> ermite.tab('s');504 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');
504 k-c from:body -> ermite.tab('c');508 k-c from:body -> ermite.tab('c');
505 k-r from:body -> ermite.tab('r');509 k-r from:body -> ermite.tab('r');
506 k-m from:body -> ermite.tab('m');510 k-m from:body -> ermite.tab('m');
@@ -516,7 +520,6 @@
516 k-permalink from:body -> ermite.permalink();520 k-permalink from:body -> ermite.permalink();
517 k-merge from:body -> window.ermiteMR?.openMerge();521 k-merge from:body -> window.ermiteMR?.openMerge();
518 k-read from:body -> window.ermiteMR?.markRead();522 k-read from:body -> window.ermiteMR?.markRead();
519- k-gf from:body -> window.ermiteMR?.focusFiles();
520 k-keys from:body -> ermite.toggleKeys()">523 k-keys from:body -> ermite.toggleKeys()">
521 </div>524 </div>
522 </body>525 </body>
M internal/app/testdata/web/summary.html +33 -30
@@ -64,31 +64,31 @@
64 <nav class="tabs" aria-label="Sections">64 <nav class="tabs" aria-label="Sections">
65 <ul>65 <ul>
66 <li>66 <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>
68 </a>68 </a>
69 </li>69 </li>
70 <li>70 <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>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <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>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
@@ -313,14 +313,16 @@
313 </div>313 </div>
314 <div>314 <div>
315 <dt>315 <dt>
316-<kbd>s</kbd>316+<kbd>g s</kbd>
317-<kbd>t</kbd>317+<kbd>g f</kbd>
318-<kbd>c</kbd>318+<kbd>g c</kbd>
319-<kbd>r</kbd>319+<kbd>g b</kbd>
320-<kbd>m</kbd>320+<kbd>g m</kbd>
321-<kbd>i</kbd>321+<kbd>g i</kbd>
322+<kbd>g r</kbd>
323+<kbd>g a</kbd>
322 </dt>324 </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>
324 </div>326 </div>
325 <div>327 <div>
326 <dt>328 <dt>
@@ -423,45 +425,45 @@
423 <span class="k">go</span>425 <span class="k">go</span>
424 </a>426 </a>
425 </li>427 </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)">
427 <a role="presentation" data-href="/demo">429 <a role="presentation" data-href="/demo">
428-<span class="lbl">summary</span>430+<span class="lbl">summary (g s)</span>
429 <span class="k">section</span>431 <span class="k">section</span>
430 </a>432 </a>
431 </li>433 </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)">
433 <a role="presentation" data-href="/demo/tree/main">435 <a role="presentation" data-href="/demo/tree/main">
434-<span class="lbl">files</span>436+<span class="lbl">files (g f)</span>
435 <span class="k">section</span>437 <span class="k">section</span>
436 </a>438 </a>
437 </li>439 </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)">
439 <a role="presentation" data-href="/demo/commits/main">441 <a role="presentation" data-href="/demo/commits/main">
440-<span class="lbl">commits</span>442+<span class="lbl">commits (g c)</span>
441 <span class="k">section</span>443 <span class="k">section</span>
442 </a>444 </a>
443 </li>445 </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)">
445 <a role="presentation" data-href="/demo/refs">447 <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>
447 <span class="k">section</span>449 <span class="k">section</span>
448 </a>450 </a>
449 </li>451 </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)">
451 <a role="presentation" data-href="/demo/merge-requests">453 <a role="presentation" data-href="/demo/merge-requests">
452-<span class="lbl">merge requests</span>454+<span class="lbl">merge requests (g m)</span>
453 <span class="k">section</span>455 <span class="k">section</span>
454 </a>456 </a>
455 </li>457 </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)">
457 <a role="presentation" data-href="/demo/releases">459 <a role="presentation" data-href="/demo/releases">
458-<span class="lbl">releases</span>460+<span class="lbl">releases (g r)</span>
459 <span class="k">section</span>461 <span class="k">section</span>
460 </a>462 </a>
461 </li>463 </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)">
463 <a role="presentation" data-href="/demo/actions">465 <a role="presentation" data-href="/demo/actions">
464-<span class="lbl">actions</span>466+<span class="lbl">actions (g a)</span>
465 <span class="k">section</span>467 <span class="k">section</span>
466 </a>468 </a>
467 </li>469 </li>
@@ -526,7 +528,9 @@
526 k-filter from:body -> ermite.focusFilter();528 k-filter from:body -> ermite.focusFilter();
527 k-gh from:body -> ermite.go('.brand');529 k-gh from:body -> ermite.go('.brand');
528 k-s from:body -> ermite.tab('s');530 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');
530 k-c from:body -> ermite.tab('c');534 k-c from:body -> ermite.tab('c');
531 k-r from:body -> ermite.tab('r');535 k-r from:body -> ermite.tab('r');
532 k-m from:body -> ermite.tab('m');536 k-m from:body -> ermite.tab('m');
@@ -542,7 +546,6 @@
542 k-permalink from:body -> ermite.permalink();546 k-permalink from:body -> ermite.permalink();
543 k-merge from:body -> window.ermiteMR?.openMerge();547 k-merge from:body -> window.ermiteMR?.openMerge();
544 k-read from:body -> window.ermiteMR?.markRead();548 k-read from:body -> window.ermiteMR?.markRead();
545- k-gf from:body -> window.ermiteMR?.focusFiles();
546 k-keys from:body -> ermite.toggleKeys()">549 k-keys from:body -> ermite.toggleKeys()">
547 </div>550 </div>
548 </body>551 </body>
M internal/app/testdata/web/tree-nested.html +33 -30
@@ -76,31 +76,31 @@
76 <nav class="tabs" aria-label="Sections">76 <nav class="tabs" aria-label="Sections">
77 <ul>77 <ul>
78 <li>78 <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>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <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>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <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>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <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>
92 </a>92 </a>
93 </li>93 </li>
94 <li>94 <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>
96 </a>96 </a>
97 </li>97 </li>
98 <li>98 <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>
100 </a>100 </a>
101 </li>101 </li>
102 <li>102 <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>
104 </a>104 </a>
105 </li>105 </li>
106 </ul>106 </ul>
@@ -243,14 +243,16 @@
243 </div>243 </div>
244 <div>244 <div>
245 <dt>245 <dt>
246-<kbd>s</kbd>246+<kbd>g s</kbd>
247-<kbd>t</kbd>247+<kbd>g f</kbd>
248-<kbd>c</kbd>248+<kbd>g c</kbd>
249-<kbd>r</kbd>249+<kbd>g b</kbd>
250-<kbd>m</kbd>250+<kbd>g m</kbd>
251-<kbd>i</kbd>251+<kbd>g i</kbd>
252+<kbd>g r</kbd>
253+<kbd>g a</kbd>
252 </dt>254 </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>
254 </div>256 </div>
255 <div>257 <div>
256 <dt>258 <dt>
@@ -349,45 +351,45 @@
349 <span class="k">go</span>351 <span class="k">go</span>
350 </a>352 </a>
351 </li>353 </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)">
353 <a role="presentation" data-href="/files">355 <a role="presentation" data-href="/files">
354-<span class="lbl">summary</span>356+<span class="lbl">summary (g s)</span>
355 <span class="k">section</span>357 <span class="k">section</span>
356 </a>358 </a>
357 </li>359 </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)">
359 <a role="presentation" data-href="/files/tree/main">361 <a role="presentation" data-href="/files/tree/main">
360-<span class="lbl">files</span>362+<span class="lbl">files (g f)</span>
361 <span class="k">section</span>363 <span class="k">section</span>
362 </a>364 </a>
363 </li>365 </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)">
365 <a role="presentation" data-href="/files/commits/main">367 <a role="presentation" data-href="/files/commits/main">
366-<span class="lbl">commits</span>368+<span class="lbl">commits (g c)</span>
367 <span class="k">section</span>369 <span class="k">section</span>
368 </a>370 </a>
369 </li>371 </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)">
371 <a role="presentation" data-href="/files/refs">373 <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>
373 <span class="k">section</span>375 <span class="k">section</span>
374 </a>376 </a>
375 </li>377 </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)">
377 <a role="presentation" data-href="/files/merge-requests">379 <a role="presentation" data-href="/files/merge-requests">
378-<span class="lbl">merge requests</span>380+<span class="lbl">merge requests (g m)</span>
379 <span class="k">section</span>381 <span class="k">section</span>
380 </a>382 </a>
381 </li>383 </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)">
383 <a role="presentation" data-href="/files/releases">385 <a role="presentation" data-href="/files/releases">
384-<span class="lbl">releases</span>386+<span class="lbl">releases (g r)</span>
385 <span class="k">section</span>387 <span class="k">section</span>
386 </a>388 </a>
387 </li>389 </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)">
389 <a role="presentation" data-href="/files/actions">391 <a role="presentation" data-href="/files/actions">
390-<span class="lbl">actions</span>392+<span class="lbl">actions (g a)</span>
391 <span class="k">section</span>393 <span class="k">section</span>
392 </a>394 </a>
393 </li>395 </li>
@@ -440,7 +442,9 @@
440 k-filter from:body -> ermite.focusFilter();442 k-filter from:body -> ermite.focusFilter();
441 k-gh from:body -> ermite.go('.brand');443 k-gh from:body -> ermite.go('.brand');
442 k-s from:body -> ermite.tab('s');444 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');
444 k-c from:body -> ermite.tab('c');448 k-c from:body -> ermite.tab('c');
445 k-r from:body -> ermite.tab('r');449 k-r from:body -> ermite.tab('r');
446 k-m from:body -> ermite.tab('m');450 k-m from:body -> ermite.tab('m');
@@ -456,7 +460,6 @@
456 k-permalink from:body -> ermite.permalink();460 k-permalink from:body -> ermite.permalink();
457 k-merge from:body -> window.ermiteMR?.openMerge();461 k-merge from:body -> window.ermiteMR?.openMerge();
458 k-read from:body -> window.ermiteMR?.markRead();462 k-read from:body -> window.ermiteMR?.markRead();
459- k-gf from:body -> window.ermiteMR?.focusFiles();
460 k-keys from:body -> ermite.toggleKeys()">463 k-keys from:body -> ermite.toggleKeys()">
461 </div>464 </div>
462 </body>465 </body>
M internal/app/testdata/web/tree-root.html +33 -30
@@ -70,31 +70,31 @@
70 <nav class="tabs" aria-label="Sections">70 <nav class="tabs" aria-label="Sections">
71 <ul>71 <ul>
72 <li>72 <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>
74 </a>74 </a>
75 </li>75 </li>
76 <li>76 <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>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <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>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <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>
86 </a>86 </a>
87 </li>87 </li>
88 <li>88 <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>
90 </a>90 </a>
91 </li>91 </li>
92 <li>92 <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>
94 </a>94 </a>
95 </li>95 </li>
96 <li>96 <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>
98 </a>98 </a>
99 </li>99 </li>
100 </ul>100 </ul>
@@ -464,14 +464,16 @@
464 </div>464 </div>
465 <div>465 <div>
466 <dt>466 <dt>
467-<kbd>s</kbd>467+<kbd>g s</kbd>
468-<kbd>t</kbd>468+<kbd>g f</kbd>
469-<kbd>c</kbd>469+<kbd>g c</kbd>
470-<kbd>r</kbd>470+<kbd>g b</kbd>
471-<kbd>m</kbd>471+<kbd>g m</kbd>
472-<kbd>i</kbd>472+<kbd>g i</kbd>
473+<kbd>g r</kbd>
474+<kbd>g a</kbd>
473 </dt>475 </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>
475 </div>477 </div>
476 <div>478 <div>
477 <dt>479 <dt>
@@ -570,45 +572,45 @@
570 <span class="k">go</span>572 <span class="k">go</span>
571 </a>573 </a>
572 </li>574 </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)">
574 <a role="presentation" data-href="/files">576 <a role="presentation" data-href="/files">
575-<span class="lbl">summary</span>577+<span class="lbl">summary (g s)</span>
576 <span class="k">section</span>578 <span class="k">section</span>
577 </a>579 </a>
578 </li>580 </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)">
580 <a role="presentation" data-href="/files/tree/main">582 <a role="presentation" data-href="/files/tree/main">
581-<span class="lbl">files</span>583+<span class="lbl">files (g f)</span>
582 <span class="k">section</span>584 <span class="k">section</span>
583 </a>585 </a>
584 </li>586 </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)">
586 <a role="presentation" data-href="/files/commits/main">588 <a role="presentation" data-href="/files/commits/main">
587-<span class="lbl">commits</span>589+<span class="lbl">commits (g c)</span>
588 <span class="k">section</span>590 <span class="k">section</span>
589 </a>591 </a>
590 </li>592 </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)">
592 <a role="presentation" data-href="/files/refs">594 <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>
594 <span class="k">section</span>596 <span class="k">section</span>
595 </a>597 </a>
596 </li>598 </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)">
598 <a role="presentation" data-href="/files/merge-requests">600 <a role="presentation" data-href="/files/merge-requests">
599-<span class="lbl">merge requests</span>601+<span class="lbl">merge requests (g m)</span>
600 <span class="k">section</span>602 <span class="k">section</span>
601 </a>603 </a>
602 </li>604 </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)">
604 <a role="presentation" data-href="/files/releases">606 <a role="presentation" data-href="/files/releases">
605-<span class="lbl">releases</span>607+<span class="lbl">releases (g r)</span>
606 <span class="k">section</span>608 <span class="k">section</span>
607 </a>609 </a>
608 </li>610 </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)">
610 <a role="presentation" data-href="/files/actions">612 <a role="presentation" data-href="/files/actions">
611-<span class="lbl">actions</span>613+<span class="lbl">actions (g a)</span>
612 <span class="k">section</span>614 <span class="k">section</span>
613 </a>615 </a>
614 </li>616 </li>
@@ -661,7 +663,9 @@
661 k-filter from:body -> ermite.focusFilter();663 k-filter from:body -> ermite.focusFilter();
662 k-gh from:body -> ermite.go('.brand');664 k-gh from:body -> ermite.go('.brand');
663 k-s from:body -> ermite.tab('s');665 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');
665 k-c from:body -> ermite.tab('c');669 k-c from:body -> ermite.tab('c');
666 k-r from:body -> ermite.tab('r');670 k-r from:body -> ermite.tab('r');
667 k-m from:body -> ermite.tab('m');671 k-m from:body -> ermite.tab('m');
@@ -677,7 +681,6 @@
677 k-permalink from:body -> ermite.permalink();681 k-permalink from:body -> ermite.permalink();
678 k-merge from:body -> window.ermiteMR?.openMerge();682 k-merge from:body -> window.ermiteMR?.openMerge();
679 k-read from:body -> window.ermiteMR?.markRead();683 k-read from:body -> window.ermiteMR?.markRead();
680- k-gf from:body -> window.ermiteMR?.focusFiles();
681 k-keys from:body -> ermite.toggleKeys()">684 k-keys from:body -> ermite.toggleKeys()">
682 </div>685 </div>
683 </body>686 </body>
M internal/app/testdata/web/tree-slash-ref.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -240,14 +240,16 @@
240 </div>240 </div>
241 <div>241 <div>
242 <dt>242 <dt>
243-<kbd>s</kbd>243+<kbd>g s</kbd>
244-<kbd>t</kbd>244+<kbd>g f</kbd>
245-<kbd>c</kbd>245+<kbd>g c</kbd>
246-<kbd>r</kbd>246+<kbd>g b</kbd>
247-<kbd>m</kbd>247+<kbd>g m</kbd>
248-<kbd>i</kbd>248+<kbd>g i</kbd>
249+<kbd>g r</kbd>
250+<kbd>g a</kbd>
249 </dt>251 </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>
251 </div>253 </div>
252 <div>254 <div>
253 <dt>255 <dt>
@@ -346,45 +348,45 @@
346 <span class="k">go</span>348 <span class="k">go</span>
347 </a>349 </a>
348 </li>350 </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)">
350 <a role="presentation" data-href="/files">352 <a role="presentation" data-href="/files">
351-<span class="lbl">summary</span>353+<span class="lbl">summary (g s)</span>
352 <span class="k">section</span>354 <span class="k">section</span>
353 </a>355 </a>
354 </li>356 </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)">
356 <a role="presentation" data-href="/files/tree/main">358 <a role="presentation" data-href="/files/tree/main">
357-<span class="lbl">files</span>359+<span class="lbl">files (g f)</span>
358 <span class="k">section</span>360 <span class="k">section</span>
359 </a>361 </a>
360 </li>362 </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)">
362 <a role="presentation" data-href="/files/commits/main">364 <a role="presentation" data-href="/files/commits/main">
363-<span class="lbl">commits</span>365+<span class="lbl">commits (g c)</span>
364 <span class="k">section</span>366 <span class="k">section</span>
365 </a>367 </a>
366 </li>368 </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)">
368 <a role="presentation" data-href="/files/refs">370 <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>
370 <span class="k">section</span>372 <span class="k">section</span>
371 </a>373 </a>
372 </li>374 </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)">
374 <a role="presentation" data-href="/files/merge-requests">376 <a role="presentation" data-href="/files/merge-requests">
375-<span class="lbl">merge requests</span>377+<span class="lbl">merge requests (g m)</span>
376 <span class="k">section</span>378 <span class="k">section</span>
377 </a>379 </a>
378 </li>380 </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)">
380 <a role="presentation" data-href="/files/releases">382 <a role="presentation" data-href="/files/releases">
381-<span class="lbl">releases</span>383+<span class="lbl">releases (g r)</span>
382 <span class="k">section</span>384 <span class="k">section</span>
383 </a>385 </a>
384 </li>386 </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)">
386 <a role="presentation" data-href="/files/actions">388 <a role="presentation" data-href="/files/actions">
387-<span class="lbl">actions</span>389+<span class="lbl">actions (g a)</span>
388 <span class="k">section</span>390 <span class="k">section</span>
389 </a>391 </a>
390 </li>392 </li>
@@ -437,7 +439,9 @@
437 k-filter from:body -> ermite.focusFilter();439 k-filter from:body -> ermite.focusFilter();
438 k-gh from:body -> ermite.go('.brand');440 k-gh from:body -> ermite.go('.brand');
439 k-s from:body -> ermite.tab('s');441 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');
441 k-c from:body -> ermite.tab('c');445 k-c from:body -> ermite.tab('c');
442 k-r from:body -> ermite.tab('r');446 k-r from:body -> ermite.tab('r');
443 k-m from:body -> ermite.tab('m');447 k-m from:body -> ermite.tab('m');
@@ -453,7 +457,6 @@
453 k-permalink from:body -> ermite.permalink();457 k-permalink from:body -> ermite.permalink();
454 k-merge from:body -> window.ermiteMR?.openMerge();458 k-merge from:body -> window.ermiteMR?.openMerge();
455 k-read from:body -> window.ermiteMR?.markRead();459 k-read from:body -> window.ermiteMR?.markRead();
456- k-gf from:body -> window.ermiteMR?.focusFiles();
457 k-keys from:body -> ermite.toggleKeys()">460 k-keys from:body -> ermite.toggleKeys()">
458 </div>461 </div>
459 </body>462 </body>
M internal/app/testdata/web/tree-subdir.html +33 -30
@@ -73,31 +73,31 @@
73 <nav class="tabs" aria-label="Sections">73 <nav class="tabs" aria-label="Sections">
74 <ul>74 <ul>
75 <li>75 <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>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <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>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <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>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <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>
89 </a>89 </a>
90 </li>90 </li>
91 <li>91 <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>
93 </a>93 </a>
94 </li>94 </li>
95 <li>95 <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>
97 </a>97 </a>
98 </li>98 </li>
99 <li>99 <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>
101 </a>101 </a>
102 </li>102 </li>
103 </ul>103 </ul>
@@ -256,14 +256,16 @@
256 </div>256 </div>
257 <div>257 <div>
258 <dt>258 <dt>
259-<kbd>s</kbd>259+<kbd>g s</kbd>
260-<kbd>t</kbd>260+<kbd>g f</kbd>
261-<kbd>c</kbd>261+<kbd>g c</kbd>
262-<kbd>r</kbd>262+<kbd>g b</kbd>
263-<kbd>m</kbd>263+<kbd>g m</kbd>
264-<kbd>i</kbd>264+<kbd>g i</kbd>
265+<kbd>g r</kbd>
266+<kbd>g a</kbd>
265 </dt>267 </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>
267 </div>269 </div>
268 <div>270 <div>
269 <dt>271 <dt>
@@ -362,45 +364,45 @@
362 <span class="k">go</span>364 <span class="k">go</span>
363 </a>365 </a>
364 </li>366 </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)">
366 <a role="presentation" data-href="/files">368 <a role="presentation" data-href="/files">
367-<span class="lbl">summary</span>369+<span class="lbl">summary (g s)</span>
368 <span class="k">section</span>370 <span class="k">section</span>
369 </a>371 </a>
370 </li>372 </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)">
372 <a role="presentation" data-href="/files/tree/main">374 <a role="presentation" data-href="/files/tree/main">
373-<span class="lbl">files</span>375+<span class="lbl">files (g f)</span>
374 <span class="k">section</span>376 <span class="k">section</span>
375 </a>377 </a>
376 </li>378 </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)">
378 <a role="presentation" data-href="/files/commits/main">380 <a role="presentation" data-href="/files/commits/main">
379-<span class="lbl">commits</span>381+<span class="lbl">commits (g c)</span>
380 <span class="k">section</span>382 <span class="k">section</span>
381 </a>383 </a>
382 </li>384 </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)">
384 <a role="presentation" data-href="/files/refs">386 <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>
386 <span class="k">section</span>388 <span class="k">section</span>
387 </a>389 </a>
388 </li>390 </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)">
390 <a role="presentation" data-href="/files/merge-requests">392 <a role="presentation" data-href="/files/merge-requests">
391-<span class="lbl">merge requests</span>393+<span class="lbl">merge requests (g m)</span>
392 <span class="k">section</span>394 <span class="k">section</span>
393 </a>395 </a>
394 </li>396 </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)">
396 <a role="presentation" data-href="/files/releases">398 <a role="presentation" data-href="/files/releases">
397-<span class="lbl">releases</span>399+<span class="lbl">releases (g r)</span>
398 <span class="k">section</span>400 <span class="k">section</span>
399 </a>401 </a>
400 </li>402 </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)">
402 <a role="presentation" data-href="/files/actions">404 <a role="presentation" data-href="/files/actions">
403-<span class="lbl">actions</span>405+<span class="lbl">actions (g a)</span>
404 <span class="k">section</span>406 <span class="k">section</span>
405 </a>407 </a>
406 </li>408 </li>
@@ -453,7 +455,9 @@
453 k-filter from:body -> ermite.focusFilter();455 k-filter from:body -> ermite.focusFilter();
454 k-gh from:body -> ermite.go('.brand');456 k-gh from:body -> ermite.go('.brand');
455 k-s from:body -> ermite.tab('s');457 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');
457 k-c from:body -> ermite.tab('c');461 k-c from:body -> ermite.tab('c');
458 k-r from:body -> ermite.tab('r');462 k-r from:body -> ermite.tab('r');
459 k-m from:body -> ermite.tab('m');463 k-m from:body -> ermite.tab('m');
@@ -469,7 +473,6 @@
469 k-permalink from:body -> ermite.permalink();473 k-permalink from:body -> ermite.permalink();
470 k-merge from:body -> window.ermiteMR?.openMerge();474 k-merge from:body -> window.ermiteMR?.openMerge();
471 k-read from:body -> window.ermiteMR?.markRead();475 k-read from:body -> window.ermiteMR?.markRead();
472- k-gf from:body -> window.ermiteMR?.focusFiles();
473 k-keys from:body -> ermite.toggleKeys()">476 k-keys from:body -> ermite.toggleKeys()">
474 </div>477 </div>
475 </body>478 </body>
M internal/app/web_issue_test.go +1 -1
@@ -28,7 +28,7 @@ func TestWebIssuePagesEndToEnd(t *testing.T) {
28 // Off by default: no page, no tab, for the owner as well.28 // Off by default: no page, no tab, for the owner as well.
29 assert.Equal(t, 404, b.get("/proj/issues", as(token)).status)29 assert.Equal(t, 404, b.get("/proj/issues", as(token)).status)
30 assert.Equal(t, 404, b.get("/proj/issues/new", as(token)).status)30 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"`)
32 32
33 mode := domain.IssuesOwner33 mode := domain.IssuesOwner
34 _, err := r.forge.Repos.Update(r.ctx, domain.Root(), r.name("proj"), ports.UpdateRepoParams{Issues: &mode})34 _, 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) {
93 require.Equal(t, 200, list.status)93 require.Equal(t, 200, list.status)
94 assert.Contains(t, list.body, "no open merge requests")94 assert.Contains(t, list.body, "no open merge requests")
95 assert.Contains(t, list.body, `href="/proj/compare/feat/search"`)95 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")
97 assert.NotContains(t, strings.ToLower(list.body), "proposal")97 assert.NotContains(t, strings.ToLower(list.body), "proposal")
98 98
99 // Open one from the compare page.99 // 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
311 return _c311 return _c
312 }312 }
313 313
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+
314 // GetRelease provides a mock function for the type ReleaseStore380 // GetRelease provides a mock function for the type ReleaseStore
315 func (_mock *ReleaseStore) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {381 func (_mock *ReleaseStore) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {
316 ret := _mock.Called(ctx, repo, id)382 ret := _mock.Called(ctx, repo, id)
M internal/ports/release.go +3 -0
@@ -13,6 +13,8 @@ import (
13 // Used includes both uploading and uploaded assets.13 // Used includes both uploading and uploaded assets.
14 type ReleaseWrite struct {14 type ReleaseWrite struct {
15 Repo domain.RepoName15 Repo domain.RepoName
16+ SetLatest bool // false leaves the latest index unchanged
17+ LatestID int64 // zero means no eligible release when SetLatest
16 Used int6418 Used int64
17 UsageVersion uint6419 UsageVersion uint64
18 PutReleases, DeleteReleases []domain.Release20 PutReleases, DeleteReleases []domain.Release
@@ -21,6 +23,7 @@ type ReleaseWrite struct {
21 23
22 // ReleaseStore keeps metadata, never contents. *kvstore.Releases implements it.24 // ReleaseStore keeps metadata, never contents. *kvstore.Releases implements it.
23 type ReleaseStore interface {25 type ReleaseStore interface {
26+ GetLatestRelease(ctx context.Context, repo domain.RepoName) (domain.Release, error)
24 NextReleaseID(ctx context.Context) (int64, error)27 NextReleaseID(ctx context.Context) (int64, error)
25 ReleaseUsage(ctx context.Context, repo domain.RepoName) (used int64, version uint64, err error)28 ReleaseUsage(ctx context.Context, repo domain.RepoName) (used int64, version uint64, err error)
26 GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error)29 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
92 if err = fn(&w); err != nil {92 if err = fn(&w); err != nil {
93 return err93 return err
94 }94 }
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+ }
95 err = r.store.ApplyReleases(ctx, w)119 err = r.store.ApplyReleases(ctx, w)
96 if errors.Is(err, domain.ErrConflict) {120 if errors.Is(err, domain.ErrConflict) {
97 continue121 continue
@@ -245,15 +269,7 @@ func (r *Releases) GetRelease(ctx context.Context, p domain.Principal, name doma
245 case ref.Tag != "":269 case ref.Tag != "":
246 rel, err = r.store.GetReleaseByTag(ctx, name, ref.Tag)270 rel, err = r.store.GetReleaseByTag(ctx, name, ref.Tag)
247 default:271 default:
248- var all []domain.Release272+ rel, err = r.store.GetLatestRelease(ctx, name)
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- }
257 }273 }
258 if err == nil && rel.Draft && !releaseWriter(p, name) {274 if err == nil && rel.Draft && !releaseWriter(p, name) {
259 err = domain.ErrNotFound275 err = domain.ErrNotFound
M internal/usecase/releases_integration_test.go +59 -0
@@ -263,6 +263,7 @@ func TestReleaseWriteRetriesUsageConflict(t *testing.T) {
263 reader.EXPECT().ResolveRevision(ctx, repo, rev).Return(domain.ResolvedRevision{}, domain.ErrNotFound)263 reader.EXPECT().ResolveRevision(ctx, repo, rev).Return(domain.ResolvedRevision{}, domain.ErrNotFound)
264 store.EXPECT().GetReleaseByTag(ctx, repo, "v1").Return(domain.Release{}, domain.ErrNotFound)264 store.EXPECT().GetReleaseByTag(ctx, repo, "v1").Return(domain.Release{}, domain.ErrNotFound)
265 store.EXPECT().NextReleaseID(ctx).Return(int64(1), nil)265 store.EXPECT().NextReleaseID(ctx).Return(int64(1), nil)
266+ store.EXPECT().ListReleases(ctx, repo).Return(nil, nil).Twice()
266 store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(1), nil).Once()267 store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(1), nil).Once()
267 store.EXPECT().ApplyReleases(ctx, mock.MatchedBy(func(w ports.ReleaseWrite) bool { return w.UsageVersion == 1 && len(w.PutReleases) == 1 })).Return(domain.ErrConflict).Once()268 store.EXPECT().ApplyReleases(ctx, mock.MatchedBy(func(w ports.ReleaseWrite) bool { return w.UsageVersion == 1 && len(w.PutReleases) == 1 })).Return(domain.ErrConflict).Once()
268 store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(2), nil).Once()269 store.EXPECT().ReleaseUsage(ctx, repo).Return(int64(0), uint64(2), nil).Once()
@@ -334,3 +335,61 @@ func TestReleaseUpdateTargetNeverMovesExistingTag(t *testing.T) {
334 require.NoError(t, err)335 require.NoError(t, err)
335 require.Equal(t, oidA, got.TargetCommitish)336 require.Equal(t, oidA, got.TargetCommitish)
336 }337 }
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 42

↑↓ moveenter openesc close