feat(web): review page polish and home page ordered by last activity (!6)
- feat(web): one merge request card, a diff view menu, a wide diff mode and quieter hunk rows - feat(web): make the wide mode a global w shortcut, set before the first paint, and tighten the merge request header - refactor(web): drop the Wide button of the diff bar, the w shortcut covers it - fix(web): a selected line range is one block, its title names the whole range and its hidden lines stay selected once loaded - test(app): refresh the web golden pages for the view menu, the w shortcut and the store script - feat(web): move the inbox button of the home page next to the logo, with a mailbox icon and a darker count badge - feat(web): order the home page by last activity and move repositories up live - fix(web): match loaded lines to a selection by file key, keep the focus on a closing menu, close menus with escape on every page and restore the hunk header contrast - fix(repos): mutate metadata atomically and record approved issue activity - test: run the activity store test outside the usecase package and its home row subtests in parallel
92 files · +2426 -1392
M gen/ermite/storage/repo.pb.go +22 -11 generated file: collapsed
M internal/adapters/kvstore/issues_test.go +4 -2
| @@ -258,13 +258,15 @@ func TestRepoSettingsKeepTheIssuesMode(t *testing.T) { | |||
| 258 | 258 | assert.Equal(t, domain.IssuesOff, got.Settings.Issues) | |
| 259 | 259 | for _, m := range []domain.IssuesMode{domain.IssuesOwner, domain.IssuesPublic, domain.IssuesOff} { | |
| 260 | 260 | got.Settings.Issues = m | |
| 261 | - | require.NoError(t, s.Update(ctx, got)) | |
| 261 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | |
| 262 | + | require.NoError(t, err) | |
| 262 | 263 | again, err := s.Get(ctx, n) | |
| 263 | 264 | require.NoError(t, err) | |
| 264 | 265 | assert.Equal(t, m, again.Settings.Issues) | |
| 265 | 266 | } | |
| 266 | 267 | got.Settings.Issues = "everyone" | |
| 267 | - | assert.ErrorIs(t, s.Update(ctx, got), domain.ErrInvalid) | |
| 268 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | |
| 269 | + | assert.ErrorIs(t, err, domain.ErrInvalid) | |
| 268 | 270 | } | |
| 269 | 271 | ||
| 270 | 272 | // TestSchema4UpgradesTheTokensWithAllScopes: the tokens that held every | |
M internal/adapters/kvstore/mergerequests_test.go +4 -2
| @@ -442,13 +442,15 @@ func TestRepoSettings(t *testing.T) { | |||
| 442 | 442 | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | |
| 443 | 443 | ||
| 444 | 444 | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false, Issues: domain.IssuesOwner} | |
| 445 | - | require.NoError(t, s.Update(ctx, got)) | |
| 445 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | |
| 446 | + | require.NoError(t, err) | |
| 446 | 447 | again, err := s.Get(ctx, n) | |
| 447 | 448 | require.NoError(t, err) | |
| 448 | 449 | assert.Equal(t, got.Settings, again.Settings) | |
| 449 | 450 | ||
| 450 | 451 | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{"rebase"}} | |
| 451 | - | assert.ErrorIs(t, s.Update(ctx, got), domain.ErrInvalid) | |
| 452 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | |
| 453 | + | assert.ErrorIs(t, err, domain.ErrInvalid) | |
| 452 | 454 | } | |
| 453 | 455 | ||
| 454 | 456 | // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before | |
M internal/adapters/kvstore/repos.go +44 -22
| @@ -64,31 +64,51 @@ func (s *Store) List(ctx context.Context) ([]domain.Repo, error) { | |||
| 64 | 64 | return repos, nil | |
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | - | // Update replaces the stored repository. It returns domain.ErrNotFound if | |
| 68 | - | // there is none and domain.ErrConflict if another write changed it since it | |
| 69 | - | // was read. | |
| 67 | + | const repoMutationTries = 8 | |
| 68 | + | ||
| 69 | + | // Mutate applies a retry-safe mutation under the version it read. | |
| 70 | 70 | // | |
| 71 | - | //hexcheck:ignore suspicious-business-logic-in-adapter port method name only | |
| 72 | - | func (s *Store) Update(ctx context.Context, repo domain.Repo) error { | |
| 73 | - | val, err := encodeRepo(repo) | |
| 74 | - | if err != nil { | |
| 75 | - | return err | |
| 76 | - | } | |
| 77 | - | key := repoKey(repo.Name) | |
| 78 | - | item, ok := s.db.Get(ctx, key) | |
| 79 | - | if !ok { | |
| 80 | - | return domain.ErrNotFound | |
| 81 | - | } | |
| 82 | - | var b kv.Batch | |
| 83 | - | b.RequireVersion(key, item.Version) | |
| 84 | - | b.Put(key, val) | |
| 85 | - | if _, err := s.db.Apply(ctx, &b); err != nil { | |
| 86 | - | if errors.Is(err, kv.ErrConflict) { | |
| 87 | - | return domain.ErrConflict | |
| 71 | + | //hexcheck:ignore suspicious-business-logic-in-adapter optimistic concurrency is a storage concern | |
| 72 | + | func (s *Store) Mutate(ctx context.Context, name domain.RepoName, fn func(*domain.Repo) (bool, error)) (domain.Repo, error) { | |
| 73 | + | key := repoKey(name) | |
| 74 | + | for range repoMutationTries { | |
| 75 | + | if err := ctx.Err(); err != nil { | |
| 76 | + | return domain.Repo{}, err | |
| 77 | + | } | |
| 78 | + | item, ok := s.db.Get(ctx, key) | |
| 79 | + | if !ok { | |
| 80 | + | return domain.Repo{}, domain.ErrNotFound | |
| 88 | 81 | } | |
| 89 | - | return fmt.Errorf("update repository %q: %w", repo.Name, err) | |
| 82 | + | repo, err := decodeRepo(item.Value) | |
| 83 | + | if err != nil { | |
| 84 | + | return domain.Repo{}, err | |
| 85 | + | } | |
| 86 | + | changed, err := fn(&repo) | |
| 87 | + | if err != nil { | |
| 88 | + | return domain.Repo{}, err | |
| 89 | + | } | |
| 90 | + | if repo.Name != name { | |
| 91 | + | return domain.Repo{}, fmt.Errorf("mutate repository name: %w", domain.ErrInvalidName) | |
| 92 | + | } | |
| 93 | + | if !changed { | |
| 94 | + | return decodeRepo(item.Value) | |
| 95 | + | } | |
| 96 | + | val, err := encodeRepo(repo) | |
| 97 | + | if err != nil { | |
| 98 | + | return domain.Repo{}, err | |
| 99 | + | } | |
| 100 | + | var b kv.Batch | |
| 101 | + | b.RequireVersion(key, item.Version) | |
| 102 | + | b.Put(key, val) | |
| 103 | + | if _, err := s.db.Apply(ctx, &b); err != nil { | |
| 104 | + | if errors.Is(err, kv.ErrConflict) { | |
| 105 | + | continue | |
| 106 | + | } | |
| 107 | + | return domain.Repo{}, fmt.Errorf("mutate repository %q: %w", name, err) | |
| 108 | + | } | |
| 109 | + | return decodeRepo(val) | |
| 90 | 110 | } | |
| 91 | - | return nil | |
| 111 | + | return domain.Repo{}, domain.ErrConflict | |
| 92 | 112 | } | |
| 93 | 113 | ||
| 94 | 114 | // guardRepoKey makes the batch depend on the repository: it requires the | |
| @@ -208,6 +228,7 @@ func encodeRepo(r domain.Repo) ([]byte, error) { | |||
| 208 | 228 | Visibility: vis, | |
| 209 | 229 | CreatedAt: encodeTime(r.CreatedAt), | |
| 210 | 230 | PushedAt: encodeTime(r.PushedAt), | |
| 231 | + | ActiveAt: encodeTime(r.ActiveAt), | |
| 211 | 232 | Settings: pbSettings, | |
| 212 | 233 | } | |
| 213 | 234 | return mustMarshal(pb), nil | |
| @@ -229,6 +250,7 @@ func decodeRepo(val []byte) (domain.Repo, error) { | |||
| 229 | 250 | Visibility: decodeVisibility(pb.GetVisibility()), | |
| 230 | 251 | CreatedAt: decodeTime(pb.GetCreatedAt()), | |
| 231 | 252 | PushedAt: decodeTime(pb.GetPushedAt()), | |
| 253 | + | ActiveAt: decodeTime(pb.GetActiveAt()), | |
| 232 | 254 | Settings: decodeSettings(pb.GetSettings()), | |
| 233 | 255 | }, nil | |
| 234 | 256 | } | |
A internal/adapters/kvstore/repos_mutate_test.go +87 -0
| @@ -0,0 +1,87 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package kvstore_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite/internal/domain" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func TestRepoMutateInterleavedWrites(t *testing.T) { | |
| 16 | + | ctx := t.Context() | |
| 17 | + | s := openStore(t, t.TempDir()) | |
| 18 | + | n := name(t, "proj") | |
| 19 | + | original := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: created} | |
| 20 | + | require.NoError(t, s.Create(ctx, original)) | |
| 21 | + | later := created.Add(2 * time.Hour) | |
| 22 | + | calls := 0 | |
| 23 | + | got, err := s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { | |
| 24 | + | calls++ | |
| 25 | + | // Test-only interleaving: inject a committed write after the outer read. | |
| 26 | + | if calls == 1 { | |
| 27 | + | _, err := s.Mutate(ctx, n, func(current *domain.Repo) (bool, error) { | |
| 28 | + | current.Visibility = domain.VisibilityPrivate | |
| 29 | + | current.ActiveAt = later | |
| 30 | + | current.PushedAt = later | |
| 31 | + | return true, nil | |
| 32 | + | }) | |
| 33 | + | require.NoError(t, err) | |
| 34 | + | } | |
| 35 | + | r.Description = "updated" | |
| 36 | + | at := created.Add(time.Hour) | |
| 37 | + | if at.After(r.ActiveAt) { | |
| 38 | + | r.ActiveAt = at | |
| 39 | + | } | |
| 40 | + | if at.After(r.PushedAt) { | |
| 41 | + | r.PushedAt = at | |
| 42 | + | } | |
| 43 | + | return true, nil | |
| 44 | + | }) | |
| 45 | + | require.NoError(t, err) | |
| 46 | + | require.Equal(t, 2, calls) | |
| 47 | + | require.Equal(t, domain.VisibilityPrivate, got.Visibility) | |
| 48 | + | require.Equal(t, "updated", got.Description) | |
| 49 | + | require.Equal(t, later, got.ActiveAt) | |
| 50 | + | require.Equal(t, later, got.PushedAt) | |
| 51 | + | stored, err := s.Get(ctx, n) | |
| 52 | + | require.NoError(t, err) | |
| 53 | + | require.Equal(t, got, stored) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func TestRepoMutateContracts(t *testing.T) { | |
| 57 | + | ctx := t.Context() | |
| 58 | + | s := openStore(t, t.TempDir()) | |
| 59 | + | n := name(t, "proj") | |
| 60 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: created})) | |
| 61 | + | original, err := s.Get(ctx, n) | |
| 62 | + | require.NoError(t, err) | |
| 63 | + | boom := errors.New("callback failed") | |
| 64 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Description = "discard"; return true, boom }) | |
| 65 | + | require.ErrorIs(t, err, boom) | |
| 66 | + | got, err := s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Description = "discard"; return false, nil }) | |
| 67 | + | require.NoError(t, err) | |
| 68 | + | require.Equal(t, original, got) | |
| 69 | + | for _, changed := range []bool{false, true} { | |
| 70 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Name = name(t, "other"); return changed, nil }) | |
| 71 | + | require.ErrorIs(t, err, domain.ErrInvalidName) | |
| 72 | + | } | |
| 73 | + | got, err = s.Get(ctx, n) | |
| 74 | + | require.NoError(t, err) | |
| 75 | + | require.Equal(t, original, got) | |
| 76 | + | calls := 0 | |
| 77 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { | |
| 78 | + | calls++ | |
| 79 | + | _, err := s.Mutate(ctx, n, func(current *domain.Repo) (bool, error) { current.Description = "concurrent"; return true, nil }) | |
| 80 | + | require.NoError(t, err) | |
| 81 | + | return true, nil | |
| 82 | + | }) | |
| 83 | + | require.ErrorIs(t, err, domain.ErrConflict) | |
| 84 | + | require.Equal(t, 8, calls) | |
| 85 | + | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { require.NoError(t, s.Delete(ctx, n)); return true, nil }) | |
| 86 | + | require.ErrorIs(t, err, domain.ErrNotFound) | |
| 87 | + | } | |
M internal/adapters/kvstore/store_test.go +8 -4
| @@ -43,7 +43,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||
| 43 | 43 | s := openStore(t, t.TempDir()) | |
| 44 | 44 | ||
| 45 | 45 | b := domain.Repo{Name: name(t, "b"), Description: "second", Topics: []string{"cli", "go"}, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()} | |
| 46 | - | a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), Settings: domain.DefaultRepoSettings()} | |
| 46 | + | a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), ActiveAt: created.Add(2 * time.Hour), Settings: domain.DefaultRepoSettings()} | |
| 47 | 47 | require.NoError(t, s.Create(ctx, b)) | |
| 48 | 48 | require.NoError(t, s.Create(ctx, a)) | |
| 49 | 49 | ||
| @@ -54,6 +54,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||
| 54 | 54 | require.NoError(t, err) | |
| 55 | 55 | assert.Equal(t, a, got) | |
| 56 | 56 | assert.True(t, got.PushedAt.Equal(a.PushedAt)) | |
| 57 | + | assert.True(t, got.ActiveAt.Equal(a.ActiveAt)) | |
| 57 | 58 | ||
| 58 | 59 | list, err := s.List(ctx) | |
| 59 | 60 | require.NoError(t, err) | |
| @@ -65,13 +66,15 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||
| 65 | 66 | b.Description = "changed" | |
| 66 | 67 | b.Topics = []string{"forge"} | |
| 67 | 68 | b.Visibility = domain.VisibilityPrivate | |
| 68 | - | require.NoError(t, s.Update(ctx, b)) | |
| 69 | + | _, err = s.Mutate(ctx, b.Name, func(r *domain.Repo) (bool, error) { *r = b; return true, nil }) | |
| 70 | + | require.NoError(t, err) | |
| 69 | 71 | got, err = s.Get(ctx, b.Name) | |
| 70 | 72 | require.NoError(t, err) | |
| 71 | 73 | assert.Equal(t, b, got) | |
| 72 | 74 | ||
| 73 | 75 | b.Topics = nil | |
| 74 | - | require.NoError(t, s.Update(ctx, b)) | |
| 76 | + | _, err = s.Mutate(ctx, b.Name, func(r *domain.Repo) (bool, error) { *r = b; return true, nil }) | |
| 77 | + | require.NoError(t, err) | |
| 75 | 78 | got, err = s.Get(ctx, b.Name) | |
| 76 | 79 | require.NoError(t, err) | |
| 77 | 80 | assert.Nil(t, got.Topics, "cleared topics read back as nil") | |
| @@ -80,7 +83,8 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||
| 80 | 83 | _, err = s.Get(ctx, b.Name) | |
| 81 | 84 | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 82 | 85 | require.NoError(t, s.Delete(ctx, b.Name), "deleting a missing repository is not an error") | |
| 83 | - | assert.ErrorIs(t, s.Update(ctx, b), domain.ErrNotFound) | |
| 86 | + | _, err = s.Mutate(ctx, b.Name, func(r *domain.Repo) (bool, error) { return true, nil }) | |
| 87 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 84 | 88 | ||
| 85 | 89 | _, err = s.Get(ctx, name(t, "never")) | |
| 86 | 90 | assert.ErrorIs(t, err, domain.ErrNotFound) | |
M internal/adapters/web/components.templ +2 -2
| @@ -37,7 +37,7 @@ templ CloneBlock(c Clone) { | |||
| 37 | 37 | // right its figures and CI state over its newest commits. Its id is the | |
| 38 | 38 | // target of SSE swaps; the name link comes first, `enter` follows it. | |
| 39 | 39 | templ HomeRowView(r HomeRow) { | |
| 40 | - | <li id={ r.ID } class="repo" data-cursor?={ r.Cursor }> | |
| 40 | + | <li id={ r.ID } class="repo" data-cursor?={ r.Cursor } data-activity?={ r.Fresh }> | |
| 41 | 41 | <div class="repo-main"> | |
| 42 | 42 | <h2 class="repo-name"> | |
| 43 | 43 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | |
| @@ -45,7 +45,7 @@ templ HomeRowView(r HomeRow) { | |||
| 45 | 45 | <span class="pill">private</span> | |
| 46 | 46 | } | |
| 47 | 47 | if r.Fresh { | |
| 48 | - | <span class="pill new">new push</span> | |
| 48 | + | <span class="pill new">new activity</span> | |
| 49 | 49 | } | |
| 50 | 50 | </h2> | |
| 51 | 51 | if r.Desc != "" { | |
M internal/adapters/web/components_templ.go +119 -113 generated file: collapsed
M internal/adapters/web/diff.templ +22 -4
| @@ -72,9 +72,7 @@ templ Commit(l Layout, p CommitPage) { | |||
| 72 | 72 | <div class="diff-tools" role="group" aria-label="Diff view"> | |
| 73 | 73 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 74 | 74 | <span class="grow"></span> | |
| 75 | - | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 76 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |
| 77 | - | @collapseButton() | |
| 75 | + | @diffViewMenu(p.DiffView) | |
| 78 | 76 | </div> | |
| 79 | 77 | if p.Invisible { | |
| 80 | 78 | <p class="notice"> | |
| @@ -112,6 +110,26 @@ templ Commit(l Layout, p CommitPage) { | |||
| 112 | 110 | } | |
| 113 | 111 | } | |
| 114 | 112 | ||
| 113 | + | // diffViewMenu is the layout of the diff (unified, the default, or split) and | |
| 114 | + | // the fold of its files, in one menu: a <details> that works without scripts; | |
| 115 | + | // u, d and x still act without opening it. Its button names the layout shown. | |
| 116 | + | templ diffViewMenu(p DiffView) { | |
| 117 | + | <details class="menu vmenu"> | |
| 118 | + | <summary class="btn" aria-label="Diff view"> | |
| 119 | + | if p.Both { | |
| 120 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | |
| 121 | + | } else { | |
| 122 | + | { viewLabel(p.View) } | |
| 123 | + | } | |
| 124 | + | </summary> | |
| 125 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 126 | + | @viewLink(p, "unified", "Unified", "u", p.UnifiedHref) | |
| 127 | + | @viewLink(p, "split", "Split", "d", p.SplitHref) | |
| 128 | + | @collapseButton() | |
| 129 | + | </div> | |
| 130 | + | </details> | |
| 131 | + | } | |
| 132 | + | ||
| 115 | 133 | // viewLink is a button of the diff tools. It is a link, so that the split | |
| 116 | 134 | // layout works without JavaScript; with it, and when both layouts are in the | |
| 117 | 135 | // page, a click only flips the layout. | |
| @@ -203,7 +221,7 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 203 | 221 | <details | |
| 204 | 222 | class={ "dfile", "lnw-" + itoa(f.Digits) } | |
| 205 | 223 | id={ f.ID } | |
| 206 | - | data-file | |
| 224 | + | data-file={ f.Key } | |
| 207 | 225 | if f.Open { | |
| 208 | 226 | open | |
| 209 | 227 | } | |
M internal/adapters/web/diff_templ.go +523 -437
| @@ -359,15 +359,7 @@ func Commit(l Layout, p CommitPage) templ.Component { | |||
| 359 | 359 | if templ_7745c5c3_Err != nil { | |
| 360 | 360 | return templ_7745c5c3_Err | |
| 361 | 361 | } | |
| 362 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 363 | - | if templ_7745c5c3_Err != nil { | |
| 364 | - | return templ_7745c5c3_Err | |
| 365 | - | } | |
| 366 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 367 | - | if templ_7745c5c3_Err != nil { | |
| 368 | - | return templ_7745c5c3_Err | |
| 369 | - | } | |
| 370 | - | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |
| 362 | + | templ_7745c5c3_Err = diffViewMenu(p.DiffView).Render(ctx, templ_7745c5c3_Buffer) | |
| 371 | 363 | if templ_7745c5c3_Err != nil { | |
| 372 | 364 | return templ_7745c5c3_Err | |
| 373 | 365 | } | |
| @@ -393,7 +385,7 @@ func Commit(l Layout, p CommitPage) templ.Component { | |||
| 393 | 385 | var templ_7745c5c3_Var17 string | |
| 394 | 386 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 395 | 387 | if templ_7745c5c3_Err != nil { | |
| 396 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 86, Col: 29} | |
| 388 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 84, Col: 29} | |
| 397 | 389 | } | |
| 398 | 390 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | |
| 399 | 391 | if templ_7745c5c3_Err != nil { | |
| @@ -416,7 +408,7 @@ func Commit(l Layout, p CommitPage) templ.Component { | |||
| 416 | 408 | var templ_7745c5c3_Var18 string | |
| 417 | 409 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 418 | 410 | if templ_7745c5c3_Err != nil { | |
| 419 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 91, Col: 22} | |
| 411 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 89, Col: 22} | |
| 420 | 412 | } | |
| 421 | 413 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 422 | 414 | if templ_7745c5c3_Err != nil { | |
| @@ -434,7 +426,7 @@ func Commit(l Layout, p CommitPage) templ.Component { | |||
| 434 | 426 | var templ_7745c5c3_Var19 string | |
| 435 | 427 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 436 | 428 | if templ_7745c5c3_Err != nil { | |
| 437 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 93, Col: 22} | |
| 429 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 91, Col: 22} | |
| 438 | 430 | } | |
| 439 | 431 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) | |
| 440 | 432 | if templ_7745c5c3_Err != nil { | |
| @@ -469,7 +461,7 @@ func Commit(l Layout, p CommitPage) templ.Component { | |||
| 469 | 461 | var templ_7745c5c3_Var20 string | |
| 470 | 462 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 471 | 463 | if templ_7745c5c3_Err != nil { | |
| 472 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 103, Col: 41} | |
| 464 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 101, Col: 41} | |
| 473 | 465 | } | |
| 474 | 466 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | |
| 475 | 467 | if templ_7745c5c3_Err != nil { | |
| @@ -509,6 +501,87 @@ func Commit(l Layout, p CommitPage) templ.Component { | |||
| 509 | 501 | }) | |
| 510 | 502 | } | |
| 511 | 503 | ||
| 504 | + | // diffViewMenu is the layout of the diff (unified, the default, or split) and | |
| 505 | + | // the fold of its files, in one menu: a <details> that works without scripts; | |
| 506 | + | // u, d and x still act without opening it. Its button names the layout shown. | |
| 507 | + | func diffViewMenu(p DiffView) templ.Component { | |
| 508 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 509 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 510 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 511 | + | return templ_7745c5c3_CtxErr | |
| 512 | + | } | |
| 513 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 514 | + | if !templ_7745c5c3_IsBuffer { | |
| 515 | + | defer func() { | |
| 516 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 517 | + | if templ_7745c5c3_Err == nil { | |
| 518 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 519 | + | } | |
| 520 | + | }() | |
| 521 | + | } | |
| 522 | + | ctx = templ.InitializeContext(ctx) | |
| 523 | + | templ_7745c5c3_Var21 := templ.GetChildren(ctx) | |
| 524 | + | if templ_7745c5c3_Var21 == nil { | |
| 525 | + | templ_7745c5c3_Var21 = templ.NopComponent | |
| 526 | + | } | |
| 527 | + | ctx = templ.ClearChildren(ctx) | |
| 528 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<details class=\"menu vmenu\"><summary class=\"btn\" aria-label=\"Diff view\">") | |
| 529 | + | if templ_7745c5c3_Err != nil { | |
| 530 | + | return templ_7745c5c3_Err | |
| 531 | + | } | |
| 532 | + | if p.Both { | |
| 533 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<span hx-live=\"this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'\">") | |
| 534 | + | if templ_7745c5c3_Err != nil { | |
| 535 | + | return templ_7745c5c3_Err | |
| 536 | + | } | |
| 537 | + | var templ_7745c5c3_Var22 string | |
| 538 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(viewLabel(p.View)) | |
| 539 | + | if templ_7745c5c3_Err != nil { | |
| 540 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 120, Col: 122} | |
| 541 | + | } | |
| 542 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 543 | + | if templ_7745c5c3_Err != nil { | |
| 544 | + | return templ_7745c5c3_Err | |
| 545 | + | } | |
| 546 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span>") | |
| 547 | + | if templ_7745c5c3_Err != nil { | |
| 548 | + | return templ_7745c5c3_Err | |
| 549 | + | } | |
| 550 | + | } else { | |
| 551 | + | var templ_7745c5c3_Var23 string | |
| 552 | + | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(viewLabel(p.View)) | |
| 553 | + | if templ_7745c5c3_Err != nil { | |
| 554 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 122, Col: 23} | |
| 555 | + | } | |
| 556 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) | |
| 557 | + | if templ_7745c5c3_Err != nil { | |
| 558 | + | return templ_7745c5c3_Err | |
| 559 | + | } | |
| 560 | + | } | |
| 561 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Diff view\">") | |
| 562 | + | if templ_7745c5c3_Err != nil { | |
| 563 | + | return templ_7745c5c3_Err | |
| 564 | + | } | |
| 565 | + | templ_7745c5c3_Err = viewLink(p, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 566 | + | if templ_7745c5c3_Err != nil { | |
| 567 | + | return templ_7745c5c3_Err | |
| 568 | + | } | |
| 569 | + | templ_7745c5c3_Err = viewLink(p, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 570 | + | if templ_7745c5c3_Err != nil { | |
| 571 | + | return templ_7745c5c3_Err | |
| 572 | + | } | |
| 573 | + | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |
| 574 | + | if templ_7745c5c3_Err != nil { | |
| 575 | + | return templ_7745c5c3_Err | |
| 576 | + | } | |
| 577 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div></details>") | |
| 578 | + | if templ_7745c5c3_Err != nil { | |
| 579 | + | return templ_7745c5c3_Err | |
| 580 | + | } | |
| 581 | + | return nil | |
| 582 | + | }) | |
| 583 | + | } | |
| 584 | + | ||
| 512 | 585 | // viewLink is a button of the diff tools. It is a link, so that the split | |
| 513 | 586 | // layout works without JavaScript; with it, and when both layouts are in the | |
| 514 | 587 | // page, a click only flips the layout. | |
| @@ -528,106 +601,106 @@ func viewLink(p DiffView, view, label, key, href string) templ.Component { | |||
| 528 | 601 | }() | |
| 529 | 602 | } | |
| 530 | 603 | ctx = templ.InitializeContext(ctx) | |
| 531 | - | templ_7745c5c3_Var21 := templ.GetChildren(ctx) | |
| 532 | - | if templ_7745c5c3_Var21 == nil { | |
| 533 | - | templ_7745c5c3_Var21 = templ.NopComponent | |
| 604 | + | templ_7745c5c3_Var24 := templ.GetChildren(ctx) | |
| 605 | + | if templ_7745c5c3_Var24 == nil { | |
| 606 | + | templ_7745c5c3_Var24 = templ.NopComponent | |
| 534 | 607 | } | |
| 535 | 608 | ctx = templ.ClearChildren(ctx) | |
| 536 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a href=\"") | |
| 609 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<a href=\"") | |
| 537 | 610 | if templ_7745c5c3_Err != nil { | |
| 538 | 611 | return templ_7745c5c3_Err | |
| 539 | 612 | } | |
| 540 | - | var templ_7745c5c3_Var22 templ.SafeURL | |
| 541 | - | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 613 | + | var templ_7745c5c3_Var25 templ.SafeURL | |
| 614 | + | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 542 | 615 | if templ_7745c5c3_Err != nil { | |
| 543 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 120, Col: 28} | |
| 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 138, Col: 28} | |
| 544 | 617 | } | |
| 545 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 618 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | |
| 546 | 619 | if templ_7745c5c3_Err != nil { | |
| 547 | 620 | return templ_7745c5c3_Err | |
| 548 | 621 | } | |
| 549 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" class=\"btn vbtn\" data-view=\"") | |
| 622 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" class=\"btn vbtn\" data-view=\"") | |
| 550 | 623 | if templ_7745c5c3_Err != nil { | |
| 551 | 624 | return templ_7745c5c3_Err | |
| 552 | 625 | } | |
| 553 | - | var templ_7745c5c3_Var23 string | |
| 554 | - | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(view) | |
| 626 | + | var templ_7745c5c3_Var26 string | |
| 627 | + | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(view) | |
| 555 | 628 | if templ_7745c5c3_Err != nil { | |
| 556 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 122, Col: 18} | |
| 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 140, Col: 18} | |
| 557 | 630 | } | |
| 558 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | |
| 631 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26) | |
| 559 | 632 | if templ_7745c5c3_Err != nil { | |
| 560 | 633 | return templ_7745c5c3_Err | |
| 561 | 634 | } | |
| 562 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" aria-keyshortcuts=\"") | |
| 635 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" aria-keyshortcuts=\"") | |
| 563 | 636 | if templ_7745c5c3_Err != nil { | |
| 564 | 637 | return templ_7745c5c3_Err | |
| 565 | 638 | } | |
| 566 | - | var templ_7745c5c3_Var24 string | |
| 567 | - | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(key) | |
| 639 | + | var templ_7745c5c3_Var27 string | |
| 640 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(key) | |
| 568 | 641 | if templ_7745c5c3_Err != nil { | |
| 569 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 123, Col: 25} | |
| 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 141, Col: 25} | |
| 570 | 643 | } | |
| 571 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | |
| 644 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) | |
| 572 | 645 | if templ_7745c5c3_Err != nil { | |
| 573 | 646 | return templ_7745c5c3_Err | |
| 574 | 647 | } | |
| 575 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\"") | |
| 648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\"") | |
| 576 | 649 | if templ_7745c5c3_Err != nil { | |
| 577 | 650 | return templ_7745c5c3_Err | |
| 578 | 651 | } | |
| 579 | 652 | if p.View == view { | |
| 580 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " aria-current=\"true\"") | |
| 653 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " aria-current=\"true\"") | |
| 581 | 654 | if templ_7745c5c3_Err != nil { | |
| 582 | 655 | return templ_7745c5c3_Err | |
| 583 | 656 | } | |
| 584 | 657 | } | |
| 585 | 658 | if p.Both { | |
| 586 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " data-fire=\"") | |
| 659 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " data-fire=\"") | |
| 587 | 660 | if templ_7745c5c3_Err != nil { | |
| 588 | 661 | return templ_7745c5c3_Err | |
| 589 | 662 | } | |
| 590 | - | var templ_7745c5c3_Var25 string | |
| 591 | - | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(view) | |
| 663 | + | var templ_7745c5c3_Var28 string | |
| 664 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(view) | |
| 592 | 665 | if templ_7745c5c3_Err != nil { | |
| 593 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 128, Col: 19} | |
| 666 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 146, Col: 19} | |
| 594 | 667 | } | |
| 595 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) | |
| 668 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | |
| 596 | 669 | if templ_7745c5c3_Err != nil { | |
| 597 | 670 | return templ_7745c5c3_Err | |
| 598 | 671 | } | |
| 599 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" :aria-current=\"q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false\"") | |
| 672 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" :aria-current=\"q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false\"") | |
| 600 | 673 | if templ_7745c5c3_Err != nil { | |
| 601 | 674 | return templ_7745c5c3_Err | |
| 602 | 675 | } | |
| 603 | 676 | } | |
| 604 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, ">") | |
| 677 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, ">") | |
| 605 | 678 | if templ_7745c5c3_Err != nil { | |
| 606 | 679 | return templ_7745c5c3_Err | |
| 607 | 680 | } | |
| 608 | - | var templ_7745c5c3_Var26 string | |
| 609 | - | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(label) | |
| 681 | + | var templ_7745c5c3_Var29 string | |
| 682 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(label) | |
| 610 | 683 | if templ_7745c5c3_Err != nil { | |
| 611 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 131, Col: 9} | |
| 684 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 149, Col: 9} | |
| 612 | 685 | } | |
| 613 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 686 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 614 | 687 | if templ_7745c5c3_Err != nil { | |
| 615 | 688 | return templ_7745c5c3_Err | |
| 616 | 689 | } | |
| 617 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<kbd aria-hidden=\"true\">") | |
| 690 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<kbd aria-hidden=\"true\">") | |
| 618 | 691 | if templ_7745c5c3_Err != nil { | |
| 619 | 692 | return templ_7745c5c3_Err | |
| 620 | 693 | } | |
| 621 | - | var templ_7745c5c3_Var27 string | |
| 622 | - | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(key) | |
| 694 | + | var templ_7745c5c3_Var30 string | |
| 695 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(key) | |
| 623 | 696 | if templ_7745c5c3_Err != nil { | |
| 624 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 131, Col: 40} | |
| 697 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 149, Col: 40} | |
| 625 | 698 | } | |
| 626 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 699 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 627 | 700 | if templ_7745c5c3_Err != nil { | |
| 628 | 701 | return templ_7745c5c3_Err | |
| 629 | 702 | } | |
| 630 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</kbd></a>") | |
| 703 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</kbd></a>") | |
| 631 | 704 | if templ_7745c5c3_Err != nil { | |
| 632 | 705 | return templ_7745c5c3_Err | |
| 633 | 706 | } | |
| @@ -653,12 +726,12 @@ func collapseButton() templ.Component { | |||
| 653 | 726 | }() | |
| 654 | 727 | } | |
| 655 | 728 | ctx = templ.InitializeContext(ctx) | |
| 656 | - | templ_7745c5c3_Var28 := templ.GetChildren(ctx) | |
| 657 | - | if templ_7745c5c3_Var28 == nil { | |
| 658 | - | templ_7745c5c3_Var28 = templ.NopComponent | |
| 729 | + | templ_7745c5c3_Var31 := templ.GetChildren(ctx) | |
| 730 | + | if templ_7745c5c3_Var31 == nil { | |
| 731 | + | templ_7745c5c3_Var31 = templ.NopComponent | |
| 659 | 732 | } | |
| 660 | 733 | ctx = templ.ClearChildren(ctx) | |
| 661 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"button\" aria-keyshortcuts=\"x\" data-js data-fire=\"collapse\" class=\"btn\"><span hx-live=\"this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'\">Collapse all</span><kbd aria-hidden=\"true\">x</kbd></button>") | |
| 734 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<button type=\"button\" aria-keyshortcuts=\"x\" data-js data-fire=\"collapse\" class=\"btn\"><span hx-live=\"this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'\">Collapse all</span><kbd aria-hidden=\"true\">x</kbd></button>") | |
| 662 | 735 | if templ_7745c5c3_Err != nil { | |
| 663 | 736 | return templ_7745c5c3_Err | |
| 664 | 737 | } | |
| @@ -682,83 +755,83 @@ func personView(pr Person) templ.Component { | |||
| 682 | 755 | }() | |
| 683 | 756 | } | |
| 684 | 757 | ctx = templ.InitializeContext(ctx) | |
| 685 | - | templ_7745c5c3_Var29 := templ.GetChildren(ctx) | |
| 686 | - | if templ_7745c5c3_Var29 == nil { | |
| 687 | - | templ_7745c5c3_Var29 = templ.NopComponent | |
| 758 | + | templ_7745c5c3_Var32 := templ.GetChildren(ctx) | |
| 759 | + | if templ_7745c5c3_Var32 == nil { | |
| 760 | + | templ_7745c5c3_Var32 = templ.NopComponent | |
| 688 | 761 | } | |
| 689 | 762 | ctx = templ.ClearChildren(ctx) | |
| 690 | - | var templ_7745c5c3_Var30 string | |
| 691 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(pr.Name) | |
| 763 | + | var templ_7745c5c3_Var33 string | |
| 764 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(pr.Name) | |
| 692 | 765 | if templ_7745c5c3_Err != nil { | |
| 693 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 143, Col: 10} | |
| 766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 161, Col: 10} | |
| 694 | 767 | } | |
| 695 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 768 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 696 | 769 | if templ_7745c5c3_Err != nil { | |
| 697 | 770 | return templ_7745c5c3_Err | |
| 698 | 771 | } | |
| 699 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " ") | |
| 772 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " ") | |
| 700 | 773 | if templ_7745c5c3_Err != nil { | |
| 701 | 774 | return templ_7745c5c3_Err | |
| 702 | 775 | } | |
| 703 | 776 | if pr.WhenAt != "" { | |
| 704 | - | var templ_7745c5c3_Var31 string | |
| 705 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 777 | + | var templ_7745c5c3_Var34 string | |
| 778 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 706 | 779 | if templ_7745c5c3_Err != nil { | |
| 707 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 145, Col: 10} | |
| 780 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 163, Col: 10} | |
| 708 | 781 | } | |
| 709 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 782 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 710 | 783 | if templ_7745c5c3_Err != nil { | |
| 711 | 784 | return templ_7745c5c3_Err | |
| 712 | 785 | } | |
| 713 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<time datetime=\"") | |
| 786 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<time datetime=\"") | |
| 714 | 787 | if templ_7745c5c3_Err != nil { | |
| 715 | 788 | return templ_7745c5c3_Err | |
| 716 | 789 | } | |
| 717 | - | var templ_7745c5c3_Var32 string | |
| 718 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(pr.WhenAt) | |
| 790 | + | var templ_7745c5c3_Var35 string | |
| 791 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(pr.WhenAt) | |
| 719 | 792 | if templ_7745c5c3_Err != nil { | |
| 720 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 145, Col: 38} | |
| 793 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 163, Col: 38} | |
| 721 | 794 | } | |
| 722 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) | |
| 795 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) | |
| 723 | 796 | if templ_7745c5c3_Err != nil { | |
| 724 | 797 | return templ_7745c5c3_Err | |
| 725 | 798 | } | |
| 726 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" title=\"") | |
| 799 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" title=\"") | |
| 727 | 800 | if templ_7745c5c3_Err != nil { | |
| 728 | 801 | return templ_7745c5c3_Err | |
| 729 | 802 | } | |
| 730 | - | var templ_7745c5c3_Var33 string | |
| 731 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(pr.Abs) | |
| 803 | + | var templ_7745c5c3_Var36 string | |
| 804 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(pr.Abs) | |
| 732 | 805 | if templ_7745c5c3_Err != nil { | |
| 733 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 145, Col: 55} | |
| 806 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 163, Col: 55} | |
| 734 | 807 | } | |
| 735 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) | |
| 808 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) | |
| 736 | 809 | if templ_7745c5c3_Err != nil { | |
| 737 | 810 | return templ_7745c5c3_Err | |
| 738 | 811 | } | |
| 739 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\">") | |
| 812 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\">") | |
| 740 | 813 | if templ_7745c5c3_Err != nil { | |
| 741 | 814 | return templ_7745c5c3_Err | |
| 742 | 815 | } | |
| 743 | - | var templ_7745c5c3_Var34 string | |
| 744 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(pr.When) | |
| 816 | + | var templ_7745c5c3_Var37 string | |
| 817 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(pr.When) | |
| 745 | 818 | if templ_7745c5c3_Err != nil { | |
| 746 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 145, Col: 67} | |
| 819 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 163, Col: 67} | |
| 747 | 820 | } | |
| 748 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 821 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 749 | 822 | if templ_7745c5c3_Err != nil { | |
| 750 | 823 | return templ_7745c5c3_Err | |
| 751 | 824 | } | |
| 752 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</time>") | |
| 825 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</time>") | |
| 753 | 826 | if templ_7745c5c3_Err != nil { | |
| 754 | 827 | return templ_7745c5c3_Err | |
| 755 | 828 | } | |
| 756 | - | var templ_7745c5c3_Var35 string | |
| 757 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + pr.Abs) | |
| 829 | + | var templ_7745c5c3_Var38 string | |
| 830 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + pr.Abs) | |
| 758 | 831 | if templ_7745c5c3_Err != nil { | |
| 759 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 145, Col: 93} | |
| 832 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 163, Col: 93} | |
| 760 | 833 | } | |
| 761 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 834 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | |
| 762 | 835 | if templ_7745c5c3_Err != nil { | |
| 763 | 836 | return templ_7745c5c3_Err | |
| 764 | 837 | } | |
| @@ -783,25 +856,25 @@ func commitRail(p CommitPage) templ.Component { | |||
| 783 | 856 | }() | |
| 784 | 857 | } | |
| 785 | 858 | ctx = templ.InitializeContext(ctx) | |
| 786 | - | templ_7745c5c3_Var36 := templ.GetChildren(ctx) | |
| 787 | - | if templ_7745c5c3_Var36 == nil { | |
| 788 | - | templ_7745c5c3_Var36 = templ.NopComponent | |
| 859 | + | templ_7745c5c3_Var39 := templ.GetChildren(ctx) | |
| 860 | + | if templ_7745c5c3_Var39 == nil { | |
| 861 | + | templ_7745c5c3_Var39 = templ.NopComponent | |
| 789 | 862 | } | |
| 790 | 863 | ctx = templ.ClearChildren(ctx) | |
| 791 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">") | |
| 864 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">") | |
| 792 | 865 | if templ_7745c5c3_Err != nil { | |
| 793 | 866 | return templ_7745c5c3_Err | |
| 794 | 867 | } | |
| 795 | - | var templ_7745c5c3_Var37 string | |
| 796 | - | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 868 | + | var templ_7745c5c3_Var40 string | |
| 869 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 797 | 870 | if templ_7745c5c3_Err != nil { | |
| 798 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 151, Col: 65} | |
| 871 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 169, Col: 65} | |
| 799 | 872 | } | |
| 800 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) | |
| 873 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 801 | 874 | if templ_7745c5c3_Err != nil { | |
| 802 | 875 | return templ_7745c5c3_Err | |
| 803 | 876 | } | |
| 804 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</span></h2>") | |
| 877 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span></h2>") | |
| 805 | 878 | if templ_7745c5c3_Err != nil { | |
| 806 | 879 | return templ_7745c5c3_Err | |
| 807 | 880 | } | |
| @@ -809,7 +882,7 @@ func commitRail(p CommitPage) templ.Component { | |||
| 809 | 882 | if templ_7745c5c3_Err != nil { | |
| 810 | 883 | return templ_7745c5c3_Err | |
| 811 | 884 | } | |
| 812 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</section>") | |
| 885 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</section>") | |
| 813 | 886 | if templ_7745c5c3_Err != nil { | |
| 814 | 887 | return templ_7745c5c3_Err | |
| 815 | 888 | } | |
| @@ -835,236 +908,236 @@ func FileList(files []DiffFile) templ.Component { | |||
| 835 | 908 | }() | |
| 836 | 909 | } | |
| 837 | 910 | ctx = templ.InitializeContext(ctx) | |
| 838 | - | templ_7745c5c3_Var38 := templ.GetChildren(ctx) | |
| 839 | - | if templ_7745c5c3_Var38 == nil { | |
| 840 | - | templ_7745c5c3_Var38 = templ.NopComponent | |
| 911 | + | templ_7745c5c3_Var41 := templ.GetChildren(ctx) | |
| 912 | + | if templ_7745c5c3_Var41 == nil { | |
| 913 | + | templ_7745c5c3_Var41 = templ.NopComponent | |
| 841 | 914 | } | |
| 842 | 915 | ctx = templ.ClearChildren(ctx) | |
| 843 | 916 | if len(files) > fileFilterMin { | |
| 844 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<div class=\"ffilter\"><label for=\"files-filter\" class=\"sr\">filter the changed files</label> <input id=\"files-filter\" type=\"search\" placeholder=\"Filter files…\" autocomplete=\"off\" data-pick=\".files > li\" hx-on:input=\"ermite.filter(this)\"></div>") | |
| 917 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<div class=\"ffilter\"><label for=\"files-filter\" class=\"sr\">filter the changed files</label> <input id=\"files-filter\" type=\"search\" placeholder=\"Filter files…\" autocomplete=\"off\" data-pick=\".files > li\" hx-on:input=\"ermite.filter(this)\"></div>") | |
| 845 | 918 | if templ_7745c5c3_Err != nil { | |
| 846 | 919 | return templ_7745c5c3_Err | |
| 847 | 920 | } | |
| 848 | 921 | } | |
| 849 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<ol class=\"files\" aria-label=\"Changed files\">") | |
| 922 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<ol class=\"files\" aria-label=\"Changed files\">") | |
| 850 | 923 | if templ_7745c5c3_Err != nil { | |
| 851 | 924 | return templ_7745c5c3_Err | |
| 852 | 925 | } | |
| 853 | 926 | for _, f := range files { | |
| 854 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<li><a href=\"") | |
| 927 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<li><a href=\"") | |
| 855 | 928 | if templ_7745c5c3_Err != nil { | |
| 856 | 929 | return templ_7745c5c3_Err | |
| 857 | 930 | } | |
| 858 | - | var templ_7745c5c3_Var39 templ.SafeURL | |
| 859 | - | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#" + f.ID)) | |
| 931 | + | var templ_7745c5c3_Var42 templ.SafeURL | |
| 932 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#" + f.ID)) | |
| 860 | 933 | if templ_7745c5c3_Err != nil { | |
| 861 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 176, Col: 37} | |
| 934 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 194, Col: 37} | |
| 862 | 935 | } | |
| 863 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 936 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 864 | 937 | if templ_7745c5c3_Err != nil { | |
| 865 | 938 | return templ_7745c5c3_Err | |
| 866 | 939 | } | |
| 867 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" title=\"") | |
| 940 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" title=\"") | |
| 868 | 941 | if templ_7745c5c3_Err != nil { | |
| 869 | 942 | return templ_7745c5c3_Err | |
| 870 | 943 | } | |
| 871 | - | var templ_7745c5c3_Var40 string | |
| 872 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 944 | + | var templ_7745c5c3_Var43 string | |
| 945 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 873 | 946 | if templ_7745c5c3_Err != nil { | |
| 874 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 177, Col: 19} | |
| 947 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 195, Col: 19} | |
| 875 | 948 | } | |
| 876 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | |
| 949 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43) | |
| 877 | 950 | if templ_7745c5c3_Err != nil { | |
| 878 | 951 | return templ_7745c5c3_Err | |
| 879 | 952 | } | |
| 880 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" hx-on:click=\"(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))\"><span class=\"st\" data-st=\"") | |
| 953 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\" hx-on:click=\"(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))\"><span class=\"st\" data-st=\"") | |
| 881 | 954 | if templ_7745c5c3_Err != nil { | |
| 882 | 955 | return templ_7745c5c3_Err | |
| 883 | 956 | } | |
| 884 | - | var templ_7745c5c3_Var41 string | |
| 885 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Status) | |
| 957 | + | var templ_7745c5c3_Var44 string | |
| 958 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Status) | |
| 886 | 959 | if templ_7745c5c3_Err != nil { | |
| 887 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 180, Col: 40} | |
| 960 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 198, Col: 40} | |
| 888 | 961 | } | |
| 889 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41) | |
| 962 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44) | |
| 890 | 963 | if templ_7745c5c3_Err != nil { | |
| 891 | 964 | return templ_7745c5c3_Err | |
| 892 | 965 | } | |
| 893 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" title=\"") | |
| 966 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" title=\"") | |
| 894 | 967 | if templ_7745c5c3_Err != nil { | |
| 895 | 968 | return templ_7745c5c3_Err | |
| 896 | 969 | } | |
| 897 | - | var templ_7745c5c3_Var42 string | |
| 898 | - | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(statusName(f.Status)) | |
| 970 | + | var templ_7745c5c3_Var45 string | |
| 971 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(statusName(f.Status)) | |
| 899 | 972 | if templ_7745c5c3_Err != nil { | |
| 900 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 180, Col: 71} | |
| 973 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 198, Col: 71} | |
| 901 | 974 | } | |
| 902 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42) | |
| 975 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45) | |
| 903 | 976 | if templ_7745c5c3_Err != nil { | |
| 904 | 977 | return templ_7745c5c3_Err | |
| 905 | 978 | } | |
| 906 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">") | |
| 979 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\">") | |
| 907 | 980 | if templ_7745c5c3_Err != nil { | |
| 908 | 981 | return templ_7745c5c3_Err | |
| 909 | 982 | } | |
| 910 | - | var templ_7745c5c3_Var43 string | |
| 911 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(f.Status) | |
| 983 | + | var templ_7745c5c3_Var46 string | |
| 984 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(f.Status) | |
| 912 | 985 | if templ_7745c5c3_Err != nil { | |
| 913 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 180, Col: 84} | |
| 986 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 198, Col: 84} | |
| 914 | 987 | } | |
| 915 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 988 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 916 | 989 | if templ_7745c5c3_Err != nil { | |
| 917 | 990 | return templ_7745c5c3_Err | |
| 918 | 991 | } | |
| 919 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</span> <span class=\"nm\"><span class=\"p\">") | |
| 992 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</span> <span class=\"nm\"><span class=\"p\">") | |
| 920 | 993 | if templ_7745c5c3_Err != nil { | |
| 921 | 994 | return templ_7745c5c3_Err | |
| 922 | 995 | } | |
| 923 | - | var templ_7745c5c3_Var44 string | |
| 924 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(f.Name) | |
| 996 | + | var templ_7745c5c3_Var47 string | |
| 997 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(f.Name) | |
| 925 | 998 | if templ_7745c5c3_Err != nil { | |
| 926 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 181, Col: 46} | |
| 999 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 199, Col: 46} | |
| 927 | 1000 | } | |
| 928 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 1001 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | |
| 929 | 1002 | if templ_7745c5c3_Err != nil { | |
| 930 | 1003 | return templ_7745c5c3_Err | |
| 931 | 1004 | } | |
| 932 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span>") | |
| 1005 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</span>") | |
| 933 | 1006 | if templ_7745c5c3_Err != nil { | |
| 934 | 1007 | return templ_7745c5c3_Err | |
| 935 | 1008 | } | |
| 936 | - | var templ_7745c5c3_Var45 string | |
| 937 | - | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1009 | + | var templ_7745c5c3_Var48 string | |
| 1010 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 938 | 1011 | if templ_7745c5c3_Err != nil { | |
| 939 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 181, Col: 60} | |
| 1012 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 199, Col: 60} | |
| 940 | 1013 | } | |
| 941 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 1014 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 942 | 1015 | if templ_7745c5c3_Err != nil { | |
| 943 | 1016 | return templ_7745c5c3_Err | |
| 944 | 1017 | } | |
| 945 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<span class=\"d\"><bdi>") | |
| 1018 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<span class=\"d\"><bdi>") | |
| 946 | 1019 | if templ_7745c5c3_Err != nil { | |
| 947 | 1020 | return templ_7745c5c3_Err | |
| 948 | 1021 | } | |
| 949 | - | var templ_7745c5c3_Var46 string | |
| 950 | - | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(f.Dir) | |
| 1022 | + | var templ_7745c5c3_Var49 string | |
| 1023 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(f.Dir) | |
| 951 | 1024 | if templ_7745c5c3_Err != nil { | |
| 952 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 181, Col: 90} | |
| 1025 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 199, Col: 90} | |
| 953 | 1026 | } | |
| 954 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 1027 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 955 | 1028 | if templ_7745c5c3_Err != nil { | |
| 956 | 1029 | return templ_7745c5c3_Err | |
| 957 | 1030 | } | |
| 958 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</bdi></span></span> ") | |
| 1031 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</bdi></span></span> ") | |
| 959 | 1032 | if templ_7745c5c3_Err != nil { | |
| 960 | 1033 | return templ_7745c5c3_Err | |
| 961 | 1034 | } | |
| 962 | 1035 | if f.OpenNotes > 0 { | |
| 963 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<span class=\"cn\" title=\"") | |
| 1036 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<span class=\"cn\" title=\"") | |
| 964 | 1037 | if templ_7745c5c3_Err != nil { | |
| 965 | 1038 | return templ_7745c5c3_Err | |
| 966 | 1039 | } | |
| 967 | - | var templ_7745c5c3_Var47 string | |
| 968 | - | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(plural(f.OpenNotes, "open comment", "open comments")) | |
| 1040 | + | var templ_7745c5c3_Var50 string | |
| 1041 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(plural(f.OpenNotes, "open comment", "open comments")) | |
| 969 | 1042 | if templ_7745c5c3_Err != nil { | |
| 970 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 183, Col: 83} | |
| 1043 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 201, Col: 83} | |
| 971 | 1044 | } | |
| 972 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47) | |
| 1045 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | |
| 973 | 1046 | if templ_7745c5c3_Err != nil { | |
| 974 | 1047 | return templ_7745c5c3_Err | |
| 975 | 1048 | } | |
| 976 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "\">") | |
| 1049 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\">") | |
| 977 | 1050 | if templ_7745c5c3_Err != nil { | |
| 978 | 1051 | return templ_7745c5c3_Err | |
| 979 | 1052 | } | |
| 980 | - | var templ_7745c5c3_Var48 string | |
| 981 | - | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.OpenNotes)) | |
| 1053 | + | var templ_7745c5c3_Var51 string | |
| 1054 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.OpenNotes)) | |
| 982 | 1055 | if templ_7745c5c3_Err != nil { | |
| 983 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 183, Col: 105} | |
| 1056 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 201, Col: 105} | |
| 984 | 1057 | } | |
| 985 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 1058 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 986 | 1059 | if templ_7745c5c3_Err != nil { | |
| 987 | 1060 | return templ_7745c5c3_Err | |
| 988 | 1061 | } | |
| 989 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</span> ") | |
| 1062 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</span> ") | |
| 990 | 1063 | if templ_7745c5c3_Err != nil { | |
| 991 | 1064 | return templ_7745c5c3_Err | |
| 992 | 1065 | } | |
| 993 | 1066 | } else if f.Notes > 0 { | |
| 994 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<span class=\"cn done\" title=\"") | |
| 1067 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<span class=\"cn done\" title=\"") | |
| 995 | 1068 | if templ_7745c5c3_Err != nil { | |
| 996 | 1069 | return templ_7745c5c3_Err | |
| 997 | 1070 | } | |
| 998 | - | var templ_7745c5c3_Var49 string | |
| 999 | - | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(plural(f.Notes, "resolved comment", "resolved comments")) | |
| 1071 | + | var templ_7745c5c3_Var52 string | |
| 1072 | + | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(plural(f.Notes, "resolved comment", "resolved comments")) | |
| 1000 | 1073 | if templ_7745c5c3_Err != nil { | |
| 1001 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 185, Col: 92} | |
| 1074 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 203, Col: 92} | |
| 1002 | 1075 | } | |
| 1003 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | |
| 1076 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52) | |
| 1004 | 1077 | if templ_7745c5c3_Err != nil { | |
| 1005 | 1078 | return templ_7745c5c3_Err | |
| 1006 | 1079 | } | |
| 1007 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\">") | |
| 1080 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\">") | |
| 1008 | 1081 | if templ_7745c5c3_Err != nil { | |
| 1009 | 1082 | return templ_7745c5c3_Err | |
| 1010 | 1083 | } | |
| 1011 | - | var templ_7745c5c3_Var50 string | |
| 1012 | - | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Notes)) | |
| 1084 | + | var templ_7745c5c3_Var53 string | |
| 1085 | + | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Notes)) | |
| 1013 | 1086 | if templ_7745c5c3_Err != nil { | |
| 1014 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 185, Col: 110} | |
| 1087 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 203, Col: 110} | |
| 1015 | 1088 | } | |
| 1016 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | |
| 1089 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) | |
| 1017 | 1090 | if templ_7745c5c3_Err != nil { | |
| 1018 | 1091 | return templ_7745c5c3_Err | |
| 1019 | 1092 | } | |
| 1020 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</span> ") | |
| 1093 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</span> ") | |
| 1021 | 1094 | if templ_7745c5c3_Err != nil { | |
| 1022 | 1095 | return templ_7745c5c3_Err | |
| 1023 | 1096 | } | |
| 1024 | 1097 | } | |
| 1025 | 1098 | if f.Bin { | |
| 1026 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<span class=\"n mute\">bin</span>") | |
| 1099 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<span class=\"n mute\">bin</span>") | |
| 1027 | 1100 | if templ_7745c5c3_Err != nil { | |
| 1028 | 1101 | return templ_7745c5c3_Err | |
| 1029 | 1102 | } | |
| 1030 | 1103 | } else { | |
| 1031 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<span class=\"n\"><span class=\"plus\">+") | |
| 1104 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<span class=\"n\"><span class=\"plus\">+") | |
| 1032 | 1105 | if templ_7745c5c3_Err != nil { | |
| 1033 | 1106 | return templ_7745c5c3_Err | |
| 1034 | 1107 | } | |
| 1035 | - | var templ_7745c5c3_Var51 string | |
| 1036 | - | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Add)) | |
| 1108 | + | var templ_7745c5c3_Var54 string | |
| 1109 | + | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Add)) | |
| 1037 | 1110 | if templ_7745c5c3_Err != nil { | |
| 1038 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 190, Col: 55} | |
| 1111 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 208, Col: 55} | |
| 1039 | 1112 | } | |
| 1040 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 1113 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1041 | 1114 | if templ_7745c5c3_Err != nil { | |
| 1042 | 1115 | return templ_7745c5c3_Err | |
| 1043 | 1116 | } | |
| 1044 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</span> <span class=\"minus\">-") | |
| 1117 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</span> <span class=\"minus\">-") | |
| 1045 | 1118 | if templ_7745c5c3_Err != nil { | |
| 1046 | 1119 | return templ_7745c5c3_Err | |
| 1047 | 1120 | } | |
| 1048 | - | var templ_7745c5c3_Var52 string | |
| 1049 | - | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Del)) | |
| 1121 | + | var templ_7745c5c3_Var55 string | |
| 1122 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Del)) | |
| 1050 | 1123 | if templ_7745c5c3_Err != nil { | |
| 1051 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 190, Col: 99} | |
| 1124 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 208, Col: 99} | |
| 1052 | 1125 | } | |
| 1053 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | |
| 1126 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1054 | 1127 | if templ_7745c5c3_Err != nil { | |
| 1055 | 1128 | return templ_7745c5c3_Err | |
| 1056 | 1129 | } | |
| 1057 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</span></span>") | |
| 1130 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</span></span>") | |
| 1058 | 1131 | if templ_7745c5c3_Err != nil { | |
| 1059 | 1132 | return templ_7745c5c3_Err | |
| 1060 | 1133 | } | |
| 1061 | 1134 | } | |
| 1062 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</a></li>") | |
| 1135 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</a></li>") | |
| 1063 | 1136 | if templ_7745c5c3_Err != nil { | |
| 1064 | 1137 | return templ_7745c5c3_Err | |
| 1065 | 1138 | } | |
| 1066 | 1139 | } | |
| 1067 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</ol>") | |
| 1140 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</ol>") | |
| 1068 | 1141 | if templ_7745c5c3_Err != nil { | |
| 1069 | 1142 | return templ_7745c5c3_Err | |
| 1070 | 1143 | } | |
| @@ -1092,270 +1165,283 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1092 | 1165 | }() | |
| 1093 | 1166 | } | |
| 1094 | 1167 | ctx = templ.InitializeContext(ctx) | |
| 1095 | - | templ_7745c5c3_Var53 := templ.GetChildren(ctx) | |
| 1096 | - | if templ_7745c5c3_Var53 == nil { | |
| 1097 | - | templ_7745c5c3_Var53 = templ.NopComponent | |
| 1168 | + | templ_7745c5c3_Var56 := templ.GetChildren(ctx) | |
| 1169 | + | if templ_7745c5c3_Var56 == nil { | |
| 1170 | + | templ_7745c5c3_Var56 = templ.NopComponent | |
| 1098 | 1171 | } | |
| 1099 | 1172 | ctx = templ.ClearChildren(ctx) | |
| 1100 | - | var templ_7745c5c3_Var54 = []any{"dfile", "lnw-" + itoa(f.Digits)} | |
| 1101 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var54...) | |
| 1173 | + | var templ_7745c5c3_Var57 = []any{"dfile", "lnw-" + itoa(f.Digits)} | |
| 1174 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var57...) | |
| 1102 | 1175 | if templ_7745c5c3_Err != nil { | |
| 1103 | 1176 | return templ_7745c5c3_Err | |
| 1104 | 1177 | } | |
| 1105 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<details class=\"") | |
| 1178 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<details class=\"") | |
| 1106 | 1179 | if templ_7745c5c3_Err != nil { | |
| 1107 | 1180 | return templ_7745c5c3_Err | |
| 1108 | 1181 | } | |
| 1109 | - | var templ_7745c5c3_Var55 string | |
| 1110 | - | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var54).String()) | |
| 1182 | + | var templ_7745c5c3_Var58 string | |
| 1183 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var57).String()) | |
| 1111 | 1184 | if templ_7745c5c3_Err != nil { | |
| 1112 | 1185 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 1, Col: 0} | |
| 1113 | 1186 | } | |
| 1114 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var55) | |
| 1187 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58) | |
| 1115 | 1188 | if templ_7745c5c3_Err != nil { | |
| 1116 | 1189 | return templ_7745c5c3_Err | |
| 1117 | 1190 | } | |
| 1118 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\" id=\"") | |
| 1191 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\" id=\"") | |
| 1119 | 1192 | if templ_7745c5c3_Err != nil { | |
| 1120 | 1193 | return templ_7745c5c3_Err | |
| 1121 | 1194 | } | |
| 1122 | - | var templ_7745c5c3_Var56 string | |
| 1123 | - | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID) | |
| 1195 | + | var templ_7745c5c3_Var59 string | |
| 1196 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID) | |
| 1124 | 1197 | if templ_7745c5c3_Err != nil { | |
| 1125 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 205, Col: 11} | |
| 1198 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 223, Col: 11} | |
| 1126 | 1199 | } | |
| 1127 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56) | |
| 1200 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59) | |
| 1128 | 1201 | if templ_7745c5c3_Err != nil { | |
| 1129 | 1202 | return templ_7745c5c3_Err | |
| 1130 | 1203 | } | |
| 1131 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\" data-file") | |
| 1204 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\" data-file=\"") | |
| 1205 | + | if templ_7745c5c3_Err != nil { | |
| 1206 | + | return templ_7745c5c3_Err | |
| 1207 | + | } | |
| 1208 | + | var templ_7745c5c3_Var60 string | |
| 1209 | + | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Key) | |
| 1210 | + | if templ_7745c5c3_Err != nil { | |
| 1211 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 224, Col: 19} | |
| 1212 | + | } | |
| 1213 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60) | |
| 1214 | + | if templ_7745c5c3_Err != nil { | |
| 1215 | + | return templ_7745c5c3_Err | |
| 1216 | + | } | |
| 1217 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\"") | |
| 1132 | 1218 | if templ_7745c5c3_Err != nil { | |
| 1133 | 1219 | return templ_7745c5c3_Err | |
| 1134 | 1220 | } | |
| 1135 | 1221 | if f.Open { | |
| 1136 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, " open") | |
| 1222 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, " open") | |
| 1137 | 1223 | if templ_7745c5c3_Err != nil { | |
| 1138 | 1224 | return templ_7745c5c3_Err | |
| 1139 | 1225 | } | |
| 1140 | 1226 | } | |
| 1141 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "><summary><span class=\"st\">") | |
| 1227 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "><summary><span class=\"st\">") | |
| 1142 | 1228 | if templ_7745c5c3_Err != nil { | |
| 1143 | 1229 | return templ_7745c5c3_Err | |
| 1144 | 1230 | } | |
| 1145 | - | var templ_7745c5c3_Var57 string | |
| 1146 | - | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(f.Status) | |
| 1231 | + | var templ_7745c5c3_Var61 string | |
| 1232 | + | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(f.Status) | |
| 1147 | 1233 | if templ_7745c5c3_Err != nil { | |
| 1148 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 30} | |
| 1234 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 30} | |
| 1149 | 1235 | } | |
| 1150 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1236 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1151 | 1237 | if templ_7745c5c3_Err != nil { | |
| 1152 | 1238 | return templ_7745c5c3_Err | |
| 1153 | 1239 | } | |
| 1154 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</span>") | |
| 1240 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</span>") | |
| 1155 | 1241 | if templ_7745c5c3_Err != nil { | |
| 1156 | 1242 | return templ_7745c5c3_Err | |
| 1157 | 1243 | } | |
| 1158 | - | var templ_7745c5c3_Var58 string | |
| 1159 | - | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1244 | + | var templ_7745c5c3_Var62 string | |
| 1245 | + | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1160 | 1246 | if templ_7745c5c3_Err != nil { | |
| 1161 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 44} | |
| 1247 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 44} | |
| 1162 | 1248 | } | |
| 1163 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1249 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1164 | 1250 | if templ_7745c5c3_Err != nil { | |
| 1165 | 1251 | return templ_7745c5c3_Err | |
| 1166 | 1252 | } | |
| 1167 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "<span class=\"p\">") | |
| 1253 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<span class=\"p\">") | |
| 1168 | 1254 | if templ_7745c5c3_Err != nil { | |
| 1169 | 1255 | return templ_7745c5c3_Err | |
| 1170 | 1256 | } | |
| 1171 | - | var templ_7745c5c3_Var59 string | |
| 1172 | - | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(f.Path) | |
| 1257 | + | var templ_7745c5c3_Var63 string | |
| 1258 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(f.Path) | |
| 1173 | 1259 | if templ_7745c5c3_Err != nil { | |
| 1174 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 70} | |
| 1260 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 70} | |
| 1175 | 1261 | } | |
| 1176 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1262 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | |
| 1177 | 1263 | if templ_7745c5c3_Err != nil { | |
| 1178 | 1264 | return templ_7745c5c3_Err | |
| 1179 | 1265 | } | |
| 1180 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</span>") | |
| 1266 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "</span>") | |
| 1181 | 1267 | if templ_7745c5c3_Err != nil { | |
| 1182 | 1268 | return templ_7745c5c3_Err | |
| 1183 | 1269 | } | |
| 1184 | - | var templ_7745c5c3_Var60 string | |
| 1185 | - | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1270 | + | var templ_7745c5c3_Var64 string | |
| 1271 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1186 | 1272 | if templ_7745c5c3_Err != nil { | |
| 1187 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 84} | |
| 1273 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 84} | |
| 1188 | 1274 | } | |
| 1189 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) | |
| 1275 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1190 | 1276 | if templ_7745c5c3_Err != nil { | |
| 1191 | 1277 | return templ_7745c5c3_Err | |
| 1192 | 1278 | } | |
| 1193 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<span class=\"plus\">+") | |
| 1279 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<span class=\"plus\">+") | |
| 1194 | 1280 | if templ_7745c5c3_Err != nil { | |
| 1195 | 1281 | return templ_7745c5c3_Err | |
| 1196 | 1282 | } | |
| 1197 | - | var templ_7745c5c3_Var61 string | |
| 1198 | - | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Add)) | |
| 1283 | + | var templ_7745c5c3_Var65 string | |
| 1284 | + | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Add)) | |
| 1199 | 1285 | if templ_7745c5c3_Err != nil { | |
| 1200 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 119} | |
| 1286 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 119} | |
| 1201 | 1287 | } | |
| 1202 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1288 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1203 | 1289 | if templ_7745c5c3_Err != nil { | |
| 1204 | 1290 | return templ_7745c5c3_Err | |
| 1205 | 1291 | } | |
| 1206 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</span>") | |
| 1292 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span>") | |
| 1207 | 1293 | if templ_7745c5c3_Err != nil { | |
| 1208 | 1294 | return templ_7745c5c3_Err | |
| 1209 | 1295 | } | |
| 1210 | - | var templ_7745c5c3_Var62 string | |
| 1211 | - | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1296 | + | var templ_7745c5c3_Var66 string | |
| 1297 | + | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1212 | 1298 | if templ_7745c5c3_Err != nil { | |
| 1213 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 133} | |
| 1299 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 133} | |
| 1214 | 1300 | } | |
| 1215 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1301 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | |
| 1216 | 1302 | if templ_7745c5c3_Err != nil { | |
| 1217 | 1303 | return templ_7745c5c3_Err | |
| 1218 | 1304 | } | |
| 1219 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<span class=\"minus\">-") | |
| 1305 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<span class=\"minus\">-") | |
| 1220 | 1306 | if templ_7745c5c3_Err != nil { | |
| 1221 | 1307 | return templ_7745c5c3_Err | |
| 1222 | 1308 | } | |
| 1223 | - | var templ_7745c5c3_Var63 string | |
| 1224 | - | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Del)) | |
| 1309 | + | var templ_7745c5c3_Var67 string | |
| 1310 | + | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(f.Del)) | |
| 1225 | 1311 | if templ_7745c5c3_Err != nil { | |
| 1226 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 212, Col: 169} | |
| 1312 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 230, Col: 169} | |
| 1227 | 1313 | } | |
| 1228 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) | |
| 1314 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) | |
| 1229 | 1315 | if templ_7745c5c3_Err != nil { | |
| 1230 | 1316 | return templ_7745c5c3_Err | |
| 1231 | 1317 | } | |
| 1232 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</span> ") | |
| 1318 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span> ") | |
| 1233 | 1319 | if templ_7745c5c3_Err != nil { | |
| 1234 | 1320 | return templ_7745c5c3_Err | |
| 1235 | 1321 | } | |
| 1236 | 1322 | if f.Note != "" { | |
| 1237 | - | var templ_7745c5c3_Var64 string | |
| 1238 | - | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1323 | + | var templ_7745c5c3_Var68 string | |
| 1324 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1239 | 1325 | if templ_7745c5c3_Err != nil { | |
| 1240 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 214, Col: 9} | |
| 1326 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 232, Col: 9} | |
| 1241 | 1327 | } | |
| 1242 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1328 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1243 | 1329 | if templ_7745c5c3_Err != nil { | |
| 1244 | 1330 | return templ_7745c5c3_Err | |
| 1245 | 1331 | } | |
| 1246 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<span class=\"note\">") | |
| 1332 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<span class=\"note\">") | |
| 1247 | 1333 | if templ_7745c5c3_Err != nil { | |
| 1248 | 1334 | return templ_7745c5c3_Err | |
| 1249 | 1335 | } | |
| 1250 | - | var templ_7745c5c3_Var65 string | |
| 1251 | - | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(f.Note) | |
| 1336 | + | var templ_7745c5c3_Var69 string | |
| 1337 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(f.Note) | |
| 1252 | 1338 | if templ_7745c5c3_Err != nil { | |
| 1253 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 214, Col: 38} | |
| 1339 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 232, Col: 38} | |
| 1254 | 1340 | } | |
| 1255 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1341 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1256 | 1342 | if templ_7745c5c3_Err != nil { | |
| 1257 | 1343 | return templ_7745c5c3_Err | |
| 1258 | 1344 | } | |
| 1259 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "</span>") | |
| 1345 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</span>") | |
| 1260 | 1346 | if templ_7745c5c3_Err != nil { | |
| 1261 | 1347 | return templ_7745c5c3_Err | |
| 1262 | 1348 | } | |
| 1263 | 1349 | } | |
| 1264 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</summary> ") | |
| 1350 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</summary> ") | |
| 1265 | 1351 | if templ_7745c5c3_Err != nil { | |
| 1266 | 1352 | return templ_7745c5c3_Err | |
| 1267 | 1353 | } | |
| 1268 | 1354 | if f.Lazy { | |
| 1269 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " ") | |
| 1355 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, " ") | |
| 1270 | 1356 | if templ_7745c5c3_Err != nil { | |
| 1271 | 1357 | return templ_7745c5c3_Err | |
| 1272 | 1358 | } | |
| 1273 | - | var templ_7745c5c3_Var66 = []any{"dlazy", "dlazy-" + itoa(f.Height)} | |
| 1274 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var66...) | |
| 1359 | + | var templ_7745c5c3_Var70 = []any{"dlazy", "dlazy-" + itoa(f.Height)} | |
| 1360 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var70...) | |
| 1275 | 1361 | if templ_7745c5c3_Err != nil { | |
| 1276 | 1362 | return templ_7745c5c3_Err | |
| 1277 | 1363 | } | |
| 1278 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<div class=\"") | |
| 1364 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<div class=\"") | |
| 1279 | 1365 | if templ_7745c5c3_Err != nil { | |
| 1280 | 1366 | return templ_7745c5c3_Err | |
| 1281 | 1367 | } | |
| 1282 | - | var templ_7745c5c3_Var67 string | |
| 1283 | - | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var66).String()) | |
| 1368 | + | var templ_7745c5c3_Var71 string | |
| 1369 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var70).String()) | |
| 1284 | 1370 | if templ_7745c5c3_Err != nil { | |
| 1285 | 1371 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 1, Col: 0} | |
| 1286 | 1372 | } | |
| 1287 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67) | |
| 1373 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71) | |
| 1288 | 1374 | if templ_7745c5c3_Err != nil { | |
| 1289 | 1375 | return templ_7745c5c3_Err | |
| 1290 | 1376 | } | |
| 1291 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\" data-src=\"") | |
| 1377 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\" data-src=\"") | |
| 1292 | 1378 | if templ_7745c5c3_Err != nil { | |
| 1293 | 1379 | return templ_7745c5c3_Err | |
| 1294 | 1380 | } | |
| 1295 | - | var templ_7745c5c3_Var68 string | |
| 1296 | - | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Src) | |
| 1381 | + | var templ_7745c5c3_Var72 string | |
| 1382 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Src) | |
| 1297 | 1383 | if templ_7745c5c3_Err != nil { | |
| 1298 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 221, Col: 69} | |
| 1384 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 239, Col: 69} | |
| 1299 | 1385 | } | |
| 1300 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68) | |
| 1386 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72) | |
| 1301 | 1387 | if templ_7745c5c3_Err != nil { | |
| 1302 | 1388 | return templ_7745c5c3_Err | |
| 1303 | 1389 | } | |
| 1304 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\" data-notes=\"") | |
| 1390 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\" data-notes=\"") | |
| 1305 | 1391 | if templ_7745c5c3_Err != nil { | |
| 1306 | 1392 | return templ_7745c5c3_Err | |
| 1307 | 1393 | } | |
| 1308 | - | var templ_7745c5c3_Var69 string | |
| 1309 | - | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Comments) | |
| 1394 | + | var templ_7745c5c3_Var73 string | |
| 1395 | + | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Comments) | |
| 1310 | 1396 | if templ_7745c5c3_Err != nil { | |
| 1311 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 221, Col: 95} | |
| 1397 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 239, Col: 95} | |
| 1312 | 1398 | } | |
| 1313 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69) | |
| 1399 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var73) | |
| 1314 | 1400 | if templ_7745c5c3_Err != nil { | |
| 1315 | 1401 | return templ_7745c5c3_Err | |
| 1316 | 1402 | } | |
| 1317 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\"><a href=\"") | |
| 1403 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\"><a href=\"") | |
| 1318 | 1404 | if templ_7745c5c3_Err != nil { | |
| 1319 | 1405 | return templ_7745c5c3_Err | |
| 1320 | 1406 | } | |
| 1321 | - | var templ_7745c5c3_Var70 templ.SafeURL | |
| 1322 | - | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("?from=" + itoa(f.From) + "#" + f.ID)) | |
| 1407 | + | var templ_7745c5c3_Var74 templ.SafeURL | |
| 1408 | + | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("?from=" + itoa(f.From) + "#" + f.ID)) | |
| 1323 | 1409 | if templ_7745c5c3_Err != nil { | |
| 1324 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 222, Col: 65} | |
| 1410 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 240, Col: 65} | |
| 1325 | 1411 | } | |
| 1326 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1412 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74)) | |
| 1327 | 1413 | if templ_7745c5c3_Err != nil { | |
| 1328 | 1414 | return templ_7745c5c3_Err | |
| 1329 | 1415 | } | |
| 1330 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\">show this file</a></div>") | |
| 1416 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\">show this file</a></div>") | |
| 1331 | 1417 | if templ_7745c5c3_Err != nil { | |
| 1332 | 1418 | return templ_7745c5c3_Err | |
| 1333 | 1419 | } | |
| 1334 | 1420 | } else if len(f.Hunks) > 0 { | |
| 1335 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<div class=\"dscroll\">") | |
| 1421 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<div class=\"dscroll\">") | |
| 1336 | 1422 | if templ_7745c5c3_Err != nil { | |
| 1337 | 1423 | return templ_7745c5c3_Err | |
| 1338 | 1424 | } | |
| 1339 | 1425 | if p.ShowUnified { | |
| 1340 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<table class=\"dt uni\" aria-label=\"") | |
| 1426 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<table class=\"dt uni\" aria-label=\"") | |
| 1341 | 1427 | if templ_7745c5c3_Err != nil { | |
| 1342 | 1428 | return templ_7745c5c3_Err | |
| 1343 | 1429 | } | |
| 1344 | - | var templ_7745c5c3_Var71 string | |
| 1345 | - | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path + ", unified") | |
| 1430 | + | var templ_7745c5c3_Var75 string | |
| 1431 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path + ", unified") | |
| 1346 | 1432 | if templ_7745c5c3_Err != nil { | |
| 1347 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 227, Col: 60} | |
| 1433 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 245, Col: 60} | |
| 1348 | 1434 | } | |
| 1349 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71) | |
| 1435 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75) | |
| 1350 | 1436 | if templ_7745c5c3_Err != nil { | |
| 1351 | 1437 | return templ_7745c5c3_Err | |
| 1352 | 1438 | } | |
| 1353 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\">") | |
| 1439 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\">") | |
| 1354 | 1440 | if templ_7745c5c3_Err != nil { | |
| 1355 | 1441 | return templ_7745c5c3_Err | |
| 1356 | 1442 | } | |
| 1357 | 1443 | for _, h := range f.Hunks { | |
| 1358 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<tbody>") | |
| 1444 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<tbody>") | |
| 1359 | 1445 | if templ_7745c5c3_Err != nil { | |
| 1360 | 1446 | return templ_7745c5c3_Err | |
| 1361 | 1447 | } | |
| @@ -1365,43 +1451,43 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1365 | 1451 | return templ_7745c5c3_Err | |
| 1366 | 1452 | } | |
| 1367 | 1453 | } | |
| 1368 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<tr class=\"hunk\"><td colspan=\"4\">") | |
| 1454 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<tr class=\"hunk\"><td colspan=\"4\">") | |
| 1369 | 1455 | if templ_7745c5c3_Err != nil { | |
| 1370 | 1456 | return templ_7745c5c3_Err | |
| 1371 | 1457 | } | |
| 1372 | - | var templ_7745c5c3_Var72 string | |
| 1373 | - | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(h.Header) | |
| 1458 | + | var templ_7745c5c3_Var76 string | |
| 1459 | + | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(h.Header) | |
| 1374 | 1460 | if templ_7745c5c3_Err != nil { | |
| 1375 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 233, Col: 51} | |
| 1461 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 251, Col: 51} | |
| 1376 | 1462 | } | |
| 1377 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1463 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | |
| 1378 | 1464 | if templ_7745c5c3_Err != nil { | |
| 1379 | 1465 | return templ_7745c5c3_Err | |
| 1380 | 1466 | } | |
| 1381 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</td></tr>") | |
| 1467 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</td></tr>") | |
| 1382 | 1468 | if templ_7745c5c3_Err != nil { | |
| 1383 | 1469 | return templ_7745c5c3_Err | |
| 1384 | 1470 | } | |
| 1385 | 1471 | for _, ln := range h.Lines { | |
| 1386 | - | var templ_7745c5c3_Var73 = []any{ln.Kind, templ.KV("sel", ln.Selected)} | |
| 1387 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var73...) | |
| 1472 | + | var templ_7745c5c3_Var77 = []any{ln.Kind, templ.KV("sel", ln.Selected)} | |
| 1473 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var77...) | |
| 1388 | 1474 | if templ_7745c5c3_Err != nil { | |
| 1389 | 1475 | return templ_7745c5c3_Err | |
| 1390 | 1476 | } | |
| 1391 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<tr class=\"") | |
| 1477 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<tr class=\"") | |
| 1392 | 1478 | if templ_7745c5c3_Err != nil { | |
| 1393 | 1479 | return templ_7745c5c3_Err | |
| 1394 | 1480 | } | |
| 1395 | - | var templ_7745c5c3_Var74 string | |
| 1396 | - | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var73).String()) | |
| 1481 | + | var templ_7745c5c3_Var78 string | |
| 1482 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var77).String()) | |
| 1397 | 1483 | if templ_7745c5c3_Err != nil { | |
| 1398 | 1484 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 1, Col: 0} | |
| 1399 | 1485 | } | |
| 1400 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1486 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) | |
| 1401 | 1487 | if templ_7745c5c3_Err != nil { | |
| 1402 | 1488 | return templ_7745c5c3_Err | |
| 1403 | 1489 | } | |
| 1404 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\"><td class=\"ln\">") | |
| 1490 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "\"><td class=\"ln\">") | |
| 1405 | 1491 | if templ_7745c5c3_Err != nil { | |
| 1406 | 1492 | return templ_7745c5c3_Err | |
| 1407 | 1493 | } | |
| @@ -1409,7 +1495,7 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1409 | 1495 | if templ_7745c5c3_Err != nil { | |
| 1410 | 1496 | return templ_7745c5c3_Err | |
| 1411 | 1497 | } | |
| 1412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</td><td class=\"ln\">") | |
| 1498 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</td><td class=\"ln\">") | |
| 1413 | 1499 | if templ_7745c5c3_Err != nil { | |
| 1414 | 1500 | return templ_7745c5c3_Err | |
| 1415 | 1501 | } | |
| @@ -1417,20 +1503,20 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1417 | 1503 | if templ_7745c5c3_Err != nil { | |
| 1418 | 1504 | return templ_7745c5c3_Err | |
| 1419 | 1505 | } | |
| 1420 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</td><td class=\"m\">") | |
| 1506 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</td><td class=\"m\">") | |
| 1421 | 1507 | if templ_7745c5c3_Err != nil { | |
| 1422 | 1508 | return templ_7745c5c3_Err | |
| 1423 | 1509 | } | |
| 1424 | - | var templ_7745c5c3_Var75 string | |
| 1425 | - | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Mark) | |
| 1510 | + | var templ_7745c5c3_Var79 string | |
| 1511 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Mark) | |
| 1426 | 1512 | if templ_7745c5c3_Err != nil { | |
| 1427 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 238, Col: 33} | |
| 1513 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 256, Col: 33} | |
| 1428 | 1514 | } | |
| 1429 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1515 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) | |
| 1430 | 1516 | if templ_7745c5c3_Err != nil { | |
| 1431 | 1517 | return templ_7745c5c3_Err | |
| 1432 | 1518 | } | |
| 1433 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "</td><td class=\"tx chroma\">") | |
| 1519 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</td><td class=\"tx chroma\">") | |
| 1434 | 1520 | if templ_7745c5c3_Err != nil { | |
| 1435 | 1521 | return templ_7745c5c3_Err | |
| 1436 | 1522 | } | |
| @@ -1438,12 +1524,12 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1438 | 1524 | if templ_7745c5c3_Err != nil { | |
| 1439 | 1525 | return templ_7745c5c3_Err | |
| 1440 | 1526 | } | |
| 1441 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</td></tr>") | |
| 1527 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "</td></tr>") | |
| 1442 | 1528 | if templ_7745c5c3_Err != nil { | |
| 1443 | 1529 | return templ_7745c5c3_Err | |
| 1444 | 1530 | } | |
| 1445 | 1531 | if len(ln.Notes) > 0 || ln.Form != nil { | |
| 1446 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<tr class=\"fnote\"><td colspan=\"3\"></td><td>") | |
| 1532 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "<tr class=\"fnote\"><td colspan=\"3\"></td><td>") | |
| 1447 | 1533 | if templ_7745c5c3_Err != nil { | |
| 1448 | 1534 | return templ_7745c5c3_Err | |
| 1449 | 1535 | } | |
| @@ -1451,19 +1537,19 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1451 | 1537 | if templ_7745c5c3_Err != nil { | |
| 1452 | 1538 | return templ_7745c5c3_Err | |
| 1453 | 1539 | } | |
| 1454 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</td></tr>") | |
| 1540 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "</td></tr>") | |
| 1455 | 1541 | if templ_7745c5c3_Err != nil { | |
| 1456 | 1542 | return templ_7745c5c3_Err | |
| 1457 | 1543 | } | |
| 1458 | 1544 | } | |
| 1459 | 1545 | } | |
| 1460 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</tbody> ") | |
| 1546 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</tbody> ") | |
| 1461 | 1547 | if templ_7745c5c3_Err != nil { | |
| 1462 | 1548 | return templ_7745c5c3_Err | |
| 1463 | 1549 | } | |
| 1464 | 1550 | } | |
| 1465 | 1551 | if f.Tail != nil { | |
| 1466 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "<tbody>") | |
| 1552 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<tbody>") | |
| 1467 | 1553 | if templ_7745c5c3_Err != nil { | |
| 1468 | 1554 | return templ_7745c5c3_Err | |
| 1469 | 1555 | } | |
| @@ -1471,36 +1557,36 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1471 | 1557 | if templ_7745c5c3_Err != nil { | |
| 1472 | 1558 | return templ_7745c5c3_Err | |
| 1473 | 1559 | } | |
| 1474 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</tbody>") | |
| 1560 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</tbody>") | |
| 1475 | 1561 | if templ_7745c5c3_Err != nil { | |
| 1476 | 1562 | return templ_7745c5c3_Err | |
| 1477 | 1563 | } | |
| 1478 | 1564 | } | |
| 1479 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "</table>") | |
| 1565 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "</table>") | |
| 1480 | 1566 | if templ_7745c5c3_Err != nil { | |
| 1481 | 1567 | return templ_7745c5c3_Err | |
| 1482 | 1568 | } | |
| 1483 | 1569 | } | |
| 1484 | 1570 | if p.ShowSplit { | |
| 1485 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "<table class=\"dt spl\" aria-label=\"") | |
| 1571 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<table class=\"dt spl\" aria-label=\"") | |
| 1486 | 1572 | if templ_7745c5c3_Err != nil { | |
| 1487 | 1573 | return templ_7745c5c3_Err | |
| 1488 | 1574 | } | |
| 1489 | - | var templ_7745c5c3_Var76 string | |
| 1490 | - | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path + ", split") | |
| 1575 | + | var templ_7745c5c3_Var80 string | |
| 1576 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path + ", split") | |
| 1491 | 1577 | if templ_7745c5c3_Err != nil { | |
| 1492 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 258, Col: 58} | |
| 1578 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 276, Col: 58} | |
| 1493 | 1579 | } | |
| 1494 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76) | |
| 1580 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1495 | 1581 | if templ_7745c5c3_Err != nil { | |
| 1496 | 1582 | return templ_7745c5c3_Err | |
| 1497 | 1583 | } | |
| 1498 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "\"><colgroup><col class=\"c-ln\"><col class=\"c-m\"><col><col class=\"c-ln\"><col class=\"c-m\"><col></colgroup> ") | |
| 1584 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\"><colgroup><col class=\"c-ln\"><col class=\"c-m\"><col><col class=\"c-ln\"><col class=\"c-m\"><col></colgroup> ") | |
| 1499 | 1585 | if templ_7745c5c3_Err != nil { | |
| 1500 | 1586 | return templ_7745c5c3_Err | |
| 1501 | 1587 | } | |
| 1502 | 1588 | for _, h := range f.Hunks { | |
| 1503 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<tbody>") | |
| 1589 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<tbody>") | |
| 1504 | 1590 | if templ_7745c5c3_Err != nil { | |
| 1505 | 1591 | return templ_7745c5c3_Err | |
| 1506 | 1592 | } | |
| @@ -1510,25 +1596,25 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1510 | 1596 | return templ_7745c5c3_Err | |
| 1511 | 1597 | } | |
| 1512 | 1598 | } | |
| 1513 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<tr class=\"hunk\"><td colspan=\"6\">") | |
| 1599 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<tr class=\"hunk\"><td colspan=\"6\">") | |
| 1514 | 1600 | if templ_7745c5c3_Err != nil { | |
| 1515 | 1601 | return templ_7745c5c3_Err | |
| 1516 | 1602 | } | |
| 1517 | - | var templ_7745c5c3_Var77 string | |
| 1518 | - | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(h.Header) | |
| 1603 | + | var templ_7745c5c3_Var81 string | |
| 1604 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(h.Header) | |
| 1519 | 1605 | if templ_7745c5c3_Err != nil { | |
| 1520 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 265, Col: 51} | |
| 1606 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 283, Col: 51} | |
| 1521 | 1607 | } | |
| 1522 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) | |
| 1608 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | |
| 1523 | 1609 | if templ_7745c5c3_Err != nil { | |
| 1524 | 1610 | return templ_7745c5c3_Err | |
| 1525 | 1611 | } | |
| 1526 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</td></tr>") | |
| 1612 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "</td></tr>") | |
| 1527 | 1613 | if templ_7745c5c3_Err != nil { | |
| 1528 | 1614 | return templ_7745c5c3_Err | |
| 1529 | 1615 | } | |
| 1530 | 1616 | for _, row := range h.Split { | |
| 1531 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<tr>") | |
| 1617 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<tr>") | |
| 1532 | 1618 | if templ_7745c5c3_Err != nil { | |
| 1533 | 1619 | return templ_7745c5c3_Err | |
| 1534 | 1620 | } | |
| @@ -1540,12 +1626,12 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1540 | 1626 | if templ_7745c5c3_Err != nil { | |
| 1541 | 1627 | return templ_7745c5c3_Err | |
| 1542 | 1628 | } | |
| 1543 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "</tr>") | |
| 1629 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "</tr>") | |
| 1544 | 1630 | if templ_7745c5c3_Err != nil { | |
| 1545 | 1631 | return templ_7745c5c3_Err | |
| 1546 | 1632 | } | |
| 1547 | 1633 | if notes, form := row.Notes(), row.Form(); len(notes) > 0 || form != nil { | |
| 1548 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<tr class=\"fnote\"><td colspan=\"6\">") | |
| 1634 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<tr class=\"fnote\"><td colspan=\"6\">") | |
| 1549 | 1635 | if templ_7745c5c3_Err != nil { | |
| 1550 | 1636 | return templ_7745c5c3_Err | |
| 1551 | 1637 | } | |
| @@ -1553,19 +1639,19 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1553 | 1639 | if templ_7745c5c3_Err != nil { | |
| 1554 | 1640 | return templ_7745c5c3_Err | |
| 1555 | 1641 | } | |
| 1556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "</td></tr>") | |
| 1642 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</td></tr>") | |
| 1557 | 1643 | if templ_7745c5c3_Err != nil { | |
| 1558 | 1644 | return templ_7745c5c3_Err | |
| 1559 | 1645 | } | |
| 1560 | 1646 | } | |
| 1561 | 1647 | } | |
| 1562 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "</tbody> ") | |
| 1648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</tbody> ") | |
| 1563 | 1649 | if templ_7745c5c3_Err != nil { | |
| 1564 | 1650 | return templ_7745c5c3_Err | |
| 1565 | 1651 | } | |
| 1566 | 1652 | } | |
| 1567 | 1653 | if f.Tail != nil { | |
| 1568 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "<tbody>") | |
| 1654 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<tbody>") | |
| 1569 | 1655 | if templ_7745c5c3_Err != nil { | |
| 1570 | 1656 | return templ_7745c5c3_Err | |
| 1571 | 1657 | } | |
| @@ -1573,48 +1659,48 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1573 | 1659 | if templ_7745c5c3_Err != nil { | |
| 1574 | 1660 | return templ_7745c5c3_Err | |
| 1575 | 1661 | } | |
| 1576 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</tbody>") | |
| 1662 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "</tbody>") | |
| 1577 | 1663 | if templ_7745c5c3_Err != nil { | |
| 1578 | 1664 | return templ_7745c5c3_Err | |
| 1579 | 1665 | } | |
| 1580 | 1666 | } | |
| 1581 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "</table>") | |
| 1667 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "</table>") | |
| 1582 | 1668 | if templ_7745c5c3_Err != nil { | |
| 1583 | 1669 | return templ_7745c5c3_Err | |
| 1584 | 1670 | } | |
| 1585 | 1671 | } | |
| 1586 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "</div>") | |
| 1672 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "</div>") | |
| 1587 | 1673 | if templ_7745c5c3_Err != nil { | |
| 1588 | 1674 | return templ_7745c5c3_Err | |
| 1589 | 1675 | } | |
| 1590 | 1676 | } else { | |
| 1591 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "<p class=\"note pad\">") | |
| 1677 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "<p class=\"note pad\">") | |
| 1592 | 1678 | if templ_7745c5c3_Err != nil { | |
| 1593 | 1679 | return templ_7745c5c3_Err | |
| 1594 | 1680 | } | |
| 1595 | 1681 | if f.Note != "" { | |
| 1596 | - | var templ_7745c5c3_Var78 string | |
| 1597 | - | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(f.Note) | |
| 1682 | + | var templ_7745c5c3_Var82 string | |
| 1683 | + | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.JoinStringErrs(f.Note) | |
| 1598 | 1684 | if templ_7745c5c3_Err != nil { | |
| 1599 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 290, Col: 13} | |
| 1685 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 308, Col: 13} | |
| 1600 | 1686 | } | |
| 1601 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | |
| 1687 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var82)) | |
| 1602 | 1688 | if templ_7745c5c3_Err != nil { | |
| 1603 | 1689 | return templ_7745c5c3_Err | |
| 1604 | 1690 | } | |
| 1605 | 1691 | } else { | |
| 1606 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "no textual change.") | |
| 1692 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "no textual change.") | |
| 1607 | 1693 | if templ_7745c5c3_Err != nil { | |
| 1608 | 1694 | return templ_7745c5c3_Err | |
| 1609 | 1695 | } | |
| 1610 | 1696 | } | |
| 1611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</p>") | |
| 1697 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</p>") | |
| 1612 | 1698 | if templ_7745c5c3_Err != nil { | |
| 1613 | 1699 | return templ_7745c5c3_Err | |
| 1614 | 1700 | } | |
| 1615 | 1701 | } | |
| 1616 | 1702 | if len(f.Loose) > 0 { | |
| 1617 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "<div class=\"dloose\">") | |
| 1703 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<div class=\"dloose\">") | |
| 1618 | 1704 | if templ_7745c5c3_Err != nil { | |
| 1619 | 1705 | return templ_7745c5c3_Err | |
| 1620 | 1706 | } | |
| @@ -1624,12 +1710,12 @@ func DiffFileView(f DiffFile, p DiffView) templ.Component { | |||
| 1624 | 1710 | return templ_7745c5c3_Err | |
| 1625 | 1711 | } | |
| 1626 | 1712 | } | |
| 1627 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "</div>") | |
| 1713 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "</div>") | |
| 1628 | 1714 | if templ_7745c5c3_Err != nil { | |
| 1629 | 1715 | return templ_7745c5c3_Err | |
| 1630 | 1716 | } | |
| 1631 | 1717 | } | |
| 1632 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "</details>") | |
| 1718 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</details>") | |
| 1633 | 1719 | if templ_7745c5c3_Err != nil { | |
| 1634 | 1720 | return templ_7745c5c3_Err | |
| 1635 | 1721 | } | |
| @@ -1655,9 +1741,9 @@ func MRFiles(p MRFilesPage) templ.Component { | |||
| 1655 | 1741 | }() | |
| 1656 | 1742 | } | |
| 1657 | 1743 | ctx = templ.InitializeContext(ctx) | |
| 1658 | - | templ_7745c5c3_Var79 := templ.GetChildren(ctx) | |
| 1659 | - | if templ_7745c5c3_Var79 == nil { | |
| 1660 | - | templ_7745c5c3_Var79 = templ.NopComponent | |
| 1744 | + | templ_7745c5c3_Var83 := templ.GetChildren(ctx) | |
| 1745 | + | if templ_7745c5c3_Var83 == nil { | |
| 1746 | + | templ_7745c5c3_Var83 = templ.NopComponent | |
| 1661 | 1747 | } | |
| 1662 | 1748 | ctx = templ.ClearChildren(ctx) | |
| 1663 | 1749 | for _, f := range p.Files { | |
| @@ -1688,12 +1774,12 @@ func MRFilesMoved() templ.Component { | |||
| 1688 | 1774 | }() | |
| 1689 | 1775 | } | |
| 1690 | 1776 | ctx = templ.InitializeContext(ctx) | |
| 1691 | - | templ_7745c5c3_Var80 := templ.GetChildren(ctx) | |
| 1692 | - | if templ_7745c5c3_Var80 == nil { | |
| 1693 | - | templ_7745c5c3_Var80 = templ.NopComponent | |
| 1777 | + | templ_7745c5c3_Var84 := templ.GetChildren(ctx) | |
| 1778 | + | if templ_7745c5c3_Var84 == nil { | |
| 1779 | + | templ_7745c5c3_Var84 = templ.NopComponent | |
| 1694 | 1780 | } | |
| 1695 | 1781 | ctx = templ.ClearChildren(ctx) | |
| 1696 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "<p class=\"notice\" data-moved>this merge request changed since the page was loaded — <a href=\"\">reload</a></p>") | |
| 1782 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "<p class=\"notice\" data-moved>this merge request changed since the page was loaded — <a href=\"\">reload</a></p>") | |
| 1697 | 1783 | if templ_7745c5c3_Err != nil { | |
| 1698 | 1784 | return templ_7745c5c3_Err | |
| 1699 | 1785 | } | |
| @@ -1720,64 +1806,64 @@ func gapRow(g DiffGap, cols int) templ.Component { | |||
| 1720 | 1806 | }() | |
| 1721 | 1807 | } | |
| 1722 | 1808 | ctx = templ.InitializeContext(ctx) | |
| 1723 | - | templ_7745c5c3_Var81 := templ.GetChildren(ctx) | |
| 1724 | - | if templ_7745c5c3_Var81 == nil { | |
| 1725 | - | templ_7745c5c3_Var81 = templ.NopComponent | |
| 1809 | + | templ_7745c5c3_Var85 := templ.GetChildren(ctx) | |
| 1810 | + | if templ_7745c5c3_Var85 == nil { | |
| 1811 | + | templ_7745c5c3_Var85 = templ.NopComponent | |
| 1726 | 1812 | } | |
| 1727 | 1813 | ctx = templ.ClearChildren(ctx) | |
| 1728 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "<tr class=\"gap\" data-js data-gap=\"") | |
| 1814 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "<tr class=\"gap\" data-js data-gap=\"") | |
| 1729 | 1815 | if templ_7745c5c3_Err != nil { | |
| 1730 | 1816 | return templ_7745c5c3_Err | |
| 1731 | 1817 | } | |
| 1732 | - | var templ_7745c5c3_Var82 string | |
| 1733 | - | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(g.Key) | |
| 1818 | + | var templ_7745c5c3_Var86 string | |
| 1819 | + | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(g.Key) | |
| 1734 | 1820 | if templ_7745c5c3_Err != nil { | |
| 1735 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 324, Col: 41} | |
| 1821 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 342, Col: 41} | |
| 1736 | 1822 | } | |
| 1737 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1823 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86) | |
| 1738 | 1824 | if templ_7745c5c3_Err != nil { | |
| 1739 | 1825 | return templ_7745c5c3_Err | |
| 1740 | 1826 | } | |
| 1741 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "\"><td colspan=\"") | |
| 1827 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "\"><td colspan=\"") | |
| 1742 | 1828 | if templ_7745c5c3_Err != nil { | |
| 1743 | 1829 | return templ_7745c5c3_Err | |
| 1744 | 1830 | } | |
| 1745 | - | var templ_7745c5c3_Var83 string | |
| 1746 | - | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(cols)) | |
| 1831 | + | var templ_7745c5c3_Var87 string | |
| 1832 | + | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(cols)) | |
| 1747 | 1833 | if templ_7745c5c3_Err != nil { | |
| 1748 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 325, Col: 26} | |
| 1834 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 343, Col: 26} | |
| 1749 | 1835 | } | |
| 1750 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | |
| 1836 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87) | |
| 1751 | 1837 | if templ_7745c5c3_Err != nil { | |
| 1752 | 1838 | return templ_7745c5c3_Err | |
| 1753 | 1839 | } | |
| 1754 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "\"><button type=\"button\" data-src=\"") | |
| 1840 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "\"><button type=\"button\" data-src=\"") | |
| 1755 | 1841 | if templ_7745c5c3_Err != nil { | |
| 1756 | 1842 | return templ_7745c5c3_Err | |
| 1757 | 1843 | } | |
| 1758 | - | var templ_7745c5c3_Var84 string | |
| 1759 | - | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(g.Src) | |
| 1844 | + | var templ_7745c5c3_Var88 string | |
| 1845 | + | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(g.Src) | |
| 1760 | 1846 | if templ_7745c5c3_Err != nil { | |
| 1761 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 326, Col: 41} | |
| 1847 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 344, Col: 41} | |
| 1762 | 1848 | } | |
| 1763 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) | |
| 1849 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88) | |
| 1764 | 1850 | if templ_7745c5c3_Err != nil { | |
| 1765 | 1851 | return templ_7745c5c3_Err | |
| 1766 | 1852 | } | |
| 1767 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "\"><span aria-hidden=\"true\">⋯</span> ") | |
| 1853 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "\"><span aria-hidden=\"true\">⋯</span> ") | |
| 1768 | 1854 | if templ_7745c5c3_Err != nil { | |
| 1769 | 1855 | return templ_7745c5c3_Err | |
| 1770 | 1856 | } | |
| 1771 | - | var templ_7745c5c3_Var85 string | |
| 1772 | - | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(g.Label()) | |
| 1857 | + | var templ_7745c5c3_Var89 string | |
| 1858 | + | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(g.Label()) | |
| 1773 | 1859 | if templ_7745c5c3_Err != nil { | |
| 1774 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 327, Col: 51} | |
| 1860 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 345, Col: 51} | |
| 1775 | 1861 | } | |
| 1776 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85)) | |
| 1862 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1777 | 1863 | if templ_7745c5c3_Err != nil { | |
| 1778 | 1864 | return templ_7745c5c3_Err | |
| 1779 | 1865 | } | |
| 1780 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</button></td></tr>") | |
| 1866 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "</button></td></tr>") | |
| 1781 | 1867 | if templ_7745c5c3_Err != nil { | |
| 1782 | 1868 | return templ_7745c5c3_Err | |
| 1783 | 1869 | } | |
| @@ -1803,56 +1889,56 @@ func DiffContext(c ContextRows) templ.Component { | |||
| 1803 | 1889 | }() | |
| 1804 | 1890 | } | |
| 1805 | 1891 | ctx = templ.InitializeContext(ctx) | |
| 1806 | - | templ_7745c5c3_Var86 := templ.GetChildren(ctx) | |
| 1807 | - | if templ_7745c5c3_Var86 == nil { | |
| 1808 | - | templ_7745c5c3_Var86 = templ.NopComponent | |
| 1892 | + | templ_7745c5c3_Var90 := templ.GetChildren(ctx) | |
| 1893 | + | if templ_7745c5c3_Var90 == nil { | |
| 1894 | + | templ_7745c5c3_Var90 = templ.NopComponent | |
| 1809 | 1895 | } | |
| 1810 | 1896 | ctx = templ.ClearChildren(ctx) | |
| 1811 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "<table><tbody data-for=\"uni\">") | |
| 1897 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "<table><tbody data-for=\"uni\">") | |
| 1812 | 1898 | if templ_7745c5c3_Err != nil { | |
| 1813 | 1899 | return templ_7745c5c3_Err | |
| 1814 | 1900 | } | |
| 1815 | 1901 | for _, ln := range c.Lines { | |
| 1816 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "<tr class=\"ctx\"><td class=\"ln\">") | |
| 1902 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "<tr class=\"ctx\"><td class=\"ln\">") | |
| 1817 | 1903 | if templ_7745c5c3_Err != nil { | |
| 1818 | 1904 | return templ_7745c5c3_Err | |
| 1819 | 1905 | } | |
| 1820 | - | var templ_7745c5c3_Var87 string | |
| 1821 | - | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Old) | |
| 1906 | + | var templ_7745c5c3_Var91 string | |
| 1907 | + | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Old) | |
| 1822 | 1908 | if templ_7745c5c3_Err != nil { | |
| 1823 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 340, Col: 28} | |
| 1909 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 358, Col: 28} | |
| 1824 | 1910 | } | |
| 1825 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) | |
| 1911 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91)) | |
| 1826 | 1912 | if templ_7745c5c3_Err != nil { | |
| 1827 | 1913 | return templ_7745c5c3_Err | |
| 1828 | 1914 | } | |
| 1829 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "</td><td class=\"ln\">") | |
| 1915 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "</td><td class=\"ln\">") | |
| 1830 | 1916 | if templ_7745c5c3_Err != nil { | |
| 1831 | 1917 | return templ_7745c5c3_Err | |
| 1832 | 1918 | } | |
| 1833 | - | var templ_7745c5c3_Var88 string | |
| 1834 | - | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(ln.New) | |
| 1919 | + | var templ_7745c5c3_Var92 string | |
| 1920 | + | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(ln.New) | |
| 1835 | 1921 | if templ_7745c5c3_Err != nil { | |
| 1836 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 341, Col: 28} | |
| 1922 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 359, Col: 28} | |
| 1837 | 1923 | } | |
| 1838 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) | |
| 1924 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92)) | |
| 1839 | 1925 | if templ_7745c5c3_Err != nil { | |
| 1840 | 1926 | return templ_7745c5c3_Err | |
| 1841 | 1927 | } | |
| 1842 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</td><td class=\"m\">") | |
| 1928 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "</td><td class=\"m\">") | |
| 1843 | 1929 | if templ_7745c5c3_Err != nil { | |
| 1844 | 1930 | return templ_7745c5c3_Err | |
| 1845 | 1931 | } | |
| 1846 | - | var templ_7745c5c3_Var89 string | |
| 1847 | - | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Mark) | |
| 1932 | + | var templ_7745c5c3_Var93 string | |
| 1933 | + | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Mark) | |
| 1848 | 1934 | if templ_7745c5c3_Err != nil { | |
| 1849 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 342, Col: 28} | |
| 1935 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 360, Col: 28} | |
| 1850 | 1936 | } | |
| 1851 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1937 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93)) | |
| 1852 | 1938 | if templ_7745c5c3_Err != nil { | |
| 1853 | 1939 | return templ_7745c5c3_Err | |
| 1854 | 1940 | } | |
| 1855 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "</td><td class=\"tx chroma\">") | |
| 1941 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</td><td class=\"tx chroma\">") | |
| 1856 | 1942 | if templ_7745c5c3_Err != nil { | |
| 1857 | 1943 | return templ_7745c5c3_Err | |
| 1858 | 1944 | } | |
| @@ -1860,7 +1946,7 @@ func DiffContext(c ContextRows) templ.Component { | |||
| 1860 | 1946 | if templ_7745c5c3_Err != nil { | |
| 1861 | 1947 | return templ_7745c5c3_Err | |
| 1862 | 1948 | } | |
| 1863 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "</td></tr>") | |
| 1949 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</td></tr>") | |
| 1864 | 1950 | if templ_7745c5c3_Err != nil { | |
| 1865 | 1951 | return templ_7745c5c3_Err | |
| 1866 | 1952 | } | |
| @@ -1871,12 +1957,12 @@ func DiffContext(c ContextRows) templ.Component { | |||
| 1871 | 1957 | return templ_7745c5c3_Err | |
| 1872 | 1958 | } | |
| 1873 | 1959 | } | |
| 1874 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</tbody> <tbody data-for=\"spl\">") | |
| 1960 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "</tbody> <tbody data-for=\"spl\">") | |
| 1875 | 1961 | if templ_7745c5c3_Err != nil { | |
| 1876 | 1962 | return templ_7745c5c3_Err | |
| 1877 | 1963 | } | |
| 1878 | 1964 | for i := range c.Lines { | |
| 1879 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "<tr>") | |
| 1965 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "<tr>") | |
| 1880 | 1966 | if templ_7745c5c3_Err != nil { | |
| 1881 | 1967 | return templ_7745c5c3_Err | |
| 1882 | 1968 | } | |
| @@ -1888,7 +1974,7 @@ func DiffContext(c ContextRows) templ.Component { | |||
| 1888 | 1974 | if templ_7745c5c3_Err != nil { | |
| 1889 | 1975 | return templ_7745c5c3_Err | |
| 1890 | 1976 | } | |
| 1891 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "</tr>") | |
| 1977 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "</tr>") | |
| 1892 | 1978 | if templ_7745c5c3_Err != nil { | |
| 1893 | 1979 | return templ_7745c5c3_Err | |
| 1894 | 1980 | } | |
| @@ -1899,7 +1985,7 @@ func DiffContext(c ContextRows) templ.Component { | |||
| 1899 | 1985 | return templ_7745c5c3_Err | |
| 1900 | 1986 | } | |
| 1901 | 1987 | } | |
| 1902 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "</tbody></table>") | |
| 1988 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "</tbody></table>") | |
| 1903 | 1989 | if templ_7745c5c3_Err != nil { | |
| 1904 | 1990 | return templ_7745c5c3_Err | |
| 1905 | 1991 | } | |
| @@ -1925,36 +2011,36 @@ func splitSide(ln *DiffLine, old bool) templ.Component { | |||
| 1925 | 2011 | }() | |
| 1926 | 2012 | } | |
| 1927 | 2013 | ctx = templ.InitializeContext(ctx) | |
| 1928 | - | templ_7745c5c3_Var90 := templ.GetChildren(ctx) | |
| 1929 | - | if templ_7745c5c3_Var90 == nil { | |
| 1930 | - | templ_7745c5c3_Var90 = templ.NopComponent | |
| 2014 | + | templ_7745c5c3_Var94 := templ.GetChildren(ctx) | |
| 2015 | + | if templ_7745c5c3_Var94 == nil { | |
| 2016 | + | templ_7745c5c3_Var94 = templ.NopComponent | |
| 1931 | 2017 | } | |
| 1932 | 2018 | ctx = templ.ClearChildren(ctx) | |
| 1933 | 2019 | if ln == nil { | |
| 1934 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "<td class=\"ln none\"></td><td class=\"m none\"></td><td class=\"tx none\"></td>") | |
| 2020 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "<td class=\"ln none\"></td><td class=\"m none\"></td><td class=\"tx none\"></td>") | |
| 1935 | 2021 | if templ_7745c5c3_Err != nil { | |
| 1936 | 2022 | return templ_7745c5c3_Err | |
| 1937 | 2023 | } | |
| 1938 | 2024 | } else { | |
| 1939 | - | var templ_7745c5c3_Var91 = []any{"ln", ln.Kind, templ.KV("sel", ln.Selected)} | |
| 1940 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var91...) | |
| 2025 | + | var templ_7745c5c3_Var95 = []any{"ln", ln.Kind, templ.KV("sel", ln.Selected)} | |
| 2026 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var95...) | |
| 1941 | 2027 | if templ_7745c5c3_Err != nil { | |
| 1942 | 2028 | return templ_7745c5c3_Err | |
| 1943 | 2029 | } | |
| 1944 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<td class=\"") | |
| 2030 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<td class=\"") | |
| 1945 | 2031 | if templ_7745c5c3_Err != nil { | |
| 1946 | 2032 | return templ_7745c5c3_Err | |
| 1947 | 2033 | } | |
| 1948 | - | var templ_7745c5c3_Var92 string | |
| 1949 | - | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var91).String()) | |
| 2034 | + | var templ_7745c5c3_Var96 string | |
| 2035 | + | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var95).String()) | |
| 1950 | 2036 | if templ_7745c5c3_Err != nil { | |
| 1951 | 2037 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 1, Col: 0} | |
| 1952 | 2038 | } | |
| 1953 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92) | |
| 2039 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var96) | |
| 1954 | 2040 | if templ_7745c5c3_Err != nil { | |
| 1955 | 2041 | return templ_7745c5c3_Err | |
| 1956 | 2042 | } | |
| 1957 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "\">") | |
| 2043 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">") | |
| 1958 | 2044 | if templ_7745c5c3_Err != nil { | |
| 1959 | 2045 | return templ_7745c5c3_Err | |
| 1960 | 2046 | } | |
| @@ -1969,64 +2055,64 @@ func splitSide(ln *DiffLine, old bool) templ.Component { | |||
| 1969 | 2055 | return templ_7745c5c3_Err | |
| 1970 | 2056 | } | |
| 1971 | 2057 | } | |
| 1972 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "</td>") | |
| 2058 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "</td>") | |
| 1973 | 2059 | if templ_7745c5c3_Err != nil { | |
| 1974 | 2060 | return templ_7745c5c3_Err | |
| 1975 | 2061 | } | |
| 1976 | - | var templ_7745c5c3_Var93 = []any{"m", ln.Kind, templ.KV("sel", ln.Selected)} | |
| 1977 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var93...) | |
| 2062 | + | var templ_7745c5c3_Var97 = []any{"m", ln.Kind, templ.KV("sel", ln.Selected)} | |
| 2063 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var97...) | |
| 1978 | 2064 | if templ_7745c5c3_Err != nil { | |
| 1979 | 2065 | return templ_7745c5c3_Err | |
| 1980 | 2066 | } | |
| 1981 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<td class=\"") | |
| 2067 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<td class=\"") | |
| 1982 | 2068 | if templ_7745c5c3_Err != nil { | |
| 1983 | 2069 | return templ_7745c5c3_Err | |
| 1984 | 2070 | } | |
| 1985 | - | var templ_7745c5c3_Var94 string | |
| 1986 | - | templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var93).String()) | |
| 2071 | + | var templ_7745c5c3_Var98 string | |
| 2072 | + | templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var97).String()) | |
| 1987 | 2073 | if templ_7745c5c3_Err != nil { | |
| 1988 | 2074 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 1, Col: 0} | |
| 1989 | 2075 | } | |
| 1990 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var94) | |
| 2076 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var98) | |
| 1991 | 2077 | if templ_7745c5c3_Err != nil { | |
| 1992 | 2078 | return templ_7745c5c3_Err | |
| 1993 | 2079 | } | |
| 1994 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\">") | |
| 2080 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\">") | |
| 1995 | 2081 | if templ_7745c5c3_Err != nil { | |
| 1996 | 2082 | return templ_7745c5c3_Err | |
| 1997 | 2083 | } | |
| 1998 | - | var templ_7745c5c3_Var95 string | |
| 1999 | - | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Mark) | |
| 2084 | + | var templ_7745c5c3_Var99 string | |
| 2085 | + | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Mark) | |
| 2000 | 2086 | if templ_7745c5c3_Err != nil { | |
| 2001 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 379, Col: 68} | |
| 2087 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 397, Col: 68} | |
| 2002 | 2088 | } | |
| 2003 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | |
| 2089 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 2004 | 2090 | if templ_7745c5c3_Err != nil { | |
| 2005 | 2091 | return templ_7745c5c3_Err | |
| 2006 | 2092 | } | |
| 2007 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "</td>") | |
| 2093 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</td>") | |
| 2008 | 2094 | if templ_7745c5c3_Err != nil { | |
| 2009 | 2095 | return templ_7745c5c3_Err | |
| 2010 | 2096 | } | |
| 2011 | - | var templ_7745c5c3_Var96 = []any{"tx", "chroma", ln.Kind, templ.KV("sel", ln.Selected)} | |
| 2012 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var96...) | |
| 2097 | + | var templ_7745c5c3_Var100 = []any{"tx", "chroma", ln.Kind, templ.KV("sel", ln.Selected)} | |
| 2098 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var100...) | |
| 2013 | 2099 | if templ_7745c5c3_Err != nil { | |
| 2014 | 2100 | return templ_7745c5c3_Err | |
| 2015 | 2101 | } | |
| 2016 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<td class=\"") | |
| 2102 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<td class=\"") | |
| 2017 | 2103 | if templ_7745c5c3_Err != nil { | |
| 2018 | 2104 | return templ_7745c5c3_Err | |
| 2019 | 2105 | } | |
| 2020 | - | var templ_7745c5c3_Var97 string | |
| 2021 | - | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var96).String()) | |
| 2106 | + | var templ_7745c5c3_Var101 string | |
| 2107 | + | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var100).String()) | |
| 2022 | 2108 | if templ_7745c5c3_Err != nil { | |
| 2023 | 2109 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 1, Col: 0} | |
| 2024 | 2110 | } | |
| 2025 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var97) | |
| 2111 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var101) | |
| 2026 | 2112 | if templ_7745c5c3_Err != nil { | |
| 2027 | 2113 | return templ_7745c5c3_Err | |
| 2028 | 2114 | } | |
| 2029 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">") | |
| 2115 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\">") | |
| 2030 | 2116 | if templ_7745c5c3_Err != nil { | |
| 2031 | 2117 | return templ_7745c5c3_Err | |
| 2032 | 2118 | } | |
| @@ -2034,7 +2120,7 @@ func splitSide(ln *DiffLine, old bool) templ.Component { | |||
| 2034 | 2120 | if templ_7745c5c3_Err != nil { | |
| 2035 | 2121 | return templ_7745c5c3_Err | |
| 2036 | 2122 | } | |
| 2037 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "</td>") | |
| 2123 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</td>") | |
| 2038 | 2124 | if templ_7745c5c3_Err != nil { | |
| 2039 | 2125 | return templ_7745c5c3_Err | |
| 2040 | 2126 | } | |
| @@ -2061,49 +2147,49 @@ func gutter(n, href string) templ.Component { | |||
| 2061 | 2147 | }() | |
| 2062 | 2148 | } | |
| 2063 | 2149 | ctx = templ.InitializeContext(ctx) | |
| 2064 | - | templ_7745c5c3_Var98 := templ.GetChildren(ctx) | |
| 2065 | - | if templ_7745c5c3_Var98 == nil { | |
| 2066 | - | templ_7745c5c3_Var98 = templ.NopComponent | |
| 2150 | + | templ_7745c5c3_Var102 := templ.GetChildren(ctx) | |
| 2151 | + | if templ_7745c5c3_Var102 == nil { | |
| 2152 | + | templ_7745c5c3_Var102 = templ.NopComponent | |
| 2067 | 2153 | } | |
| 2068 | 2154 | ctx = templ.ClearChildren(ctx) | |
| 2069 | 2155 | if href != "" { | |
| 2070 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<a href=\"") | |
| 2156 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<a href=\"") | |
| 2071 | 2157 | if templ_7745c5c3_Err != nil { | |
| 2072 | 2158 | return templ_7745c5c3_Err | |
| 2073 | 2159 | } | |
| 2074 | - | var templ_7745c5c3_Var99 templ.SafeURL | |
| 2075 | - | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 2160 | + | var templ_7745c5c3_Var103 templ.SafeURL | |
| 2161 | + | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 2076 | 2162 | if templ_7745c5c3_Err != nil { | |
| 2077 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 388, Col: 31} | |
| 2163 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 406, Col: 31} | |
| 2078 | 2164 | } | |
| 2079 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 2165 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) | |
| 2080 | 2166 | if templ_7745c5c3_Err != nil { | |
| 2081 | 2167 | return templ_7745c5c3_Err | |
| 2082 | 2168 | } | |
| 2083 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\" title=\"comment on this line\">") | |
| 2169 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\" title=\"comment on this line\">") | |
| 2084 | 2170 | if templ_7745c5c3_Err != nil { | |
| 2085 | 2171 | return templ_7745c5c3_Err | |
| 2086 | 2172 | } | |
| 2087 | - | var templ_7745c5c3_Var100 string | |
| 2088 | - | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(n) | |
| 2173 | + | var templ_7745c5c3_Var104 string | |
| 2174 | + | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(n) | |
| 2089 | 2175 | if templ_7745c5c3_Err != nil { | |
| 2090 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 388, Col: 66} | |
| 2176 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 406, Col: 66} | |
| 2091 | 2177 | } | |
| 2092 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 2178 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | |
| 2093 | 2179 | if templ_7745c5c3_Err != nil { | |
| 2094 | 2180 | return templ_7745c5c3_Err | |
| 2095 | 2181 | } | |
| 2096 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</a>") | |
| 2182 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "</a>") | |
| 2097 | 2183 | if templ_7745c5c3_Err != nil { | |
| 2098 | 2184 | return templ_7745c5c3_Err | |
| 2099 | 2185 | } | |
| 2100 | 2186 | } else { | |
| 2101 | - | var templ_7745c5c3_Var101 string | |
| 2102 | - | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(n) | |
| 2187 | + | var templ_7745c5c3_Var105 string | |
| 2188 | + | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(n) | |
| 2103 | 2189 | if templ_7745c5c3_Err != nil { | |
| 2104 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 390, Col: 5} | |
| 2190 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/diff.templ`, Line: 408, Col: 5} | |
| 2105 | 2191 | } | |
| 2106 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | |
| 2192 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2107 | 2193 | if templ_7745c5c3_Err != nil { | |
| 2108 | 2194 | return templ_7745c5c3_Err | |
| 2109 | 2195 | } | |
M internal/adapters/web/events.go +2 -0
| @@ -50,6 +50,8 @@ import ( | |||
| 50 | 50 | // | |
| 51 | 51 | // home-list data: <li id="repo-<name>">…</li>, or <li id="repo-<name>" data-gone></li> | |
| 52 | 52 | // for a repository that is deleted or no longer visible | |
| 53 | + | // Activity rows carry data-activity: push, MR lifecycle/comment, | |
| 54 | + | // or issue lifecycle/comment (never pending moderation or CI). | |
| 53 | 55 | // repo-last-push data: <dd id="last-push-<name>">…</dd> | |
| 54 | 56 | // refs-list data: <tbody id="ref-rows">…</tbody>, and again with | |
| 55 | 57 | // <dl id="refs-kv">…</dl>, the facts of the rail | |
M internal/adapters/web/format.go +8 -0
| @@ -61,6 +61,14 @@ func grouped(n int) string { | |||
| 61 | 61 | return string(b) | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | + | // viewLabel names a diff layout on the button of the view menu. | |
| 65 | + | func viewLabel(view string) string { | |
| 66 | + | if view == "split" { | |
| 67 | + | return "Split" | |
| 68 | + | } | |
| 69 | + | return "Unified" | |
| 70 | + | } | |
| 71 | + | ||
| 64 | 72 | func plural(n int, one, many string) string { | |
| 65 | 73 | if n == 1 { | |
| 66 | 74 | return "1 " + one | |
M internal/adapters/web/fragments.go +18 -0
| @@ -112,6 +112,24 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment, | |||
| 112 | 112 | name = ev.Repo.String() | |
| 113 | 113 | case domain.PushEvent: | |
| 114 | 114 | name, fresh = ev.Repo.String(), true | |
| 115 | + | case domain.MergeRequestOpened: | |
| 116 | + | name, fresh = ev.MergeRequest.Repo.String(), true | |
| 117 | + | case domain.MergeRequestMerged: | |
| 118 | + | name, fresh = ev.MergeRequest.Repo.String(), true | |
| 119 | + | case domain.MergeRequestClosed: | |
| 120 | + | name, fresh = ev.MergeRequest.Repo.String(), true | |
| 121 | + | case domain.MergeRequestReady: | |
| 122 | + | name, fresh = ev.MergeRequest.Repo.String(), true | |
| 123 | + | case domain.CommentAdded: | |
| 124 | + | name, fresh = ev.Repo.String(), true | |
| 125 | + | case domain.IssueOpened: | |
| 126 | + | name, fresh = ev.Issue.Repo.String(), true | |
| 127 | + | case domain.IssueClosed: | |
| 128 | + | name, fresh = ev.Issue.Repo.String(), true | |
| 129 | + | case domain.IssueReopened: | |
| 130 | + | name, fresh = ev.Issue.Repo.String(), true | |
| 131 | + | case domain.IssueCommented: | |
| 132 | + | name, fresh = ev.Repo.String(), true | |
| 115 | 133 | case domain.RunUpdated: | |
| 116 | 134 | name = ev.Run.Repo.String() // the CI state of the row | |
| 117 | 135 | case domain.JobUpdated: | |
M internal/adapters/web/home.go +24 -14
| @@ -56,12 +56,12 @@ func (s *Server) listRepos(ctx context.Context) ([]ports.RepoView, error) { | |||
| 56 | 56 | // homeSorts are the orders the home list offers, in order. The query also | |
| 57 | 57 | // accepts "desc" and "branch", which the page does not link to. | |
| 58 | 58 | var homeSorts = []struct{ key, label string }{ | |
| 59 | - | {"push", "last push"}, | |
| 59 | + | {"activity", "last activity"}, | |
| 60 | 60 | {"name", "name"}, | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | 63 | // homeKeys are the keys of ?sort= that sortRepos knows. | |
| 64 | - | var homeKeys = []string{"push", "name", "desc", "branch"} | |
| 64 | + | var homeKeys = []string{"activity", "name", "desc", "branch"} | |
| 65 | 65 | ||
| 66 | 66 | // sortRepos orders repos by the column key; ties keep the order by name. | |
| 67 | 67 | func sortRepos(repos []ports.RepoView, key string, desc bool) { | |
| @@ -71,8 +71,8 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) { | |||
| 71 | 71 | return strings.ToLower(a.Description) < strings.ToLower(b.Description) | |
| 72 | 72 | case "branch": | |
| 73 | 73 | return a.DefaultBranch < b.DefaultBranch | |
| 74 | - | case "push": | |
| 75 | - | return a.PushedAt.Before(b.PushedAt) | |
| 74 | + | case "activity": | |
| 75 | + | return repoActiveAt(a).Before(repoActiveAt(b)) | |
| 76 | 76 | } | |
| 77 | 77 | return a.Name.String() < b.Name.String() | |
| 78 | 78 | } | |
| @@ -84,17 +84,17 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) { | |||
| 84 | 84 | }) | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | - | // home lists the repositories: /?sort=<col>&dir=asc|desc, newest push first | |
| 87 | + | // home lists the repositories: /?sort=<col>&dir=asc|desc, newest activity first | |
| 88 | 88 | // by default. | |
| 89 | 89 | func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 90 | 90 | repos, err := s.listRepos(ctx) | |
| 91 | 91 | if err != nil { | |
| 92 | 92 | return nil, 0, err | |
| 93 | 93 | } | |
| 94 | - | key, desc := "push", true | |
| 94 | + | key, desc := "activity", true | |
| 95 | 95 | for _, k := range homeKeys { | |
| 96 | 96 | if r.URL.Query().Get("sort") == k { | |
| 97 | - | key, desc = k, k == "push" | |
| 97 | + | key, desc = k, k == "activity" | |
| 98 | 98 | } | |
| 99 | 99 | } | |
| 100 | 100 | if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" { | |
| @@ -103,11 +103,11 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 103 | 103 | sortRepos(repos, key, desc) | |
| 104 | 104 | ||
| 105 | 105 | now := s.cfg.Now() | |
| 106 | - | p := HomePage{} | |
| 106 | + | p := HomePage{Sort: key, Desc: desc} | |
| 107 | 107 | for _, c := range homeSorts { | |
| 108 | 108 | sl := SortLink{Label: c.label} | |
| 109 | 109 | next := "asc" | |
| 110 | - | if c.key == "push" { | |
| 110 | + | if c.key == "activity" { | |
| 111 | 111 | next = "desc" | |
| 112 | 112 | } | |
| 113 | 113 | if c.key == key { | |
| @@ -121,11 +121,10 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in | |||
| 121 | 121 | p.Sorts = append(p.Sorts, sl) | |
| 122 | 122 | } | |
| 123 | 123 | p.Rows = s.homeRows(ctx, repos, now) | |
| 124 | + | l := s.base() | |
| 124 | 125 | if n, show := s.inboxCount(ctx); show { | |
| 125 | - | p.Inbox = &InboxButton{Href: inboxPath, Count: n} | |
| 126 | + | l.Inbox = &InboxButton{Href: inboxPath, Count: n} | |
| 126 | 127 | } | |
| 127 | - | ||
| 128 | - | l := s.base() | |
| 129 | 128 | l.Title = s.instance | |
| 130 | 129 | l.Live, l.LiveURL = true, liveURL("home", "") | |
| 131 | 130 | l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...) | |
| @@ -268,8 +267,8 @@ func homeRow(r ports.RepoView, now time.Time, cursor, signed bool) HomeRow { | |||
| 268 | 267 | Desc: r.Description, Topics: r.Topics, Cursor: cursor, | |
| 269 | 268 | Private: signed && r.Visibility == domain.VisibilityPrivate, | |
| 270 | 269 | } | |
| 271 | - | if !r.PushedAt.IsZero() { | |
| 272 | - | row.Active, row.ActiveAt = ago(now, r.PushedAt), rfc3339(r.PushedAt) | |
| 270 | + | if at := repoActiveAt(r); !at.IsZero() { | |
| 271 | + | row.Active, row.ActiveAt = ago(now, at), rfc3339(at) | |
| 273 | 272 | } | |
| 274 | 273 | return row | |
| 275 | 274 | } | |
| @@ -285,3 +284,14 @@ func repoCmds(repos []ports.RepoView, except string) []CmdItem { | |||
| 285 | 284 | } | |
| 286 | 285 | return out | |
| 287 | 286 | } | |
| 287 | + | ||
| 288 | + | // repoActiveAt falls back for repositories without recorded activity. | |
| 289 | + | func repoActiveAt(r ports.RepoView) time.Time { | |
| 290 | + | if !r.ActiveAt.IsZero() { | |
| 291 | + | return r.ActiveAt | |
| 292 | + | } | |
| 293 | + | if r.PushedAt.After(r.CreatedAt) { | |
| 294 | + | return r.PushedAt | |
| 295 | + | } | |
| 296 | + | return r.CreatedAt | |
| 297 | + | } | |
M internal/adapters/web/home_test.go +50 -1
| @@ -252,7 +252,7 @@ func TestRunUpdatedRefreshesTheHomeRow(t *testing.T) { | |||
| 252 | 252 | assert.Equal(t, "home-list", got[0].event) | |
| 253 | 253 | assert.Contains(t, got[0].html, `id="repo-proj"`) | |
| 254 | 254 | assert.Contains(t, got[0].html, "passing") | |
| 255 | - | assert.NotContains(t, got[0].html, "new push", "a run is not a push") | |
| 255 | + | assert.NotContains(t, got[0].html, "new activity", "a run is not a push") | |
| 256 | 256 | ||
| 257 | 257 | // A job that ended refreshes the row too; one in progress does not. | |
| 258 | 258 | got, err = (&liveStream{s: srv}).render(t.Context(), ports.WatchEvent{Event: domain.JobUpdated{Job: domain.CIJob{Repo: repo, Phase: domain.CIPhaseRunning}}}) | |
| @@ -344,3 +344,52 @@ func TestHomeOffersTheLayouts(t *testing.T) { | |||
| 344 | 344 | require.NotEqual(t, -1, store) | |
| 345 | 345 | assert.Less(t, store, app, "the store loads before the app script") | |
| 346 | 346 | } | |
| 347 | + | ||
| 348 | + | func TestHomeActivityOrderAndFallback(t *testing.T) { | |
| 349 | + | t.Parallel() | |
| 350 | + | now := time.Now().UTC() | |
| 351 | + | a, b, c := publicRepo(t, "a"), publicRepo(t, "b"), publicRepo(t, "c") | |
| 352 | + | a.PushedAt = now.Add(-time.Hour) | |
| 353 | + | a.ActiveAt = now | |
| 354 | + | b.PushedAt = now.Add(-time.Minute) | |
| 355 | + | c.CreatedAt = now.Add(-30 * time.Minute) | |
| 356 | + | repos := []ports.RepoView{b, c, a} | |
| 357 | + | sortRepos(repos, "activity", true) | |
| 358 | + | assert.Equal(t, []ports.RepoView{a, b, c}, repos) | |
| 359 | + | sortRepos(repos, "activity", false) | |
| 360 | + | assert.Equal(t, []ports.RepoView{c, b, a}, repos) | |
| 361 | + | assert.Equal(t, rfc3339(now), homeRow(a, now, false, false).ActiveAt) | |
| 362 | + | c.PushedAt = c.CreatedAt.Add(-time.Hour) | |
| 363 | + | assert.Equal(t, c.CreatedAt, repoActiveAt(c), "fallback is the later of creation and push") | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | func TestActivityEventsRefreshTheHomeRow(t *testing.T) { | |
| 367 | + | t.Parallel() | |
| 368 | + | repo := publicRepo(t, "proj") | |
| 369 | + | srv := newTestServer(t, Config{Repos: reposServing(t, repo), Browse: homeBrowse(t)}) | |
| 370 | + | mr := domain.MergeRequest{Repo: repo.Name} | |
| 371 | + | issue := domain.Issue{Repo: repo.Name} | |
| 372 | + | for _, ev := range []domain.Event{ | |
| 373 | + | domain.PushEvent{Repo: repo.Name}, | |
| 374 | + | domain.MergeRequestOpened{MergeRequest: mr}, domain.MergeRequestMerged{MergeRequest: mr}, | |
| 375 | + | domain.MergeRequestClosed{MergeRequest: mr}, domain.MergeRequestReady{MergeRequest: mr}, | |
| 376 | + | domain.CommentAdded{Repo: repo.Name}, domain.IssueOpened{Issue: issue}, | |
| 377 | + | domain.IssueClosed{Issue: issue}, domain.IssueReopened{Issue: issue}, domain.IssueCommented{Repo: repo.Name}, | |
| 378 | + | } { | |
| 379 | + | t.Run(ev.EventKind(), func(t *testing.T) { | |
| 380 | + | t.Parallel() | |
| 381 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev}) | |
| 382 | + | require.NoError(t, err) | |
| 383 | + | require.Len(t, got, 1) | |
| 384 | + | assert.Equal(t, "home-list", got[0].event) | |
| 385 | + | assert.Contains(t, got[0].html, `id="repo-proj"`) | |
| 386 | + | assert.Contains(t, got[0].html, `data-activity`) | |
| 387 | + | assert.Contains(t, got[0].html, "new activity") | |
| 388 | + | }) | |
| 389 | + | } | |
| 390 | + | for _, ev := range []domain.Event{domain.IssueSubmitted{Repo: repo.Name}, domain.IssuePendingRemoved{Repo: repo.Name}} { | |
| 391 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev}) | |
| 392 | + | require.NoError(t, err) | |
| 393 | + | assert.Empty(t, got) | |
| 394 | + | } | |
| 395 | + | } | |
M internal/adapters/web/issues.templ +24 -22
| @@ -136,32 +136,34 @@ templ issueNewRail() { | |||
| 136 | 136 | // owner, the form to comment and the button that closes or reopens it. | |
| 137 | 137 | templ IssueShow(l Layout, p IssuePage) { | |
| 138 | 138 | @Shell(l, issueRail(p)) { | |
| 139 | - | <header class="show mr-h"> | |
| 140 | - | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | |
| 141 | - | <dl class="kv show-kv"> | |
| 142 | - | <dt>state</dt> | |
| 143 | - | <dd class="mr-state" data-state={ p.State }>{ p.State }</dd> | |
| 144 | - | <dt>opened</dt> | |
| 145 | - | <dd> | |
| 146 | - | { p.Opened.Name } | |
| 147 | - | if p.Opened.WhenAt != "" { | |
| 148 | - | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | |
| 139 | + | <section class="mr-card"> | |
| 140 | + | <header class="show mr-h"> | |
| 141 | + | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | |
| 142 | + | <dl class="kv show-kv"> | |
| 143 | + | <dt>state</dt> | |
| 144 | + | <dd class="mr-state" data-state={ p.State }>{ p.State }</dd> | |
| 145 | + | <dt>opened</dt> | |
| 146 | + | <dd> | |
| 147 | + | { p.Opened.Name } | |
| 148 | + | if p.Opened.WhenAt != "" { | |
| 149 | + | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | |
| 150 | + | } | |
| 151 | + | </dd> | |
| 152 | + | if p.UpdatedAt != "" { | |
| 153 | + | <dt>updated</dt> | |
| 154 | + | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | |
| 149 | 155 | } | |
| 150 | - | </dd> | |
| 151 | - | if p.UpdatedAt != "" { | |
| 152 | - | <dt>updated</dt> | |
| 153 | - | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | |
| 154 | - | } | |
| 155 | - | </dl> | |
| 156 | - | </header> | |
| 156 | + | </dl> | |
| 157 | + | </header> | |
| 158 | + | if p.Desc != "" { | |
| 159 | + | <article class="mr-desc prose" aria-label="description" hx-ignore> | |
| 160 | + | @templ.Raw(string(p.Desc)) | |
| 161 | + | </article> | |
| 162 | + | } | |
| 163 | + | </section> | |
| 157 | 164 | if p.Live { | |
| 158 | 165 | @LiveSlot("mr-notice") | |
| 159 | 166 | } | |
| 160 | - | if p.Desc != "" { | |
| 161 | - | <article class="mr-desc prose" aria-label="description" hx-ignore> | |
| 162 | - | @templ.Raw(string(p.Desc)) | |
| 163 | - | </article> | |
| 164 | - | } | |
| 165 | 167 | <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2> | |
| 166 | 168 | for _, c := range p.Comments { | |
| 167 | 169 | @commentView(c, "") | |
M internal/adapters/web/issues_templ.go +26 -26 generated file: collapsed
M internal/adapters/web/jstest/a11y.test.mjs +25 -0
| @@ -8,6 +8,7 @@ import { test } from 'node:test'; | |||
| 8 | 8 | import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs'; | |
| 9 | 9 | ||
| 10 | 10 | // The few things ermite.js uses that dom.mjs does not have. | |
| 11 | + | El.prototype.prepend = function (...nodes) { this.nodes.unshift(...this.adopt(nodes)); }; | |
| 11 | 12 | El.prototype.replaceWith = function (n) { | |
| 12 | 13 | const p = this.parent; | |
| 13 | 14 | const i = p.nodes.indexOf(this); | |
| @@ -333,3 +334,27 @@ test('the preference is read from storage when the page loads', async () => { | |||
| 333 | 334 | assert.deepEqual(dom.body.events, []); | |
| 334 | 335 | store.set('keys-off', ''); | |
| 335 | 336 | }); | |
| 337 | + | ||
| 338 | + | ||
| 339 | + | test('activity moves a home row up only in newest activity order, preserving focus and cursor', () => { | |
| 340 | + | for (const [sort, desc, activity, moves] of [ | |
| 341 | + | ['activity', 'true', true, true], | |
| 342 | + | ['activity', 'false', true, false], | |
| 343 | + | ['name', 'true', true, false], | |
| 344 | + | ['name', 'false', true, false], | |
| 345 | + | ['activity', 'true', false, false], // CI or metadata, not activity | |
| 346 | + | ]) { | |
| 347 | + | const r = rows(); | |
| 348 | + | r.list.dataset.sort = sort; | |
| 349 | + | r.list.dataset.desc = desc; | |
| 350 | + | const old = r.list.children[1]; | |
| 351 | + | old.setAttribute('data-cursor', ''); | |
| 352 | + | active = old.children[0]; | |
| 353 | + | const fresh = r.mk('repo-b', '/b'); | |
| 354 | + | if (activity) fresh.setAttribute('data-activity', ''); | |
| 355 | + | swapWith(`activity-${sort}-${desc}-${activity}`, fresh); | |
| 356 | + | assert.equal(r.list.children.indexOf(fresh), moves ? 0 : 1); | |
| 357 | + | assert.equal(active, fresh.children[0]); | |
| 358 | + | assert.ok(fresh.hasAttribute('data-cursor')); | |
| 359 | + | } | |
| 360 | + | }); | |
M internal/adapters/web/keys.templ +1 -0
| @@ -32,6 +32,7 @@ templ Keys() { | |||
| 32 | 32 | k-split from:body -> ermite.layout('split'); | |
| 33 | 33 | k-unified from:body -> ermite.layout('unified'); | |
| 34 | 34 | k-collapse from:body -> ermite.collapse(); | |
| 35 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 35 | 36 | k-expand from:body -> ermite.expandFile(); | |
| 36 | 37 | k-expandAll from:body -> ermite.expandAll(); | |
| 37 | 38 | k-permalink from:body -> ermite.permalink(); | |
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.go +6 -1
| @@ -910,10 +910,15 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||
| 910 | 910 | ln.Selected = true | |
| 911 | 911 | } | |
| 912 | 912 | } | |
| 913 | + | // The title names the whole range a shift + click selected: a.go:21-149. | |
| 914 | + | lines := itoa(on.line) | |
| 915 | + | if start < on.line { | |
| 916 | + | lines = itoa(start) + "-" + lines | |
| 917 | + | } | |
| 913 | 918 | if ln := find(on.path, side, on.line); ln != nil { | |
| 914 | 919 | ln.Form = &CommentForm{ | |
| 915 | 920 | StartLine: optionalLine(on.start), ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side, | |
| 916 | - | Title: "comment on " + visibleText(on.path) + ":" + itoa(on.line) + " (" + on.side + ")", | |
| 921 | + | Title: "comment on " + visibleText(on.path) + ":" + lines + " (" + on.side + ")", | |
| 917 | 922 | Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true, | |
| 918 | 923 | From: windowStart(fileAt[on.path]), | |
| 919 | 924 | } | |
M internal/adapters/web/mr.templ +37 -39
| @@ -109,41 +109,43 @@ templ mrListRail(p MRListPage) { | |||
| 109 | 109 | // form, the checks, the settings, the commits and the files. | |
| 110 | 110 | templ MergeRequest(l Layout, p MRPage) { | |
| 111 | 111 | @Shell(l, mrRail(p)) { | |
| 112 | - | <header class="show mr-h"> | |
| 113 | - | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | |
| 114 | - | <dl class="kv show-kv"> | |
| 115 | - | <dt>state</dt> | |
| 116 | - | <dd class="mr-state" data-state={ p.State }> | |
| 117 | - | { p.State } | |
| 118 | - | if p.MergedShort != "" { | |
| 119 | - | { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode } | |
| 120 | - | } | |
| 121 | - | </dd> | |
| 122 | - | <dt>branch</dt> | |
| 123 | - | <dd> | |
| 124 | - | @branchLink(p.Source, p.SourceHref) | |
| 125 | - | { " → " } | |
| 126 | - | @branchLink(p.Target, p.TargetHref) | |
| 127 | - | </dd> | |
| 128 | - | <dt>opened</dt> | |
| 129 | - | <dd> | |
| 130 | - | { p.Opened.Name } | |
| 131 | - | if p.Opened.WhenAt != "" { | |
| 132 | - | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | |
| 112 | + | <section class="mr-card"> | |
| 113 | + | <header class="show mr-h"> | |
| 114 | + | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | |
| 115 | + | <dl class="kv show-kv"> | |
| 116 | + | <dt>state</dt> | |
| 117 | + | <dd class="mr-state" data-state={ p.State }> | |
| 118 | + | { p.State } | |
| 119 | + | if p.MergedShort != "" { | |
| 120 | + | { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode } | |
| 121 | + | } | |
| 122 | + | </dd> | |
| 123 | + | <dt>branch</dt> | |
| 124 | + | <dd> | |
| 125 | + | @branchLink(p.Source, p.SourceHref) | |
| 126 | + | { " → " } | |
| 127 | + | @branchLink(p.Target, p.TargetHref) | |
| 128 | + | </dd> | |
| 129 | + | <dt>opened</dt> | |
| 130 | + | <dd> | |
| 131 | + | { p.Opened.Name } | |
| 132 | + | if p.Opened.WhenAt != "" { | |
| 133 | + | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | |
| 134 | + | } | |
| 135 | + | </dd> | |
| 136 | + | if p.UpdatedAt != "" { | |
| 137 | + | <dt>updated</dt> | |
| 138 | + | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | |
| 133 | 139 | } | |
| 134 | - | </dd> | |
| 135 | - | if p.UpdatedAt != "" { | |
| 136 | - | <dt>updated</dt> | |
| 137 | - | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | |
| 138 | - | } | |
| 139 | - | </dl> | |
| 140 | - | </header> | |
| 140 | + | </dl> | |
| 141 | + | </header> | |
| 142 | + | if p.Desc != "" { | |
| 143 | + | <article class="mr-desc prose" aria-label="description" hx-ignore> | |
| 144 | + | @templ.Raw(string(p.Desc)) | |
| 145 | + | </article> | |
| 146 | + | } | |
| 147 | + | </section> | |
| 141 | 148 | @LiveSlot("mr-notice") | |
| 142 | - | if p.Desc != "" { | |
| 143 | - | <article class="mr-desc prose" aria-label="description" hx-ignore> | |
| 144 | - | @templ.Raw(string(p.Desc)) | |
| 145 | - | </article> | |
| 146 | - | } | |
| 147 | 149 | if p.Since != nil { | |
| 148 | 150 | <nav class="since" aria-label="Diff range"> | |
| 149 | 151 | <a | |
| @@ -431,9 +433,7 @@ templ mrRail(p MRPage) { | |||
| 431 | 433 | templ mrBar(p MRPage) { | |
| 432 | 434 | <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge"> | |
| 433 | 435 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 434 | - | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 435 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |
| 436 | - | @collapseButton() | |
| 436 | + | @diffViewMenu(p.DiffView) | |
| 437 | 437 | <span class="grow"></span> | |
| 438 | 438 | @MRChecksSum(p.ChecksSum) | |
| 439 | 439 | if p.Box != nil { | |
| @@ -562,9 +562,7 @@ templ Compare(l Layout, p ComparePage) { | |||
| 562 | 562 | <div class="diff-tools" role="group" aria-label="Diff view"> | |
| 563 | 563 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 564 | 564 | <span class="grow"></span> | |
| 565 | - | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 566 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |
| 567 | - | @collapseButton() | |
| 565 | + | @diffViewMenu(p.DiffView) | |
| 568 | 566 | </div> | |
| 569 | 567 | if p.Invisible { | |
| 570 | 568 | <p class="notice"> | |
M internal/adapters/web/mr_templ.go +107 -123 generated file: collapsed
M internal/adapters/web/overlays.templ +1 -0
| @@ -28,6 +28,7 @@ templ OverlayHelp(mr bool) { | |||
| 28 | 28 | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> | |
| 29 | 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 & tags, merge requests, issues</dd></div> | |
| 30 | 30 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | |
| 31 | + | <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div> | |
| 31 | 32 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | |
| 32 | 33 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | |
| 33 | 34 | <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> | |
M internal/adapters/web/overlays_templ.go +12 -12 generated file: collapsed
M internal/adapters/web/pages.templ +3 -9
| @@ -2,6 +2,8 @@ | |||
| 2 | 2 | ||
| 3 | 3 | package web | |
| 4 | 4 | ||
| 5 | + | import "fmt" | |
| 6 | + | ||
| 5 | 7 | // Home is the repository list. | |
| 6 | 8 | templ Home(l Layout, p HomePage) { | |
| 7 | 9 | @Shell(l, nil) { | |
| @@ -9,14 +11,6 @@ templ Home(l Layout, p HomePage) { | |||
| 9 | 11 | <div class="home" data-layout="cards"> | |
| 10 | 12 | <section class="home-repos" aria-label="Repositories"> | |
| 11 | 13 | <div class="home-bar"> | |
| 12 | - | if p.Inbox != nil { | |
| 13 | - | <a class="btn inbox-btn" href={ templ.SafeURL(p.Inbox.Href) } title="inbox"> | |
| 14 | - | Inbox | |
| 15 | - | if p.Inbox.Count >= 0 { | |
| 16 | - | <span class="count">{ itoa(p.Inbox.Count) }</span> | |
| 17 | - | } | |
| 18 | - | </a> | |
| 19 | - | } | |
| 20 | 14 | <div class="filter"> | |
| 21 | 15 | <label for="home-filter" class="sr">filter</label> | |
| 22 | 16 | <input | |
| @@ -57,7 +51,7 @@ templ Home(l Layout, p HomePage) { | |||
| 57 | 51 | </button> | |
| 58 | 52 | </div> | |
| 59 | 53 | </div> | |
| 60 | - | <ul class="repos" id="repo-rows" data-rows> | |
| 54 | + | <ul class="repos" id="repo-rows" data-rows data-sort={ p.Sort } data-desc={ fmt.Sprint(p.Desc) }> | |
| 61 | 55 | for _, r := range p.Rows { | |
| 62 | 56 | @HomeRowView(r) | |
| 63 | 57 | } | |
M internal/adapters/web/pages_templ.go +205 -223 generated file: collapsed
M internal/adapters/web/shell.templ +14 -1
| @@ -25,7 +25,7 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 25 | 25 | <script src={ l.Assets.HTMX } defer></script> | |
| 26 | 26 | <script src={ l.Assets.Live } defer></script> | |
| 27 | 27 | <script src={ l.Assets.SSE } defer></script> | |
| 28 | - | <script src={ l.Assets.Store } defer></script> | |
| 28 | + | <script src={ l.Assets.Store }></script> | |
| 29 | 29 | <script src={ l.Assets.App } defer></script> | |
| 30 | 30 | <script src={ l.Assets.Files } defer></script> | |
| 31 | 31 | <script src={ l.Assets.Passkey } defer></script> | |
| @@ -80,6 +80,19 @@ templ Header(l Layout) { | |||
| 80 | 80 | <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)"> | |
| 81 | 81 | <span class="brand-mark" aria-hidden="true"></span> | |
| 82 | 82 | </a> | |
| 83 | + | if l.Inbox != nil { | |
| 84 | + | <a | |
| 85 | + | class="btn inbox-btn" | |
| 86 | + | href={ templ.SafeURL(l.Inbox.Href) } | |
| 87 | + | title="inbox" | |
| 88 | + | > | |
| 89 | + | <span class="ico i-mailbox" aria-hidden="true"></span> | |
| 90 | + | Inbox | |
| 91 | + | if l.Inbox.Count >= 0 { | |
| 92 | + | <span class="count">{ itoa(l.Inbox.Count) }</span> | |
| 93 | + | } | |
| 94 | + | </a> | |
| 95 | + | } | |
| 83 | 96 | if len(l.Crumbs) > 0 { | |
| 84 | 97 | <nav class="crumbs" aria-label="Path"> | |
| 85 | 98 | <ol> | |
M internal/adapters/web/shell_templ.go +158 -116 generated file: collapsed
M internal/adapters/web/static/css/20-shell.css +9 -0
| @@ -29,6 +29,7 @@ | |||
| 29 | 29 | .i-cards { --i: url(icons/cards.svg); } | |
| 30 | 30 | .i-split { --i: url(icons/split.svg); } | |
| 31 | 31 | .i-compact { --i: url(icons/compact.svg); } | |
| 32 | + | .i-mailbox { --i: url(icons/mailbox.svg); } | |
| 32 | 33 | ||
| 33 | 34 | .pill { | |
| 34 | 35 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | |
| @@ -77,6 +78,11 @@ | |||
| 77 | 78 | } | |
| 78 | 79 | /* the hermit by his fire: a grey picture in its own rounded square */ | |
| 79 | 80 | .brand-mark { width: 32px; height: 32px; flex: none; background: url(icons/brand.svg) center / contain no-repeat; } | |
| 81 | + | /* the inbox, right of the logo on the home page: its number in a darker badge */ | |
| 82 | + | .inbox-btn { | |
| 83 | + | flex: none; | |
| 84 | + | & .count { margin-left: 0; color: var(--ink-hi); background: var(--bg-canvas); box-shadow: inset 0 0 0 1px var(--border); } | |
| 85 | + | } | |
| 80 | 86 | .crumbs { | |
| 81 | 87 | min-width: 0; flex: 1 1 auto; overflow: hidden; | |
| 82 | 88 | & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); } | |
| @@ -133,6 +139,9 @@ | |||
| 133 | 139 | align-content: start; | |
| 134 | 140 | } | |
| 135 | 141 | .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); } | |
| 142 | + | /* the wide mode (w, ermiteWide): every page takes the whole width of the | |
| 143 | + | screen; the rail goes to the right edge, the content takes the rest */ | |
| 144 | + | html.wide :is(.work, .hdr-top, .tabs, .foot) { max-width: none; } | |
| 136 | 145 | .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; } | |
| 137 | 146 | .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); } | |
| 138 | 147 | main > * + * { margin-top: var(--s4); } | |
M internal/adapters/web/static/css/50-code.css +7 -4
| @@ -79,13 +79,16 @@ | |||
| 79 | 79 | & .ln { width: calc(var(--lnw) + 2ch); min-width: calc(var(--lnw) + 2ch); padding: 0 1ch; text-align: right; color: var(--ink-mute); user-select: none; } | |
| 80 | 80 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | |
| 81 | 81 | & .tx { white-space: pre; } | |
| 82 | - | & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; } | |
| 82 | + | /* the hunk headers and the hidden lines stay quiet: a faint band between | |
| 83 | + | the code, so that the eye keeps reading */ | |
| 84 | + | & .hunk td, & .gap td { background: color-mix(in srgb, var(--paper-alt) 35%, var(--paper)); } | |
| 85 | + | & .hunk td { color: var(--ink-mute); padding: 0 1ch; } | |
| 83 | 86 | /* a run of hidden lines: the whole row loads them */ | |
| 84 | - | & .gap td { padding: 0; background: var(--paper-alt); } | |
| 87 | + | & .gap td { padding: 0; } | |
| 85 | 88 | & .gap button { | |
| 86 | 89 | display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer; | |
| 87 | - | font: inherit; color: var(--ink-dim); background: none; border: 0; | |
| 88 | - | border-block: 1px dashed var(--border); | |
| 90 | + | font: inherit; color: var(--ink-mute); background: none; border: 0; | |
| 91 | + | border-block: 1px dashed color-mix(in srgb, var(--border) 60%, transparent); | |
| 89 | 92 | } | |
| 90 | 93 | & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); } | |
| 91 | 94 | & .gap button:disabled { cursor: progress; opacity: .6; } | |
M internal/adapters/web/static/css/55-diff.css +25 -0
| @@ -9,4 +9,29 @@ | |||
| 9 | 9 | ||
| 10 | 10 | @layer components { | |
| 11 | 11 | .sel { outline: 1px solid var(--ink-dim); outline-offset: -1px; } | |
| 12 | + | ||
| 13 | + | /* a range of selected diff lines (shift + click) is one block: one border | |
| 14 | + | around the run, none between its lines. Each edge is an inset shadow of | |
| 15 | + | the cells; the top one only on the first line of the run, the bottom one | |
| 16 | + | only on the last. In split, each side is a block of its own. */ | |
| 17 | + | .dt { | |
| 18 | + | --sel-edge: var(--ink-dim); | |
| 19 | + | & .sel { outline: none; } | |
| 20 | + | & tr.sel > td, & td.sel { | |
| 21 | + | box-shadow: var(--sel-l, 0 0 transparent), var(--sel-r, 0 0 transparent), var(--sel-t, 0 0 transparent), var(--sel-b, 0 0 transparent); | |
| 22 | + | --sel-t: inset 0 1px var(--sel-edge); --sel-b: inset 0 -1px var(--sel-edge); | |
| 23 | + | } | |
| 24 | + | /* unified: the row is selected */ | |
| 25 | + | & tr.sel > td:first-child { --sel-l: inset 1px 0 var(--sel-edge); } | |
| 26 | + | & tr.sel > td:last-child { --sel-r: inset -1px 0 var(--sel-edge); } | |
| 27 | + | & tr.sel + tr.sel > td { --sel-t: 0 0 transparent; } | |
| 28 | + | & tr.sel:has(+ tr.sel) > td { --sel-b: 0 0 transparent; } | |
| 29 | + | /* split: the cells of one side (number, mark, text) are selected */ | |
| 30 | + | & td.sel.ln { --sel-l: inset 1px 0 var(--sel-edge); } | |
| 31 | + | & td.sel.tx { --sel-r: inset -1px 0 var(--sel-edge); } | |
| 32 | + | & tr:has(> :nth-child(3).sel) + tr > :nth-child(-n + 3).sel, | |
| 33 | + | & tr:has(> :nth-child(6).sel) + tr > :nth-child(n + 4).sel { --sel-t: 0 0 transparent; } | |
| 34 | + | & tr:has(> :nth-child(3).sel):has(+ tr > :nth-child(3).sel) > :nth-child(-n + 3).sel, | |
| 35 | + | & tr:has(> :nth-child(6).sel):has(+ tr > :nth-child(6).sel) > :nth-child(n + 4).sel { --sel-b: 0 0 transparent; } | |
| 36 | + | } | |
| 12 | 37 | } | |
M internal/adapters/web/static/css/60-mr.css +15 -1
| @@ -1,6 +1,12 @@ | |||
| 1 | 1 | @layer components { | |
| 2 | 2 | /* merge requests: the review page. Hue only where it carries a verdict. */ | |
| 3 | 3 | .mr-h .show-kv { margin-top: 0; } | |
| 4 | + | /* one card: the header (title, facts) a shade lighter, the description below */ | |
| 5 | + | .mr-card { | |
| 6 | + | background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; | |
| 7 | + | & > .mr-h { background: var(--bg-raised); border: 0; border-radius: 0; padding-bottom: var(--s3); } | |
| 8 | + | & > .mr-desc.prose { border: 0; border-top: 1px solid var(--border); border-radius: 0; } | |
| 9 | + | } | |
| 4 | 10 | ||
| 5 | 11 | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | |
| 6 | 12 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | |
| @@ -49,6 +55,15 @@ | |||
| 49 | 55 | } | |
| 50 | 56 | & .kv { margin: 0; } | |
| 51 | 57 | } | |
| 58 | + | /* the view menu of a diff: the layout and the fold, one row each */ | |
| 59 | + | .vmenu { | |
| 60 | + | & > summary::after { content: ""; width: 0; height: 0; margin-left: 2px; border: 4px solid transparent; border-top-color: currentColor; transform: translateY(2px); } | |
| 61 | + | & .menu-pop { left: 0; right: auto; width: 220px; gap: var(--s1); padding: var(--s2); } | |
| 62 | + | & .menu-pop .btn { width: 100%; justify-content: flex-start; border-color: transparent; background: none; } | |
| 63 | + | & .menu-pop .btn:hover { background: var(--bg-raised); } | |
| 64 | + | & .menu-pop .btn[aria-current="true"] { background: var(--bg-raised); color: var(--ink-hi); } | |
| 65 | + | & .menu-pop .btn kbd { margin-left: auto; } | |
| 66 | + | } | |
| 52 | 67 | .mmenu { | |
| 53 | 68 | & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; } | |
| 54 | 69 | & .d-modes label, & .d-check { | |
| @@ -207,7 +222,6 @@ | |||
| 207 | 222 | & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); } | |
| 208 | 223 | & .since { align-self: flex-start; max-width: 100%; } | |
| 209 | 224 | } | |
| 210 | - | .inbox-btn { gap: var(--s2); } | |
| 211 | 225 | ||
| 212 | 226 | @media (max-width: 700px) { | |
| 213 | 227 | .list.mrs .c-who { display: none; } | |
A internal/adapters/web/static/icons/mailbox.svg +1 -0
| @@ -0,0 +1,1 @@ | |||
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4v8Z"/><polyline points="15,9 18,9 18,11"/><path d="M6.5 5C9 5 11 7 11 9.5V17a2 2 0 0 1-2 2"/><line x1="6" x2="7" y1="10" y2="10"/></svg> | |
M internal/adapters/web/static/js/ermite.js +70 -3
| @@ -14,6 +14,7 @@ | |||
| 14 | 14 | // 7. The service worker (ermiteInstall). | |
| 15 | 15 | // 8. Chip lists kept on one line, with a … chip (ermiteFit). | |
| 16 | 16 | // 9. The layout of the home page, remembered in the browser (ermiteLayout). | |
| 17 | + | // 10. The wide mode: every page takes the whole width (ermiteWide). | |
| 17 | 18 | // | |
| 18 | 19 | // Preferences are kept by ermiteStore (store.js, loaded first). | |
| 19 | 20 | (() => { | |
| @@ -400,6 +401,25 @@ | |||
| 400 | 401 | // file whose lines are all loaded has no gap left. | |
| 401 | 402 | window.ermiteGaps = (() => { | |
| 402 | 403 | const pending = new Map(); | |
| 404 | + | // selectLoaded marks the loaded rows that fall in the range of lines the | |
| 405 | + | // URL selects (?on=new:21-149:a.go, a shift + click), as the server marks | |
| 406 | + | // the rows it sent: the range stays one block once its hidden lines show. | |
| 407 | + | // The loaded rows are context lines, the same on both sides. | |
| 408 | + | function selectLoaded(file, rows) { | |
| 409 | + | const m = /^(new|old):(?:(\d+)-)?(\d+):(.+)$/.exec(new URLSearchParams(location.search).get('on') || ''); | |
| 410 | + | // data-file is the path comments name (the new path of a rename), as ?on= | |
| 411 | + | if (!m || file.dataset.file !== m[4]) return; | |
| 412 | + | const end = Number(m[3]); | |
| 413 | + | const start = m[2] ? Number(m[2]) : end; | |
| 414 | + | for (const r of rows) { | |
| 415 | + | const cells = [...r.querySelectorAll('td')]; | |
| 416 | + | // unified: old, new, mark, text; split: old side, then new side | |
| 417 | + | const num = Number((cells.length === 4 ? cells[m[1] === 'old' ? 0 : 1] : cells[m[1] === 'old' ? 0 : 3])?.textContent.trim()); | |
| 418 | + | if (!(num >= start && num <= end)) continue; | |
| 419 | + | if (cells.length === 4) r.classList.add('sel'); | |
| 420 | + | else for (const c of cells) c.classList.add('sel'); | |
| 421 | + | } | |
| 422 | + | } | |
| 403 | 423 | // load fills the gap the button is in, in both tables of its file. | |
| 404 | 424 | function load(btn) { | |
| 405 | 425 | const src = btn.dataset.src; | |
| @@ -424,6 +444,7 @@ | |||
| 424 | 444 | if (!from || !at) continue; | |
| 425 | 445 | const rows = [...from.children].map((r) => r.cloneNode(true)); | |
| 426 | 446 | n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length); | |
| 447 | + | selectLoaded(file, rows.filter((r) => !r.classList.contains('gap'))); | |
| 427 | 448 | at.replaceWith(...rows); | |
| 428 | 449 | } | |
| 429 | 450 | // The focus was on the button, which is gone: on the gap that is | |
| @@ -553,9 +574,44 @@ | |||
| 553 | 574 | return { get: pref.get, set, apply, values }; | |
| 554 | 575 | })(); | |
| 555 | 576 | ||
| 577 | + | // The wide mode (w, on every page): the work area takes the whole width of | |
| 578 | + | // the screen, the rail at the right edge and the content in the rest. | |
| 579 | + | // The choice is kept by ermiteStore ("wide": '1' when wide) and shown | |
| 580 | + | // as html.wide, which the styles follow. Another tab's change applies here | |
| 581 | + | // at once. | |
| 582 | + | window.ermiteWide = (() => { | |
| 583 | + | const pref = window.ermiteStore.pref('wide', { values: ['1', ''], fallback: '' }); | |
| 584 | + | function apply() { | |
| 585 | + | const wide = pref.get() === '1'; | |
| 586 | + | document.documentElement.classList.toggle?.('wide', wide); | |
| 587 | + | } | |
| 588 | + | function toggle() { | |
| 589 | + | pref.set(pref.get() === '1' ? '' : '1'); | |
| 590 | + | apply(); | |
| 591 | + | } | |
| 592 | + | pref.watch(apply); | |
| 593 | + | apply(); | |
| 594 | + | return { toggle, apply }; | |
| 595 | + | })(); | |
| 596 | + | ||
| 597 | + | // The menus of the bars (<details class="menu">: the diff view, the merge | |
| 598 | + | // menu): closing one hands the focus back to its button when it was inside, | |
| 599 | + | // so that a keyboard user keeps their place. | |
| 600 | + | function closeMenu(d) { | |
| 601 | + | const had = d.contains(document.activeElement); | |
| 602 | + | d.open = false; | |
| 603 | + | if (had) $('summary', d)?.focus(); | |
| 604 | + | } | |
| 605 | + | // closeMenus closes every open menu (escape); true when one was open. | |
| 606 | + | function closeMenus() { | |
| 607 | + | const open = $$('details.menu[open]'); | |
| 608 | + | for (const d of open) closeMenu(d); | |
| 609 | + | return open.length > 0; | |
| 610 | + | } | |
| 611 | + | ||
| 556 | 612 | // 1. Keys. | |
| 557 | 613 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | |
| 558 | - | s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc', | |
| 614 | + | s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc', | |
| 559 | 615 | // the merge request page (mr.js): the merge menu, mark read | |
| 560 | 616 | M: 'merge', R: 'read' }; | |
| 561 | 617 | // "g t" is the key "t": the tabs have one event each. | |
| @@ -571,7 +627,7 @@ | |||
| 571 | 627 | const mr = window.ermiteMR; | |
| 572 | 628 | if (e.key === 'Escape') { | |
| 573 | 629 | pending = 0; | |
| 574 | - | if (mr?.closeMenus() || mr?.leaveFiles()) return; | |
| 630 | + | if (closeMenus() || mr?.leaveFiles()) return; | |
| 575 | 631 | if (typing) e.target.blur(); | |
| 576 | 632 | return ermite.fire('esc'); | |
| 577 | 633 | } | |
| @@ -611,6 +667,13 @@ | |||
| 611 | 667 | if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; | |
| 612 | 668 | e.preventDefault(); | |
| 613 | 669 | ermite.fire(el.dataset.fire); | |
| 670 | + | // A choice in a menu closes it; the focus goes back to its button. | |
| 671 | + | const menu = el.closest('details.menu'); | |
| 672 | + | if (menu) closeMenu(menu); | |
| 673 | + | }); | |
| 674 | + | // A click outside an open menu closes it. | |
| 675 | + | addEventListener('click', (e) => { | |
| 676 | + | for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false; | |
| 614 | 677 | }); | |
| 615 | 678 | for (const a of $$('a[data-fire="refs"]')) { | |
| 616 | 679 | a.setAttribute('aria-haspopup', 'dialog'); | |
| @@ -773,7 +836,8 @@ | |||
| 773 | 836 | // internal/adapters/web/events.go). A fragment is a top-level element with an | |
| 774 | 837 | // id: it replaces the element of the page that has the same id. A fragment | |
| 775 | 838 | // marked data-gone removes it; a row of a repository the home page does not | |
| 776 | - | // list yet is added at the top of the list. A slot (data-slot: the place of a | |
| 839 | + | // list yet is added at the top of the list. Activity updates also move existing | |
| 840 | + | // home rows to the top when sorted by newest activity; CI updates stay put. A slot (data-slot: the place of a | |
| 777 | 841 | // banner, a persistent status region) keeps its element and only gets the | |
| 778 | 842 | // children of the fragment, so that screen readers announce the change. A gap, | |
| 779 | 843 | // which says the page may have missed events or that what it shows changed, | |
| @@ -872,6 +936,9 @@ window.ermiteLive = (() => { | |||
| 872 | 936 | } | |
| 873 | 937 | if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', ''); | |
| 874 | 938 | old.replaceWith(el); | |
| 939 | + | const rows = document.getElementById('repo-rows'); | |
| 940 | + | if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' && | |
| 941 | + | rows.dataset.desc === 'true' && el.hasAttribute('data-activity')) rows.prepend(el); | |
| 875 | 942 | window.htmx?.process(el); | |
| 876 | 943 | fitIn(el); | |
| 877 | 944 | if (had) refocus(el, had); | |
M internal/adapters/web/static/js/mr.js +1 -11
| @@ -196,12 +196,6 @@ window.ermiteMR = (() => { | |||
| 196 | 196 | $('[data-default]', m)?.focus(); | |
| 197 | 197 | return true; | |
| 198 | 198 | } | |
| 199 | - | function closeMenus() { | |
| 200 | - | const open = $$('details.menu[open]'); | |
| 201 | - | for (const d of open) d.open = false; | |
| 202 | - | if (open.length) $('summary', open[0])?.focus(); | |
| 203 | - | return open.length > 0; | |
| 204 | - | } | |
| 205 | 199 | // menuKey handles a key while the merge menu is open; true when it used it. | |
| 206 | 200 | function menuKey(e) { | |
| 207 | 201 | const m = menu(); | |
| @@ -311,13 +305,9 @@ window.ermiteMR = (() => { | |||
| 311 | 305 | try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ } | |
| 312 | 306 | if (next) { e.preventDefault(); location.assign(next); } | |
| 313 | 307 | }); | |
| 314 | - | // A click outside an open menu closes it. | |
| 315 | - | addEventListener('click', (e) => { | |
| 316 | - | for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false; | |
| 317 | - | }); | |
| 318 | 308 | } | |
| 319 | 309 | ||
| 320 | - | return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile }; | |
| 310 | + | return { core, start, load, reveal, openMerge, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile }; | |
| 321 | 311 | })(); | |
| 322 | 312 | ||
| 323 | 313 | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start()); | |
M internal/adapters/web/static/js/store.js +5 -0
| @@ -74,3 +74,8 @@ window.ermiteStore = (() => { | |||
| 74 | 74 | ||
| 75 | 75 | return { get, set, remove, watch, pref }; | |
| 76 | 76 | })(); | |
| 77 | + | ||
| 78 | + | // The wide mode (ermiteWide in ermite.js) is set here, before the first paint: | |
| 79 | + | // shell.templ loads this file without defer, so a wide page never draws narrow | |
| 80 | + | // first. | |
| 81 | + | if (window.ermiteStore.get('wide') === '1') document?.documentElement?.classList?.add('wide'); | |
M internal/adapters/web/view.go +6 -4
| @@ -71,6 +71,9 @@ type Layout struct { | |||
| 71 | 71 | MR bool | |
| 72 | 72 | // IssueForm loads the script of the public issue form (issue-form.js). | |
| 73 | 73 | IssueForm bool | |
| 74 | + | // Inbox is the button to the owner's inbox, next to the logo; set on the | |
| 75 | + | // home page only, for the signed-in owner. | |
| 76 | + | Inbox *InboxButton | |
| 74 | 77 | } | |
| 75 | 78 | ||
| 76 | 79 | // Link is a plain label + href pair. | |
| @@ -93,7 +96,7 @@ type HomeRow struct { | |||
| 93 | 96 | Stats *HomeStats | |
| 94 | 97 | // Private marks a private repository; only set for a signed-in visitor. | |
| 95 | 98 | Private bool | |
| 96 | - | // Fresh marks a row that a push just updated (live updates only). | |
| 99 | + | // Fresh marks a row that activity just updated (live updates only). | |
| 97 | 100 | Fresh bool | |
| 98 | 101 | // Commits are the newest of the default branch. NoCommits says there is | |
| 99 | 102 | // none (never pushed, or an empty branch); a failed read says nothing. | |
| @@ -135,11 +138,10 @@ type SortLink struct { | |||
| 135 | 138 | ||
| 136 | 139 | // HomePage is the repository list. | |
| 137 | 140 | type HomePage struct { | |
| 141 | + | Sort string | |
| 142 | + | Desc bool | |
| 138 | 143 | Sorts []SortLink | |
| 139 | 144 | Rows []HomeRow | |
| 140 | - | // Inbox is the button to the owner's inbox; nil unless the owner is signed | |
| 141 | - | // in. | |
| 142 | - | Inbox *InboxButton | |
| 143 | 145 | } | |
| 144 | 146 | ||
| 145 | 147 | // InboxButton is the link to the inbox with the number of open items; Count is | |
M internal/app/testdata/web/blob-binary.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -239,6 +239,12 @@ | |||
| 239 | 239 | </div> | |
| 240 | 240 | <div> | |
| 241 | 241 | <dt> | |
| 242 | + | <kbd>w</kbd> | |
| 243 | + | </dt> | |
| 244 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 242 | 248 | <kbd>y</kbd> | |
| 243 | 249 | </dt> | |
| 244 | 250 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -416,6 +422,7 @@ | |||
| 416 | 422 | k-split from:body -> ermite.layout('split'); | |
| 417 | 423 | k-unified from:body -> ermite.layout('unified'); | |
| 418 | 424 | k-collapse from:body -> ermite.collapse(); | |
| 425 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 419 | 426 | k-expand from:body -> ermite.expandFile(); | |
| 420 | 427 | k-expandAll from:body -> ermite.expandAll(); | |
| 421 | 428 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-code.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -309,6 +309,12 @@ | |||
| 309 | 309 | </div> | |
| 310 | 310 | <div> | |
| 311 | 311 | <dt> | |
| 312 | + | <kbd>w</kbd> | |
| 313 | + | </dt> | |
| 314 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 315 | + | </div> | |
| 316 | + | <div> | |
| 317 | + | <dt> | |
| 312 | 318 | <kbd>y</kbd> | |
| 313 | 319 | </dt> | |
| 314 | 320 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -486,6 +492,7 @@ | |||
| 486 | 492 | k-split from:body -> ermite.layout('split'); | |
| 487 | 493 | k-unified from:body -> ermite.layout('unified'); | |
| 488 | 494 | k-collapse from:body -> ermite.collapse(); | |
| 495 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 489 | 496 | k-expand from:body -> ermite.expandFile(); | |
| 490 | 497 | k-expandAll from:body -> ermite.expandAll(); | |
| 491 | 498 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-empty.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -239,6 +239,12 @@ | |||
| 239 | 239 | </div> | |
| 240 | 240 | <div> | |
| 241 | 241 | <dt> | |
| 242 | + | <kbd>w</kbd> | |
| 243 | + | </dt> | |
| 244 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 242 | 248 | <kbd>y</kbd> | |
| 243 | 249 | </dt> | |
| 244 | 250 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -416,6 +422,7 @@ | |||
| 416 | 422 | k-split from:body -> ermite.layout('split'); | |
| 417 | 423 | k-unified from:body -> ermite.layout('unified'); | |
| 418 | 424 | k-collapse from:body -> ermite.collapse(); | |
| 425 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 419 | 426 | k-expand from:body -> ermite.expandFile(); | |
| 420 | 427 | k-expandAll from:body -> ermite.expandAll(); | |
| 421 | 428 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-image.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -241,6 +241,12 @@ | |||
| 241 | 241 | </div> | |
| 242 | 242 | <div> | |
| 243 | 243 | <dt> | |
| 244 | + | <kbd>w</kbd> | |
| 245 | + | </dt> | |
| 246 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 247 | + | </div> | |
| 248 | + | <div> | |
| 249 | + | <dt> | |
| 244 | 250 | <kbd>y</kbd> | |
| 245 | 251 | </dt> | |
| 246 | 252 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -418,6 +424,7 @@ | |||
| 418 | 424 | k-split from:body -> ermite.layout('split'); | |
| 419 | 425 | k-unified from:body -> ermite.layout('unified'); | |
| 420 | 426 | k-collapse from:body -> ermite.collapse(); | |
| 427 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 421 | 428 | k-expand from:body -> ermite.expandFile(); | |
| 422 | 429 | k-expandAll from:body -> ermite.expandAll(); | |
| 423 | 430 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-markdown.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -306,6 +306,12 @@ | |||
| 306 | 306 | </div> | |
| 307 | 307 | <div> | |
| 308 | 308 | <dt> | |
| 309 | + | <kbd>w</kbd> | |
| 310 | + | </dt> | |
| 311 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 312 | + | </div> | |
| 313 | + | <div> | |
| 314 | + | <dt> | |
| 309 | 315 | <kbd>y</kbd> | |
| 310 | 316 | </dt> | |
| 311 | 317 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -483,6 +489,7 @@ | |||
| 483 | 489 | k-split from:body -> ermite.layout('split'); | |
| 484 | 490 | k-unified from:body -> ermite.layout('unified'); | |
| 485 | 491 | k-collapse from:body -> ermite.collapse(); | |
| 492 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 486 | 493 | k-expand from:body -> ermite.expandFile(); | |
| 487 | 494 | k-expandAll from:body -> ermite.expandAll(); | |
| 488 | 495 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-svg.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -264,6 +264,12 @@ | |||
| 264 | 264 | </div> | |
| 265 | 265 | <div> | |
| 266 | 266 | <dt> | |
| 267 | + | <kbd>w</kbd> | |
| 268 | + | </dt> | |
| 269 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 270 | + | </div> | |
| 271 | + | <div> | |
| 272 | + | <dt> | |
| 267 | 273 | <kbd>y</kbd> | |
| 268 | 274 | </dt> | |
| 269 | 275 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -441,6 +447,7 @@ | |||
| 441 | 447 | k-split from:body -> ermite.layout('split'); | |
| 442 | 448 | k-unified from:body -> ermite.layout('unified'); | |
| 443 | 449 | k-collapse from:body -> ermite.collapse(); | |
| 450 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 444 | 451 | k-expand from:body -> ermite.expandFile(); | |
| 445 | 452 | k-expandAll from:body -> ermite.expandAll(); | |
| 446 | 453 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-text.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -259,6 +259,12 @@ | |||
| 259 | 259 | </div> | |
| 260 | 260 | <div> | |
| 261 | 261 | <dt> | |
| 262 | + | <kbd>w</kbd> | |
| 263 | + | </dt> | |
| 264 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 265 | + | </div> | |
| 266 | + | <div> | |
| 267 | + | <dt> | |
| 262 | 268 | <kbd>y</kbd> | |
| 263 | 269 | </dt> | |
| 264 | 270 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -436,6 +442,7 @@ | |||
| 436 | 442 | k-split from:body -> ermite.layout('split'); | |
| 437 | 443 | k-unified from:body -> ermite.layout('unified'); | |
| 438 | 444 | k-collapse from:body -> ermite.collapse(); | |
| 445 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 439 | 446 | k-expand from:body -> ermite.expandFile(); | |
| 440 | 447 | k-expandAll from:body -> ermite.expandAll(); | |
| 441 | 448 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-too-large.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -239,6 +239,12 @@ | |||
| 239 | 239 | </div> | |
| 240 | 240 | <div> | |
| 241 | 241 | <dt> | |
| 242 | + | <kbd>w</kbd> | |
| 243 | + | </dt> | |
| 244 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 242 | 248 | <kbd>y</kbd> | |
| 243 | 249 | </dt> | |
| 244 | 250 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -416,6 +422,7 @@ | |||
| 416 | 422 | k-split from:body -> ermite.layout('split'); | |
| 417 | 423 | k-unified from:body -> ermite.layout('unified'); | |
| 418 | 424 | k-collapse from:body -> ermite.collapse(); | |
| 425 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 419 | 426 | k-expand from:body -> ermite.expandFile(); | |
| 420 | 427 | k-expandAll from:body -> ermite.expandAll(); | |
| 421 | 428 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/blob-trojan.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -306,6 +306,12 @@ | |||
| 306 | 306 | </div> | |
| 307 | 307 | <div> | |
| 308 | 308 | <dt> | |
| 309 | + | <kbd>w</kbd> | |
| 310 | + | </dt> | |
| 311 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 312 | + | </div> | |
| 313 | + | <div> | |
| 314 | + | <dt> | |
| 309 | 315 | <kbd>y</kbd> | |
| 310 | 316 | </dt> | |
| 311 | 317 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -483,6 +489,7 @@ | |||
| 483 | 489 | k-split from:body -> ermite.layout('split'); | |
| 484 | 490 | k-unified from:body -> ermite.layout('unified'); | |
| 485 | 491 | k-collapse from:body -> ermite.collapse(); | |
| 492 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 486 | 493 | k-expand from:body -> ermite.expandFile(); | |
| 487 | 494 | k-expandAll from:body -> ermite.expandAll(); | |
| 488 | 495 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-big.html +16 -2
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -125,6 +125,11 @@ | |||
| 125 | 125 | </span> | |
| 126 | 126 | <span class="grow"> | |
| 127 | 127 | </span> | |
| 128 | + | <details class="menu vmenu"> | |
| 129 | + | <summary class="btn" aria-label="Diff view"> | |
| 130 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 131 | + | </summary> | |
| 132 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 128 | 133 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | 134 | </a> | |
| 130 | 135 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -134,9 +139,11 @@ | |||
| 134 | 139 | <kbd aria-hidden="true">x</kbd> | |
| 135 | 140 | </button> | |
| 136 | 141 | </div> | |
| 142 | + | </details> | |
| 143 | + | </div> | |
| 137 | 144 | <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p> | |
| 138 | 145 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 139 | - | <details class="dfile lnw-2" id="f1" data-file> | |
| 146 | + | <details class="dfile lnw-2" id="f1" data-file="big.txt"> | |
| 140 | 147 | <summary> | |
| 141 | 148 | <span class="st">A</span> | |
| 142 | 149 | <span class="p">big.txt</span> | |
| @@ -281,6 +288,12 @@ | |||
| 281 | 288 | </div> | |
| 282 | 289 | <div> | |
| 283 | 290 | <dt> | |
| 291 | + | <kbd>w</kbd> | |
| 292 | + | </dt> | |
| 293 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 294 | + | </div> | |
| 295 | + | <div> | |
| 296 | + | <dt> | |
| 284 | 297 | <kbd>y</kbd> | |
| 285 | 298 | </dt> | |
| 286 | 299 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -458,6 +471,7 @@ | |||
| 458 | 471 | k-split from:body -> ermite.layout('split'); | |
| 459 | 472 | k-unified from:body -> ermite.layout('unified'); | |
| 460 | 473 | k-collapse from:body -> ermite.collapse(); | |
| 474 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 461 | 475 | k-expand from:body -> ermite.expandFile(); | |
| 462 | 476 | k-expandAll from:body -> ermite.expandAll(); | |
| 463 | 477 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-binary.html +16 -2
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -121,6 +121,11 @@ | |||
| 121 | 121 | </span> | |
| 122 | 122 | <span class="grow"> | |
| 123 | 123 | </span> | |
| 124 | + | <details class="menu vmenu"> | |
| 125 | + | <summary class="btn" aria-label="Diff view"> | |
| 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | + | </summary> | |
| 128 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 124 | 129 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 125 | 130 | </a> | |
| 126 | 131 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -130,8 +135,10 @@ | |||
| 130 | 135 | <kbd aria-hidden="true">x</kbd> | |
| 131 | 136 | </button> | |
| 132 | 137 | </div> | |
| 138 | + | </details> | |
| 139 | + | </div> | |
| 133 | 140 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 134 | - | <details class="dfile lnw-2" id="f1" data-file> | |
| 141 | + | <details class="dfile lnw-2" id="f1" data-file="logo.bin"> | |
| 135 | 142 | <summary> | |
| 136 | 143 | <span class="st">M</span> | |
| 137 | 144 | <span class="p">logo.bin</span> | |
| @@ -273,6 +280,12 @@ | |||
| 273 | 280 | </div> | |
| 274 | 281 | <div> | |
| 275 | 282 | <dt> | |
| 283 | + | <kbd>w</kbd> | |
| 284 | + | </dt> | |
| 285 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 286 | + | </div> | |
| 287 | + | <div> | |
| 288 | + | <dt> | |
| 276 | 289 | <kbd>y</kbd> | |
| 277 | 290 | </dt> | |
| 278 | 291 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -450,6 +463,7 @@ | |||
| 450 | 463 | k-split from:body -> ermite.layout('split'); | |
| 451 | 464 | k-unified from:body -> ermite.layout('unified'); | |
| 452 | 465 | k-collapse from:body -> ermite.collapse(); | |
| 466 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 453 | 467 | k-expand from:body -> ermite.expandFile(); | |
| 454 | 468 | k-expandAll from:body -> ermite.expandAll(); | |
| 455 | 469 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-merge.html +16 -2
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -120,6 +120,11 @@ | |||
| 120 | 120 | </span> | |
| 121 | 121 | <span class="grow"> | |
| 122 | 122 | </span> | |
| 123 | + | <details class="menu vmenu"> | |
| 124 | + | <summary class="btn" aria-label="Diff view"> | |
| 125 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | + | </summary> | |
| 127 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 123 | 128 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 124 | 129 | </a> | |
| 125 | 130 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -129,8 +134,10 @@ | |||
| 129 | 134 | <kbd aria-hidden="true">x</kbd> | |
| 130 | 135 | </button> | |
| 131 | 136 | </div> | |
| 137 | + | </details> | |
| 138 | + | </div> | |
| 132 | 139 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 133 | - | <details class="dfile lnw-2" id="f1" data-file open> | |
| 140 | + | <details class="dfile lnw-2" id="f1" data-file="side.txt" open> | |
| 134 | 141 | <summary> | |
| 135 | 142 | <span class="st">A</span> | |
| 136 | 143 | <span class="p">side.txt</span> | |
| @@ -315,6 +322,12 @@ | |||
| 315 | 322 | </div> | |
| 316 | 323 | <div> | |
| 317 | 324 | <dt> | |
| 325 | + | <kbd>w</kbd> | |
| 326 | + | </dt> | |
| 327 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 328 | + | </div> | |
| 329 | + | <div> | |
| 330 | + | <dt> | |
| 318 | 331 | <kbd>y</kbd> | |
| 319 | 332 | </dt> | |
| 320 | 333 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -492,6 +505,7 @@ | |||
| 492 | 505 | k-split from:body -> ermite.layout('split'); | |
| 493 | 506 | k-unified from:body -> ermite.layout('unified'); | |
| 494 | 507 | k-collapse from:body -> ermite.collapse(); | |
| 508 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 495 | 509 | k-expand from:body -> ermite.expandFile(); | |
| 496 | 510 | k-expandAll from:body -> ermite.expandAll(); | |
| 497 | 511 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-modify-split.html +17 -3
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -127,6 +127,11 @@ And <b>html</b> & "quotes".</pre> | |||
| 127 | 127 | </span> | |
| 128 | 128 | <span class="grow"> | |
| 129 | 129 | </span> | |
| 130 | + | <details class="menu vmenu"> | |
| 131 | + | <summary class="btn" aria-label="Diff view"> | |
| 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | |
| 133 | + | </summary> | |
| 134 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 130 | 135 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 131 | 136 | </a> | |
| 132 | 137 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -136,8 +141,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 136 | 141 | <kbd aria-hidden="true">x</kbd> | |
| 137 | 142 | </button> | |
| 138 | 143 | </div> | |
| 144 | + | </details> | |
| 145 | + | </div> | |
| 139 | 146 | <div id="diffs" data-diff="split"> | |
| 140 | - | <details class="dfile lnw-2" id="f1" data-file open> | |
| 147 | + | <details class="dfile lnw-2" id="f1" data-file="a.txt" open> | |
| 141 | 148 | <summary> | |
| 142 | 149 | <span class="st">M</span> | |
| 143 | 150 | <span class="p">a.txt</span> | |
| @@ -277,7 +284,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 277 | 284 | </table> | |
| 278 | 285 | </div> | |
| 279 | 286 | </details> | |
| 280 | - | <details class="dfile lnw-2" id="f2" data-file> | |
| 287 | + | <details class="dfile lnw-2" id="f2" data-file="run.sh"> | |
| 281 | 288 | <summary> | |
| 282 | 289 | <span class="st">M</span> | |
| 283 | 290 | <span class="p">run.sh</span> | |
| @@ -438,6 +445,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 438 | 445 | </div> | |
| 439 | 446 | <div> | |
| 440 | 447 | <dt> | |
| 448 | + | <kbd>w</kbd> | |
| 449 | + | </dt> | |
| 450 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 451 | + | </div> | |
| 452 | + | <div> | |
| 453 | + | <dt> | |
| 441 | 454 | <kbd>y</kbd> | |
| 442 | 455 | </dt> | |
| 443 | 456 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -615,6 +628,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 615 | 628 | k-split from:body -> ermite.layout('split'); | |
| 616 | 629 | k-unified from:body -> ermite.layout('unified'); | |
| 617 | 630 | k-collapse from:body -> ermite.collapse(); | |
| 631 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 618 | 632 | k-expand from:body -> ermite.expandFile(); | |
| 619 | 633 | k-expandAll from:body -> ermite.expandAll(); | |
| 620 | 634 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-modify.html +17 -3
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -127,6 +127,11 @@ And <b>html</b> & "quotes".</pre> | |||
| 127 | 127 | </span> | |
| 128 | 128 | <span class="grow"> | |
| 129 | 129 | </span> | |
| 130 | + | <details class="menu vmenu"> | |
| 131 | + | <summary class="btn" aria-label="Diff view"> | |
| 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 133 | + | </summary> | |
| 134 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 130 | 135 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 131 | 136 | </a> | |
| 132 | 137 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -136,8 +141,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 136 | 141 | <kbd aria-hidden="true">x</kbd> | |
| 137 | 142 | </button> | |
| 138 | 143 | </div> | |
| 144 | + | </details> | |
| 145 | + | </div> | |
| 139 | 146 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 140 | - | <details class="dfile lnw-2" id="f1" data-file open> | |
| 147 | + | <details class="dfile lnw-2" id="f1" data-file="a.txt" open> | |
| 141 | 148 | <summary> | |
| 142 | 149 | <span class="st">M</span> | |
| 143 | 150 | <span class="p">a.txt</span> | |
| @@ -277,7 +284,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 277 | 284 | </table> | |
| 278 | 285 | </div> | |
| 279 | 286 | </details> | |
| 280 | - | <details class="dfile lnw-2" id="f2" data-file> | |
| 287 | + | <details class="dfile lnw-2" id="f2" data-file="run.sh"> | |
| 281 | 288 | <summary> | |
| 282 | 289 | <span class="st">M</span> | |
| 283 | 290 | <span class="p">run.sh</span> | |
| @@ -438,6 +445,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 438 | 445 | </div> | |
| 439 | 446 | <div> | |
| 440 | 447 | <dt> | |
| 448 | + | <kbd>w</kbd> | |
| 449 | + | </dt> | |
| 450 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 451 | + | </div> | |
| 452 | + | <div> | |
| 453 | + | <dt> | |
| 441 | 454 | <kbd>y</kbd> | |
| 442 | 455 | </dt> | |
| 443 | 456 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -615,6 +628,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 615 | 628 | k-split from:body -> ermite.layout('split'); | |
| 616 | 629 | k-unified from:body -> ermite.layout('unified'); | |
| 617 | 630 | k-collapse from:body -> ermite.collapse(); | |
| 631 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 618 | 632 | k-expand from:body -> ermite.expandFile(); | |
| 619 | 633 | k-expandAll from:body -> ermite.expandAll(); | |
| 620 | 634 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-rename.html +17 -3
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -121,6 +121,11 @@ | |||
| 121 | 121 | </span> | |
| 122 | 122 | <span class="grow"> | |
| 123 | 123 | </span> | |
| 124 | + | <details class="menu vmenu"> | |
| 125 | + | <summary class="btn" aria-label="Diff view"> | |
| 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | + | </summary> | |
| 128 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 124 | 129 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 125 | 130 | </a> | |
| 126 | 131 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -130,8 +135,10 @@ | |||
| 130 | 135 | <kbd aria-hidden="true">x</kbd> | |
| 131 | 136 | </button> | |
| 132 | 137 | </div> | |
| 138 | + | </details> | |
| 139 | + | </div> | |
| 133 | 140 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 134 | - | <details class="dfile lnw-2" id="f1" data-file> | |
| 141 | + | <details class="dfile lnw-2" id="f1" data-file="docs-old.txt"> | |
| 135 | 142 | <summary> | |
| 136 | 143 | <span class="st">R</span> | |
| 137 | 144 | <span class="p">old.txt → docs-old.txt</span> | |
| @@ -141,7 +148,7 @@ | |||
| 141 | 148 | </summary> | |
| 142 | 149 | <p class="note pad">renamed, 100% similar</p> | |
| 143 | 150 | </details> | |
| 144 | - | <details class="dfile lnw-2" id="f2" data-file open> | |
| 151 | + | <details class="dfile lnw-2" id="f2" data-file="gone.txt" open> | |
| 145 | 152 | <summary> | |
| 146 | 153 | <span class="st">D</span> | |
| 147 | 154 | <span class="p">gone.txt</span> | |
| @@ -342,6 +349,12 @@ | |||
| 342 | 349 | </div> | |
| 343 | 350 | <div> | |
| 344 | 351 | <dt> | |
| 352 | + | <kbd>w</kbd> | |
| 353 | + | </dt> | |
| 354 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 355 | + | </div> | |
| 356 | + | <div> | |
| 357 | + | <dt> | |
| 345 | 358 | <kbd>y</kbd> | |
| 346 | 359 | </dt> | |
| 347 | 360 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -519,6 +532,7 @@ | |||
| 519 | 532 | k-split from:body -> ermite.layout('split'); | |
| 520 | 533 | k-unified from:body -> ermite.layout('unified'); | |
| 521 | 534 | k-collapse from:body -> ermite.collapse(); | |
| 535 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 522 | 536 | k-expand from:body -> ermite.expandFile(); | |
| 523 | 537 | k-expandAll from:body -> ermite.expandAll(); | |
| 524 | 538 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-root.html +20 -6
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -119,6 +119,11 @@ | |||
| 119 | 119 | </span> | |
| 120 | 120 | <span class="grow"> | |
| 121 | 121 | </span> | |
| 122 | + | <details class="menu vmenu"> | |
| 123 | + | <summary class="btn" aria-label="Diff view"> | |
| 124 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 125 | + | </summary> | |
| 126 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 122 | 127 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 123 | 128 | </a> | |
| 124 | 129 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -128,8 +133,10 @@ | |||
| 128 | 133 | <kbd aria-hidden="true">x</kbd> | |
| 129 | 134 | </button> | |
| 130 | 135 | </div> | |
| 136 | + | </details> | |
| 137 | + | </div> | |
| 131 | 138 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 132 | - | <details class="dfile lnw-2" id="f1" data-file open> | |
| 139 | + | <details class="dfile lnw-2" id="f1" data-file="a.txt" open> | |
| 133 | 140 | <summary> | |
| 134 | 141 | <span class="st">A</span> | |
| 135 | 142 | <span class="p">a.txt</span> | |
| @@ -251,7 +258,7 @@ | |||
| 251 | 258 | </table> | |
| 252 | 259 | </div> | |
| 253 | 260 | </details> | |
| 254 | - | <details class="dfile lnw-2" id="f2" data-file open> | |
| 261 | + | <details class="dfile lnw-2" id="f2" data-file="gone.txt" open> | |
| 255 | 262 | <summary> | |
| 256 | 263 | <span class="st">A</span> | |
| 257 | 264 | <span class="p">gone.txt</span> | |
| @@ -301,7 +308,7 @@ | |||
| 301 | 308 | </table> | |
| 302 | 309 | </div> | |
| 303 | 310 | </details> | |
| 304 | - | <details class="dfile lnw-2" id="f3" data-file> | |
| 311 | + | <details class="dfile lnw-2" id="f3" data-file="logo.bin"> | |
| 305 | 312 | <summary> | |
| 306 | 313 | <span class="st">A</span> | |
| 307 | 314 | <span class="p">logo.bin</span> | |
| @@ -311,7 +318,7 @@ | |||
| 311 | 318 | </summary> | |
| 312 | 319 | <p class="note pad">binary file: no text diff</p> | |
| 313 | 320 | </details> | |
| 314 | - | <details class="dfile lnw-2" id="f4" data-file open> | |
| 321 | + | <details class="dfile lnw-2" id="f4" data-file="old.txt" open> | |
| 315 | 322 | <summary> | |
| 316 | 323 | <span class="st">A</span> | |
| 317 | 324 | <span class="p">old.txt</span> | |
| @@ -397,7 +404,7 @@ | |||
| 397 | 404 | </table> | |
| 398 | 405 | </div> | |
| 399 | 406 | </details> | |
| 400 | - | <details class="dfile lnw-2" id="f5" data-file open> | |
| 407 | + | <details class="dfile lnw-2" id="f5" data-file="run.sh" open> | |
| 401 | 408 | <summary> | |
| 402 | 409 | <span class="st">A</span> | |
| 403 | 410 | <span class="p">run.sh</span> | |
| @@ -661,6 +668,12 @@ | |||
| 661 | 668 | </div> | |
| 662 | 669 | <div> | |
| 663 | 670 | <dt> | |
| 671 | + | <kbd>w</kbd> | |
| 672 | + | </dt> | |
| 673 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 674 | + | </div> | |
| 675 | + | <div> | |
| 676 | + | <dt> | |
| 664 | 677 | <kbd>y</kbd> | |
| 665 | 678 | </dt> | |
| 666 | 679 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -838,6 +851,7 @@ | |||
| 838 | 851 | k-split from:body -> ermite.layout('split'); | |
| 839 | 852 | k-unified from:body -> ermite.layout('unified'); | |
| 840 | 853 | k-collapse from:body -> ermite.collapse(); | |
| 854 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 841 | 855 | k-expand from:body -> ermite.expandFile(); | |
| 842 | 856 | k-expandAll from:body -> ermite.expandAll(); | |
| 843 | 857 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commit-trojan.html +16 -2
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -122,6 +122,11 @@ | |||
| 122 | 122 | </span> | |
| 123 | 123 | <span class="grow"> | |
| 124 | 124 | </span> | |
| 125 | + | <details class="menu vmenu"> | |
| 126 | + | <summary class="btn" aria-label="Diff view"> | |
| 127 | + | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 128 | + | </summary> | |
| 129 | + | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 125 | 130 | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 126 | 131 | </a> | |
| 127 | 132 | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| @@ -131,9 +136,11 @@ | |||
| 131 | 136 | <kbd aria-hidden="true">x</kbd> | |
| 132 | 137 | </button> | |
| 133 | 138 | </div> | |
| 139 | + | </details> | |
| 140 | + | </div> | |
| 134 | 141 | <p class="notice">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class="inv"><U+XXXX></span> markers, never applied.</p> | |
| 135 | 142 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 136 | - | <details class="dfile lnw-2" id="f1" data-file open> | |
| 143 | + | <details class="dfile lnw-2" id="f1" data-file="evil.go" open> | |
| 137 | 144 | <summary> | |
| 138 | 145 | <span class="st">A</span> | |
| 139 | 146 | <span class="p">evil.go</span> | |
| @@ -490,6 +497,12 @@ | |||
| 490 | 497 | </div> | |
| 491 | 498 | <div> | |
| 492 | 499 | <dt> | |
| 500 | + | <kbd>w</kbd> | |
| 501 | + | </dt> | |
| 502 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 503 | + | </div> | |
| 504 | + | <div> | |
| 505 | + | <dt> | |
| 493 | 506 | <kbd>y</kbd> | |
| 494 | 507 | </dt> | |
| 495 | 508 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -667,6 +680,7 @@ | |||
| 667 | 680 | k-split from:body -> ermite.layout('split'); | |
| 668 | 681 | k-unified from:body -> ermite.layout('unified'); | |
| 669 | 682 | k-collapse from:body -> ermite.collapse(); | |
| 683 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 670 | 684 | k-expand from:body -> ermite.expandFile(); | |
| 671 | 685 | k-expandAll from:body -> ermite.expandAll(); | |
| 672 | 686 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commits-page2.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -266,6 +266,12 @@ | |||
| 266 | 266 | </div> | |
| 267 | 267 | <div> | |
| 268 | 268 | <dt> | |
| 269 | + | <kbd>w</kbd> | |
| 270 | + | </dt> | |
| 271 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 272 | + | </div> | |
| 273 | + | <div> | |
| 274 | + | <dt> | |
| 269 | 275 | <kbd>y</kbd> | |
| 270 | 276 | </dt> | |
| 271 | 277 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -429,6 +435,7 @@ | |||
| 429 | 435 | k-split from:body -> ermite.layout('split'); | |
| 430 | 436 | k-unified from:body -> ermite.layout('unified'); | |
| 431 | 437 | k-collapse from:body -> ermite.collapse(); | |
| 438 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 432 | 439 | k-expand from:body -> ermite.expandFile(); | |
| 433 | 440 | k-expandAll from:body -> ermite.expandAll(); | |
| 434 | 441 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commits-path.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -281,6 +281,12 @@ | |||
| 281 | 281 | </div> | |
| 282 | 282 | <div> | |
| 283 | 283 | <dt> | |
| 284 | + | <kbd>w</kbd> | |
| 285 | + | </dt> | |
| 286 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 287 | + | </div> | |
| 288 | + | <div> | |
| 289 | + | <dt> | |
| 284 | 290 | <kbd>y</kbd> | |
| 285 | 291 | </dt> | |
| 286 | 292 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -444,6 +450,7 @@ | |||
| 444 | 450 | k-split from:body -> ermite.layout('split'); | |
| 445 | 451 | k-unified from:body -> ermite.layout('unified'); | |
| 446 | 452 | k-collapse from:body -> ermite.collapse(); | |
| 453 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 447 | 454 | k-expand from:body -> ermite.expandFile(); | |
| 448 | 455 | k-expandAll from:body -> ermite.expandAll(); | |
| 449 | 456 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commits-root.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -1070,6 +1070,12 @@ | |||
| 1070 | 1070 | </div> | |
| 1071 | 1071 | <div> | |
| 1072 | 1072 | <dt> | |
| 1073 | + | <kbd>w</kbd> | |
| 1074 | + | </dt> | |
| 1075 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 1076 | + | </div> | |
| 1077 | + | <div> | |
| 1078 | + | <dt> | |
| 1073 | 1079 | <kbd>y</kbd> | |
| 1074 | 1080 | </dt> | |
| 1075 | 1081 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -1233,6 +1239,7 @@ | |||
| 1233 | 1239 | k-split from:body -> ermite.layout('split'); | |
| 1234 | 1240 | k-unified from:body -> ermite.layout('unified'); | |
| 1235 | 1241 | k-collapse from:body -> ermite.collapse(); | |
| 1242 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 1236 | 1243 | k-expand from:body -> ermite.expandFile(); | |
| 1237 | 1244 | k-expandAll from:body -> ermite.expandAll(); | |
| 1238 | 1245 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/commits-small.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -415,6 +415,12 @@ | |||
| 415 | 415 | </div> | |
| 416 | 416 | <div> | |
| 417 | 417 | <dt> | |
| 418 | + | <kbd>w</kbd> | |
| 419 | + | </dt> | |
| 420 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 421 | + | </div> | |
| 422 | + | <div> | |
| 423 | + | <dt> | |
| 418 | 424 | <kbd>y</kbd> | |
| 419 | 425 | </dt> | |
| 420 | 426 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -598,6 +604,7 @@ | |||
| 598 | 604 | k-split from:body -> ermite.layout('split'); | |
| 599 | 605 | k-unified from:body -> ermite.layout('unified'); | |
| 600 | 606 | k-collapse from:body -> ermite.collapse(); | |
| 607 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 601 | 608 | k-expand from:body -> ermite.expandFile(); | |
| 602 | 609 | k-expandAll from:body -> ermite.expandAll(); | |
| 603 | 610 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/empty.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -188,6 +188,12 @@ | |||
| 188 | 188 | </div> | |
| 189 | 189 | <div> | |
| 190 | 190 | <dt> | |
| 191 | + | <kbd>w</kbd> | |
| 192 | + | </dt> | |
| 193 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 194 | + | </div> | |
| 195 | + | <div> | |
| 196 | + | <dt> | |
| 191 | 197 | <kbd>y</kbd> | |
| 192 | 198 | </dt> | |
| 193 | 199 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -282,6 +288,7 @@ | |||
| 282 | 288 | k-split from:body -> ermite.layout('split'); | |
| 283 | 289 | k-unified from:body -> ermite.layout('unified'); | |
| 284 | 290 | k-collapse from:body -> ermite.collapse(); | |
| 291 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 285 | 292 | k-expand from:body -> ermite.expandFile(); | |
| 286 | 293 | k-expandAll from:body -> ermite.expandAll(); | |
| 287 | 294 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/home.html +40 -31
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -61,7 +61,7 @@ | |||
| 61 | 61 | </div> | |
| 62 | 62 | <nav class="sort" aria-label="Sort repositories"> | |
| 63 | 63 | <span class="mute">sort</span> | |
| 64 | - | <a href="/?sort=push&dir=asc" aria-current="true">last push <span aria-hidden="true">▼</span> | |
| 64 | + | <a href="/?sort=activity&dir=asc" aria-current="true">last activity <span aria-hidden="true">▼</span> | |
| 65 | 65 | <span class="sr">, descending, select for ascending</span> | |
| 66 | 66 | </a> | |
| 67 | 67 | <a href="/?sort=name&dir=asc">name </a> | |
| @@ -81,8 +81,31 @@ | |||
| 81 | 81 | </button> | |
| 82 | 82 | </div> | |
| 83 | 83 | </div> | |
| 84 | - | <ul class="repos" id="repo-rows" data-rows> | |
| 85 | - | <li id="repo-demo" class="repo" data-cursor> | |
| 84 | + | <ul class="repos" id="repo-rows" data-rows data-sort="activity" data-desc="true"> | |
| 85 | + | <li id="repo-quiet" class="repo" data-cursor> | |
| 86 | + | <div class="repo-main"> | |
| 87 | + | <h2 class="repo-name"> | |
| 88 | + | <a href="/quiet">quiet</a> | |
| 89 | + | </h2> | |
| 90 | + | <p class="repo-desc mute">no description</p> | |
| 91 | + | <div class="repo-topics" aria-hidden="true"> | |
| 92 | + | </div> | |
| 93 | + | <div class="repo-pulse" aria-hidden="true"> | |
| 94 | + | </div> | |
| 95 | + | </div> | |
| 96 | + | <div class="repo-head"> | |
| 97 | + | <dl class="repo-stats" aria-label="Figures of quiet"> | |
| 98 | + | <div class="rs-active"> | |
| 99 | + | <dt>last activity</dt> | |
| 100 | + | <dd> | |
| 101 | + | <time datetime="STAMP" title="STAMP">AGO</time> | |
| 102 | + | </dd> | |
| 103 | + | </div> | |
| 104 | + | </dl> | |
| 105 | + | </div> | |
| 106 | + | <p class="repo-commits mute">no commits yet</p> | |
| 107 | + | </li> | |
| 108 | + | <li id="repo-demo" class="repo"> | |
| 86 | 109 | <div class="repo-main"> | |
| 87 | 110 | <h2 class="repo-name"> | |
| 88 | 111 | <a href="/demo">demo</a> | |
| @@ -148,27 +171,6 @@ | |||
| 148 | 171 | </li> | |
| 149 | 172 | </ul> | |
| 150 | 173 | </li> | |
| 151 | - | <li id="repo-quiet" class="repo"> | |
| 152 | - | <div class="repo-main"> | |
| 153 | - | <h2 class="repo-name"> | |
| 154 | - | <a href="/quiet">quiet</a> | |
| 155 | - | </h2> | |
| 156 | - | <p class="repo-desc mute">no description</p> | |
| 157 | - | <div class="repo-topics" aria-hidden="true"> | |
| 158 | - | </div> | |
| 159 | - | <div class="repo-pulse" aria-hidden="true"> | |
| 160 | - | </div> | |
| 161 | - | </div> | |
| 162 | - | <div class="repo-head"> | |
| 163 | - | <dl class="repo-stats" aria-label="Figures of quiet"> | |
| 164 | - | <div class="rs-active"> | |
| 165 | - | <dt>last activity</dt> | |
| 166 | - | <dd class="mute">never</dd> | |
| 167 | - | </div> | |
| 168 | - | </dl> | |
| 169 | - | </div> | |
| 170 | - | <p class="repo-commits mute">no commits yet</p> | |
| 171 | - | </li> | |
| 172 | 174 | </ul> | |
| 173 | 175 | </section> | |
| 174 | 176 | </div> | |
| @@ -289,6 +291,12 @@ | |||
| 289 | 291 | </div> | |
| 290 | 292 | <div> | |
| 291 | 293 | <dt> | |
| 294 | + | <kbd>w</kbd> | |
| 295 | + | </dt> | |
| 296 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 297 | + | </div> | |
| 298 | + | <div> | |
| 299 | + | <dt> | |
| 292 | 300 | <kbd>y</kbd> | |
| 293 | 301 | </dt> | |
| 294 | 302 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -337,15 +345,15 @@ | |||
| 337 | 345 | <span class="k">go</span> | |
| 338 | 346 | </a> | |
| 339 | 347 | </li> | |
| 340 | - | <li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo"> | |
| 341 | - | <a href="/demo" tabindex="-1"> | |
| 342 | - | <span class="lbl">demo</span> | |
| 348 | + | <li id="cmd-o1" role="option" aria-selected="false" data-k="repo quiet"> | |
| 349 | + | <a href="/quiet" tabindex="-1"> | |
| 350 | + | <span class="lbl">quiet</span> | |
| 343 | 351 | <span class="k">repo</span> | |
| 344 | 352 | </a> | |
| 345 | 353 | </li> | |
| 346 | - | <li id="cmd-o2" role="option" aria-selected="false" data-k="repo quiet"> | |
| 347 | - | <a href="/quiet" tabindex="-1"> | |
| 348 | - | <span class="lbl">quiet</span> | |
| 354 | + | <li id="cmd-o2" role="option" aria-selected="false" data-k="repo demo"> | |
| 355 | + | <a href="/demo" tabindex="-1"> | |
| 356 | + | <span class="lbl">demo</span> | |
| 349 | 357 | <span class="k">repo</span> | |
| 350 | 358 | </a> | |
| 351 | 359 | </li> | |
| @@ -389,6 +397,7 @@ | |||
| 389 | 397 | k-split from:body -> ermite.layout('split'); | |
| 390 | 398 | k-unified from:body -> ermite.layout('unified'); | |
| 391 | 399 | k-collapse from:body -> ermite.collapse(); | |
| 400 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 392 | 401 | k-expand from:body -> ermite.expandFile(); | |
| 393 | 402 | k-expandAll from:body -> ermite.expandAll(); | |
| 394 | 403 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/not-found-page.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -177,6 +177,12 @@ | |||
| 177 | 177 | </div> | |
| 178 | 178 | <div> | |
| 179 | 179 | <dt> | |
| 180 | + | <kbd>w</kbd> | |
| 181 | + | </dt> | |
| 182 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 183 | + | </div> | |
| 184 | + | <div> | |
| 185 | + | <dt> | |
| 180 | 186 | <kbd>y</kbd> | |
| 181 | 187 | </dt> | |
| 182 | 188 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -265,6 +271,7 @@ | |||
| 265 | 271 | k-split from:body -> ermite.layout('split'); | |
| 266 | 272 | k-unified from:body -> ermite.layout('unified'); | |
| 267 | 273 | k-collapse from:body -> ermite.collapse(); | |
| 274 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 268 | 275 | k-expand from:body -> ermite.expandFile(); | |
| 269 | 276 | k-expandAll from:body -> ermite.expandAll(); | |
| 270 | 277 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/not-found.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -177,6 +177,12 @@ | |||
| 177 | 177 | </div> | |
| 178 | 178 | <div> | |
| 179 | 179 | <dt> | |
| 180 | + | <kbd>w</kbd> | |
| 181 | + | </dt> | |
| 182 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 183 | + | </div> | |
| 184 | + | <div> | |
| 185 | + | <dt> | |
| 180 | 186 | <kbd>y</kbd> | |
| 181 | 187 | </dt> | |
| 182 | 188 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -265,6 +271,7 @@ | |||
| 265 | 271 | k-split from:body -> ermite.layout('split'); | |
| 266 | 272 | k-unified from:body -> ermite.layout('unified'); | |
| 267 | 273 | k-collapse from:body -> ermite.collapse(); | |
| 274 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 268 | 275 | k-expand from:body -> ermite.expandFile(); | |
| 269 | 276 | k-expandAll from:body -> ermite.expandAll(); | |
| 270 | 277 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/refs.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -297,6 +297,12 @@ | |||
| 297 | 297 | </div> | |
| 298 | 298 | <div> | |
| 299 | 299 | <dt> | |
| 300 | + | <kbd>w</kbd> | |
| 301 | + | </dt> | |
| 302 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 303 | + | </div> | |
| 304 | + | <div> | |
| 305 | + | <dt> | |
| 300 | 306 | <kbd>y</kbd> | |
| 301 | 307 | </dt> | |
| 302 | 308 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -490,6 +496,7 @@ | |||
| 490 | 496 | k-split from:body -> ermite.layout('split'); | |
| 491 | 497 | k-unified from:body -> ermite.layout('unified'); | |
| 492 | 498 | k-collapse from:body -> ermite.collapse(); | |
| 499 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 493 | 500 | k-expand from:body -> ermite.expandFile(); | |
| 494 | 501 | k-expandAll from:body -> ermite.expandAll(); | |
| 495 | 502 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/summary.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -323,6 +323,12 @@ | |||
| 323 | 323 | </div> | |
| 324 | 324 | <div> | |
| 325 | 325 | <dt> | |
| 326 | + | <kbd>w</kbd> | |
| 327 | + | </dt> | |
| 328 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 329 | + | </div> | |
| 330 | + | <div> | |
| 331 | + | <dt> | |
| 326 | 332 | <kbd>y</kbd> | |
| 327 | 333 | </dt> | |
| 328 | 334 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -516,6 +522,7 @@ | |||
| 516 | 522 | k-split from:body -> ermite.layout('split'); | |
| 517 | 523 | k-unified from:body -> ermite.layout('unified'); | |
| 518 | 524 | k-collapse from:body -> ermite.collapse(); | |
| 525 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 519 | 526 | k-expand from:body -> ermite.expandFile(); | |
| 520 | 527 | k-expandAll from:body -> ermite.expandAll(); | |
| 521 | 528 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/tree-nested.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -254,6 +254,12 @@ | |||
| 254 | 254 | </div> | |
| 255 | 255 | <div> | |
| 256 | 256 | <dt> | |
| 257 | + | <kbd>w</kbd> | |
| 258 | + | </dt> | |
| 259 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 260 | + | </div> | |
| 261 | + | <div> | |
| 262 | + | <dt> | |
| 257 | 263 | <kbd>y</kbd> | |
| 258 | 264 | </dt> | |
| 259 | 265 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -431,6 +437,7 @@ | |||
| 431 | 437 | k-split from:body -> ermite.layout('split'); | |
| 432 | 438 | k-unified from:body -> ermite.layout('unified'); | |
| 433 | 439 | k-collapse from:body -> ermite.collapse(); | |
| 440 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 434 | 441 | k-expand from:body -> ermite.expandFile(); | |
| 435 | 442 | k-expandAll from:body -> ermite.expandAll(); | |
| 436 | 443 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/tree-root.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -475,6 +475,12 @@ | |||
| 475 | 475 | </div> | |
| 476 | 476 | <div> | |
| 477 | 477 | <dt> | |
| 478 | + | <kbd>w</kbd> | |
| 479 | + | </dt> | |
| 480 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 481 | + | </div> | |
| 482 | + | <div> | |
| 483 | + | <dt> | |
| 478 | 484 | <kbd>y</kbd> | |
| 479 | 485 | </dt> | |
| 480 | 486 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -652,6 +658,7 @@ | |||
| 652 | 658 | k-split from:body -> ermite.layout('split'); | |
| 653 | 659 | k-unified from:body -> ermite.layout('unified'); | |
| 654 | 660 | k-collapse from:body -> ermite.collapse(); | |
| 661 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 655 | 662 | k-expand from:body -> ermite.expandFile(); | |
| 656 | 663 | k-expandAll from:body -> ermite.expandAll(); | |
| 657 | 664 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/tree-slash-ref.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -251,6 +251,12 @@ | |||
| 251 | 251 | </div> | |
| 252 | 252 | <div> | |
| 253 | 253 | <dt> | |
| 254 | + | <kbd>w</kbd> | |
| 255 | + | </dt> | |
| 256 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 257 | + | </div> | |
| 258 | + | <div> | |
| 259 | + | <dt> | |
| 254 | 260 | <kbd>y</kbd> | |
| 255 | 261 | </dt> | |
| 256 | 262 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -428,6 +434,7 @@ | |||
| 428 | 434 | k-split from:body -> ermite.layout('split'); | |
| 429 | 435 | k-unified from:body -> ermite.layout('unified'); | |
| 430 | 436 | k-collapse from:body -> ermite.collapse(); | |
| 437 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 431 | 438 | k-expand from:body -> ermite.expandFile(); | |
| 432 | 439 | k-expandAll from:body -> ermite.expandAll(); | |
| 433 | 440 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/testdata/web/tree-subdir.html +8 -1
| @@ -20,7 +20,7 @@ | |||
| 20 | 20 | </script> | |
| 21 | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | |
| 22 | 22 | </script> | |
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | |
| 23 | + | <script src="/-/static/js/store.HASH.js"> | |
| 24 | 24 | </script> | |
| 25 | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | |
| 26 | 26 | </script> | |
| @@ -267,6 +267,12 @@ | |||
| 267 | 267 | </div> | |
| 268 | 268 | <div> | |
| 269 | 269 | <dt> | |
| 270 | + | <kbd>w</kbd> | |
| 271 | + | </dt> | |
| 272 | + | <dd>wide mode: use the whole width of the screen, or not (remembered)</dd> | |
| 273 | + | </div> | |
| 274 | + | <div> | |
| 275 | + | <dt> | |
| 270 | 276 | <kbd>y</kbd> | |
| 271 | 277 | </dt> | |
| 272 | 278 | <dd>copy a permalink pinned to the commit</dd> | |
| @@ -444,6 +450,7 @@ | |||
| 444 | 450 | k-split from:body -> ermite.layout('split'); | |
| 445 | 451 | k-unified from:body -> ermite.layout('unified'); | |
| 446 | 452 | k-collapse from:body -> ermite.collapse(); | |
| 453 | + | k-wide from:body -> window.ermiteWide.toggle(); | |
| 447 | 454 | k-expand from:body -> ermite.expandFile(); | |
| 448 | 455 | k-expandAll from:body -> ermite.expandAll(); | |
| 449 | 456 | k-permalink from:body -> ermite.permalink(); | |
M internal/app/web_events_test.go +2 -2
| @@ -123,7 +123,7 @@ func TestWebEventsHome(t *testing.T) { | |||
| 123 | 123 | assert.Equal(t, "home-list", ev.name) | |
| 124 | 124 | assert.NotEmpty(t, ev.id) | |
| 125 | 125 | assert.Contains(t, ev.data, `<li id="repo-pub"`) | |
| 126 | - | assert.Contains(t, ev.data, `new push`) | |
| 126 | + | assert.Contains(t, ev.data, `new activity`) | |
| 127 | 127 | assert.Contains(t, ev.data, `href="/pub"`) | |
| 128 | 128 | assert.Contains(t, ev.data, `<ul class="repo-commits" aria-label="Recent commits of pub">`, "the live row lists the commits too") | |
| 129 | 129 | ||
| @@ -134,7 +134,7 @@ func TestWebEventsHome(t *testing.T) { | |||
| 134 | 134 | assert.Equal(t, "home-list", ev.name) | |
| 135 | 135 | assert.Contains(t, ev.data, `<li id="repo-fresh"`) | |
| 136 | 136 | assert.Contains(t, ev.data, "a <b>fresh</b> one", "text is escaped") | |
| 137 | - | assert.NotContains(t, ev.data, "new push") | |
| 137 | + | assert.NotContains(t, ev.data, "new activity") | |
| 138 | 138 | ||
| 139 | 139 | // A repository that turns private is removed from the anonymous list. | |
| 140 | 140 | priv := domain.VisibilityPrivate | |
M internal/app/web_mr_test.go +2 -0
| @@ -268,6 +268,8 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 268 | 268 | assert.Contains(t, line.body, `name="path" value="search.go"`) | |
| 269 | 269 | assert.Contains(t, line.body, `name="line" value="3"`) | |
| 270 | 270 | assert.Contains(t, line.body, `name="side" value="new"`) | |
| 271 | + | assert.Contains(t, line.body, `comment on search.go:3 (new)`, "one line: the title names it") | |
| 272 | + | assert.Contains(t, b.get(base+"?on=new:1-3:search.go", as(token)).body, `comment on search.go:1-3 (new)`, "a range: the title names all of it") | |
| 271 | 273 | assert.Equal(t, 0, strings.Count(b.get(base+"?on=new:99:search.go", as(token)).body, `name="line" value="99"`), "a line the diff lacks has no form") | |
| 272 | 274 | assert.Equal(t, 0, strings.Count(b.get(base+"?on=new:3:search.go", nil).body, `<form`), "nor for a visitor") | |
| 273 | 275 | ||
M internal/domain/repo.go +1 -0
| @@ -42,6 +42,7 @@ type Repo struct { | |||
| 42 | 42 | Visibility Visibility | |
| 43 | 43 | CreatedAt time.Time | |
| 44 | 44 | PushedAt time.Time // zero until the first push | |
| 45 | + | ActiveAt time.Time // latest push, merge request or issue activity | |
| 45 | 46 | // Settings are the merge settings; the stores always fill them in, with | |
| 46 | 47 | // the defaults for a repository that never stored any. | |
| 47 | 48 | Settings RepoSettings | |
M internal/ports/dto.go +1 -1
| @@ -21,7 +21,7 @@ type RepoPage struct { | |||
| 21 | 21 | ||
| 22 | 22 | // RepoView is a repository with the Git state that belongs in its summary. | |
| 23 | 23 | type RepoView struct { | |
| 24 | - | domain.Repo | |
| 24 | + | domain.Repo // includes ActiveAt, the latest repository activity | |
| 25 | 25 | // DefaultBranch is the branch HEAD points to, even when it does not exist | |
| 26 | 26 | // yet; empty when HEAD is detached or the directory is missing. | |
| 27 | 27 | DefaultBranch string | |
M internal/ports/mocks/RepoMetaStore.go +36 -21
| @@ -289,59 +289,74 @@ func (_c *RepoMetaStore_List_Call) RunAndReturn(run func(ctx context.Context) ([ | |||
| 289 | 289 | return _c | |
| 290 | 290 | } | |
| 291 | 291 | ||
| 292 | - | // Update provides a mock function for the type RepoMetaStore | |
| 293 | - | func (_mock *RepoMetaStore) Update(ctx context.Context, repo domain.Repo) error { | |
| 294 | - | ret := _mock.Called(ctx, repo) | |
| 292 | + | // Mutate provides a mock function for the type RepoMetaStore | |
| 293 | + | func (_mock *RepoMetaStore) Mutate(ctx context.Context, name domain.RepoName, fn func(*domain.Repo) (changed bool, err error)) (domain.Repo, error) { | |
| 294 | + | ret := _mock.Called(ctx, name, fn) | |
| 295 | 295 | ||
| 296 | 296 | if len(ret) == 0 { | |
| 297 | - | panic("no return value specified for Update") | |
| 297 | + | panic("no return value specified for Mutate") | |
| 298 | 298 | } | |
| 299 | 299 | ||
| 300 | - | var r0 error | |
| 301 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Repo) error); ok { | |
| 302 | - | r0 = returnFunc(ctx, repo) | |
| 300 | + | var r0 domain.Repo | |
| 301 | + | var r1 error | |
| 302 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, func(*domain.Repo) (changed bool, err error)) (domain.Repo, error)); ok { | |
| 303 | + | return returnFunc(ctx, name, fn) | |
| 304 | + | } | |
| 305 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, func(*domain.Repo) (changed bool, err error)) domain.Repo); ok { | |
| 306 | + | r0 = returnFunc(ctx, name, fn) | |
| 303 | 307 | } else { | |
| 304 | - | r0 = ret.Error(0) | |
| 308 | + | r0 = ret.Get(0).(domain.Repo) | |
| 305 | 309 | } | |
| 306 | - | return r0 | |
| 310 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, func(*domain.Repo) (changed bool, err error)) error); ok { | |
| 311 | + | r1 = returnFunc(ctx, name, fn) | |
| 312 | + | } else { | |
| 313 | + | r1 = ret.Error(1) | |
| 314 | + | } | |
| 315 | + | return r0, r1 | |
| 307 | 316 | } | |
| 308 | 317 | ||
| 309 | - | // RepoMetaStore_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' | |
| 310 | - | type RepoMetaStore_Update_Call struct { | |
| 318 | + | // RepoMetaStore_Mutate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mutate' | |
| 319 | + | type RepoMetaStore_Mutate_Call struct { | |
| 311 | 320 | *mock.Call | |
| 312 | 321 | } | |
| 313 | 322 | ||
| 314 | - | // Update is a helper method to define mock.On call | |
| 323 | + | // Mutate is a helper method to define mock.On call | |
| 315 | 324 | // - ctx context.Context | |
| 316 | - | // - repo domain.Repo | |
| 317 | - | func (_e *RepoMetaStore_Expecter) Update(ctx any, repo any) *RepoMetaStore_Update_Call { | |
| 318 | - | return &RepoMetaStore_Update_Call{Call: _e.mock.On("Update", ctx, repo)} | |
| 325 | + | // - name domain.RepoName | |
| 326 | + | // - fn func(*domain.Repo) (changed bool, err error) | |
| 327 | + | func (_e *RepoMetaStore_Expecter) Mutate(ctx any, name any, fn any) *RepoMetaStore_Mutate_Call { | |
| 328 | + | return &RepoMetaStore_Mutate_Call{Call: _e.mock.On("Mutate", ctx, name, fn)} | |
| 319 | 329 | } | |
| 320 | 330 | ||
| 321 | - | func (_c *RepoMetaStore_Update_Call) Run(run func(ctx context.Context, repo domain.Repo)) *RepoMetaStore_Update_Call { | |
| 331 | + | func (_c *RepoMetaStore_Mutate_Call) Run(run func(ctx context.Context, name domain.RepoName, fn func(*domain.Repo) (changed bool, err error))) *RepoMetaStore_Mutate_Call { | |
| 322 | 332 | _c.Call.Run(func(args mock.Arguments) { | |
| 323 | 333 | var arg0 context.Context | |
| 324 | 334 | if args[0] != nil { | |
| 325 | 335 | arg0 = args[0].(context.Context) | |
| 326 | 336 | } | |
| 327 | - | var arg1 domain.Repo | |
| 337 | + | var arg1 domain.RepoName | |
| 328 | 338 | if args[1] != nil { | |
| 329 | - | arg1 = args[1].(domain.Repo) | |
| 339 | + | arg1 = args[1].(domain.RepoName) | |
| 340 | + | } | |
| 341 | + | var arg2 func(*domain.Repo) (changed bool, err error) | |
| 342 | + | if args[2] != nil { | |
| 343 | + | arg2 = args[2].(func(*domain.Repo) (changed bool, err error)) | |
| 330 | 344 | } | |
| 331 | 345 | run( | |
| 332 | 346 | arg0, | |
| 333 | 347 | arg1, | |
| 348 | + | arg2, | |
| 334 | 349 | ) | |
| 335 | 350 | }) | |
| 336 | 351 | return _c | |
| 337 | 352 | } | |
| 338 | 353 | ||
| 339 | - | func (_c *RepoMetaStore_Update_Call) Return(err error) *RepoMetaStore_Update_Call { | |
| 340 | - | _c.Call.Return(err) | |
| 354 | + | func (_c *RepoMetaStore_Mutate_Call) Return(repo domain.Repo, err error) *RepoMetaStore_Mutate_Call { | |
| 355 | + | _c.Call.Return(repo, err) | |
| 341 | 356 | return _c | |
| 342 | 357 | } | |
| 343 | 358 | ||
| 344 | - | func (_c *RepoMetaStore_Update_Call) RunAndReturn(run func(ctx context.Context, repo domain.Repo) error) *RepoMetaStore_Update_Call { | |
| 359 | + | func (_c *RepoMetaStore_Mutate_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName, fn func(*domain.Repo) (changed bool, err error)) (domain.Repo, error)) *RepoMetaStore_Mutate_Call { | |
| 345 | 360 | _c.Call.Return(run) | |
| 346 | 361 | return _c | |
| 347 | 362 | } | |
M internal/ports/ports.go +7 -4
| @@ -32,10 +32,13 @@ type RepoMetaStore interface { | |||
| 32 | 32 | Get(ctx context.Context, name domain.RepoName) (domain.Repo, error) | |
| 33 | 33 | // List returns every repository ordered by name. | |
| 34 | 34 | List(ctx context.Context) ([]domain.Repo, error) | |
| 35 | - | // Update replaces the stored repository. It returns domain.ErrNotFound if | |
| 36 | - | // there is none and domain.ErrConflict if another write changed the | |
| 37 | - | // stored repository since it was read. | |
| 38 | - | Update(ctx context.Context, repo domain.Repo) error | |
| 35 | + | // Mutate applies fn to the stored repository and writes under the version read. | |
| 36 | + | // Concurrent writes retry fn up to a bounded number of attempts, then return | |
| 37 | + | // domain.ErrConflict. fn must be pure and safe to call several times, and | |
| 38 | + | // must not change Name. changed=false writes nothing. It returns the stored | |
| 39 | + | // repository, or domain.ErrNotFound when absent. Errors from fn are returned | |
| 40 | + | // unchanged with nothing written. | |
| 41 | + | Mutate(ctx context.Context, name domain.RepoName, fn func(*domain.Repo) (changed bool, err error)) (domain.Repo, error) | |
| 39 | 42 | // Delete removes the repository and everything keyed to it: its merge | |
| 40 | 43 | // requests, their comments and read positions, and its number sequence. | |
| 41 | 44 | // A repository created again under the same name starts empty, with its | |
A internal/usecase/activity.go +43 -0
| @@ -0,0 +1,43 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package usecase | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "fmt" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/zerowrap" | |
| 12 | + | ||
| 13 | + | "bnema.dev/ermite/internal/domain" | |
| 14 | + | "bnema.dev/ermite/internal/ports" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // recordActivity is best-effort: a metadata failure never fails the action. | |
| 18 | + | func recordActivity(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, at time.Time) { | |
| 19 | + | if _, err := touchActiveAt(ctx, meta, name, at); err != nil { | |
| 20 | + | log := zerowrap.FromCtx(ctx) | |
| 21 | + | log.Error().Err(err).Str("repo", name.String()).Msg("record repository activity") | |
| 22 | + | } | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // touchActiveAt preserves later activity and retries concurrent metadata writes. | |
| 26 | + | // A repository deleted meanwhile is not an error. | |
| 27 | + | func touchActiveAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) { | |
| 28 | + | repo, err := meta.Mutate(ctx, name, func(repo *domain.Repo) (bool, error) { | |
| 29 | + | changed := false | |
| 30 | + | if now.After(repo.ActiveAt) { | |
| 31 | + | repo.ActiveAt = now | |
| 32 | + | changed = true | |
| 33 | + | } | |
| 34 | + | return changed, nil | |
| 35 | + | }) | |
| 36 | + | if errors.Is(err, domain.ErrNotFound) { | |
| 37 | + | return domain.Repo{}, nil | |
| 38 | + | } | |
| 39 | + | if err != nil { | |
| 40 | + | return repo, fmt.Errorf("update repository %q: %w", name, err) | |
| 41 | + | } | |
| 42 | + | return repo, nil | |
| 43 | + | } | |
A internal/usecase/activity_store_test.go +46 -0
| @@ -0,0 +1,46 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package usecase_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ||
| 11 | + | "bnema.dev/ermite/internal/adapters/kvstore" | |
| 12 | + | "bnema.dev/ermite/internal/domain" | |
| 13 | + | "bnema.dev/ermite/internal/usecase" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestActivityTimestampsOverStore(t *testing.T) { | |
| 17 | + | ctx := t.Context() | |
| 18 | + | s, err := kvstore.Open(ctx, t.TempDir()) | |
| 19 | + | require.NoError(t, err) | |
| 20 | + | t.Cleanup(func() { require.NoError(t, s.Close(ctx)) }) | |
| 21 | + | n, err := domain.ParseRepoName("proj") | |
| 22 | + | require.NoError(t, err) | |
| 23 | + | at := time.Date(2026, 10, 19, 12, 0, 0, 0, time.UTC) | |
| 24 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: at})) | |
| 25 | + | got, err := usecase.TouchPushedAt(ctx, s, n, at.Add(time.Hour)) | |
| 26 | + | require.NoError(t, err) | |
| 27 | + | require.Equal(t, got.PushedAt, got.ActiveAt) | |
| 28 | + | _, err = usecase.TouchActiveAt(ctx, s, n, at.Add(3*time.Hour)) | |
| 29 | + | require.NoError(t, err) | |
| 30 | + | _, err = usecase.TouchPushedAt(ctx, s, n, at.Add(2*time.Hour)) | |
| 31 | + | require.NoError(t, err) | |
| 32 | + | _, err = usecase.TouchPushedAt(ctx, s, n, at) | |
| 33 | + | require.NoError(t, err) | |
| 34 | + | _, err = usecase.TouchActiveAt(ctx, s, n, at) | |
| 35 | + | require.NoError(t, err) | |
| 36 | + | got, err = s.Get(ctx, n) | |
| 37 | + | require.NoError(t, err) | |
| 38 | + | require.Equal(t, at.Add(2*time.Hour), got.PushedAt) | |
| 39 | + | require.Equal(t, at.Add(3*time.Hour), got.ActiveAt) | |
| 40 | + | require.Equal(t, domain.VisibilityPrivate, got.Visibility) | |
| 41 | + | require.NoError(t, s.Delete(ctx, n)) | |
| 42 | + | _, err = usecase.TouchPushedAt(ctx, s, n, at) | |
| 43 | + | require.NoError(t, err) | |
| 44 | + | _, err = usecase.TouchActiveAt(ctx, s, n, at) | |
| 45 | + | require.NoError(t, err) | |
| 46 | + | } | |
A internal/usecase/activity_test.go +47 -0
| @@ -0,0 +1,47 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package usecase | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 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/mocks" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | func TestTouchActiveAt(t *testing.T) { | |
| 19 | + | name, err := domain.ParseRepoName("proj") | |
| 20 | + | require.NoError(t, err) | |
| 21 | + | now := time.Now().UTC() | |
| 22 | + | t.Run("preserves later activity", func(t *testing.T) { | |
| 23 | + | meta := mocks.NewRepoMetaStore(t) | |
| 24 | + | later := domain.Repo{Name: name, ActiveAt: now.Add(time.Hour)} | |
| 25 | + | meta.EXPECT().Mutate(mock.Anything, name, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.RepoName, fn func(*domain.Repo) (bool, error)) (domain.Repo, error) { | |
| 26 | + | repo := later | |
| 27 | + | changed, err := fn(&repo) | |
| 28 | + | require.NoError(t, err) | |
| 29 | + | require.False(t, changed) | |
| 30 | + | return repo, nil | |
| 31 | + | }) | |
| 32 | + | got, err := touchActiveAt(t.Context(), meta, name, now) | |
| 33 | + | require.NoError(t, err) | |
| 34 | + | require.Equal(t, later, got) | |
| 35 | + | }) | |
| 36 | + | t.Run("deleted repository", func(t *testing.T) { | |
| 37 | + | meta := mocks.NewRepoMetaStore(t) | |
| 38 | + | meta.EXPECT().Mutate(mock.Anything, name, mock.Anything).Return(domain.Repo{}, domain.ErrNotFound) | |
| 39 | + | _, err := touchActiveAt(t.Context(), meta, name, now) | |
| 40 | + | require.NoError(t, err) | |
| 41 | + | }) | |
| 42 | + | t.Run("best effort failure", func(t *testing.T) { | |
| 43 | + | meta := mocks.NewRepoMetaStore(t) | |
| 44 | + | meta.EXPECT().Mutate(mock.Anything, name, mock.Anything).Return(domain.Repo{}, errors.New("offline")) | |
| 45 | + | require.NotPanics(t, func() { recordActivity(t.Context(), meta, name, now) }) | |
| 46 | + | }) | |
| 47 | + | } | |
M internal/usecase/export_test.go +10 -0
| @@ -19,6 +19,16 @@ import ( | |||
| 19 | 19 | // Sweep exposes the daily sweep to the tests. | |
| 20 | 20 | func (c *CI) Sweep(ctx context.Context) { c.sweep(ctx) } | |
| 21 | 21 | ||
| 22 | + | // TouchPushedAt exposes touchPushedAt to the tests. | |
| 23 | + | func TouchPushedAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) { | |
| 24 | + | return touchPushedAt(ctx, meta, name, now) | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // TouchActiveAt exposes touchActiveAt to the tests. | |
| 28 | + | func TouchActiveAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) { | |
| 29 | + | return touchActiveAt(ctx, meta, name, now) | |
| 30 | + | } | |
| 31 | + | ||
| 22 | 32 | // RunTitle gives the title of a run as startRun sets it: the evaluated | |
| 23 | 33 | // run-name, else commitTitle. | |
| 24 | 34 | func (c *CI) RunTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, commitTitle string) string { | |
M internal/usecase/issue_moderation.go +1 -0
| @@ -85,6 +85,7 @@ func (m *IssueModeration) Approve(ctx context.Context, p domain.Principal, repo | |||
| 85 | 85 | log := zerowrap.FromCtx(ctx) | |
| 86 | 86 | log.Info().Str("repo", repo.String()).Int64("number", issue.Number). | |
| 87 | 87 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("pending issue approved") | |
| 88 | + | recordActivity(ctx, m.meta, repo, now) | |
| 88 | 89 | m.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: r.Visibility, CreatedAt: r.CreatedAt}) | |
| 89 | 90 | return issue, nil | |
| 90 | 91 | } | |
M internal/usecase/issue_moderation_test.go +1 -0
| @@ -89,6 +89,7 @@ func TestIssueModerationApprove(t *testing.T) { | |||
| 89 | 89 | u := newModerationUnit(t, domain.IssuesPublic) | |
| 90 | 90 | issue := domain.Issue{Repo: u.repo.Name, Number: 7, State: domain.IssueStateOpen} | |
| 91 | 91 | u.pending.EXPECT().Approve(mock.Anything, u.repo.Name, u.id, now).Return(issue, nil).Once() | |
| 92 | + | expectRepoMutation(t, u.meta, u.repo, mock.MatchedBy(func(r domain.Repo) bool { return r.ActiveAt.Equal(now) })).Once() | |
| 92 | 93 | u.bus.EXPECT().Publish(mock.Anything, domain.IssueOpened{Issue: issue, At: now, Visibility: u.repo.Visibility, CreatedAt: u.repo.CreatedAt}).Once() | |
| 93 | 94 | got, err := u.mod.Approve(ctx, domain.Owner(), u.repo.Name, u.id) | |
| 94 | 95 | require.NoError(t, err) | |
M internal/usecase/issues.go +3 -0
| @@ -124,6 +124,7 @@ func (i *Issues) Create(ctx context.Context, p domain.Principal, name domain.Rep | |||
| 124 | 124 | log := zerowrap.FromCtx(ctx) | |
| 125 | 125 | log.Info().Str("repo", name.String()).Int64("number", issue.Number). | |
| 126 | 126 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue opened") | |
| 127 | + | recordActivity(ctx, i.meta, repo.Name, now) | |
| 127 | 128 | i.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 128 | 129 | return issue, nil | |
| 129 | 130 | } | |
| @@ -211,6 +212,7 @@ func (i *Issues) AddComment(ctx context.Context, p domain.Principal, name domain | |||
| 211 | 212 | log := zerowrap.FromCtx(ctx) | |
| 212 | 213 | log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID). | |
| 213 | 214 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue comment added") | |
| 215 | + | recordActivity(ctx, i.meta, name, c.CreatedAt) | |
| 214 | 216 | i.bus.Publish(ctx, domain.IssueCommented{ | |
| 215 | 217 | Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | |
| 216 | 218 | }) | |
| @@ -260,6 +262,7 @@ func (i *Issues) transition(ctx context.Context, p domain.Principal, name domain | |||
| 260 | 262 | log := zerowrap.FromCtx(ctx) | |
| 261 | 263 | log.Info().Str("repo", name.String()).Int64("number", number). | |
| 262 | 264 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue " + verb + "d") | |
| 265 | + | recordActivity(ctx, i.meta, repo.Name, issue.UpdatedAt) | |
| 263 | 266 | if to == domain.IssueStateOpen { | |
| 264 | 267 | i.bus.Publish(ctx, domain.IssueReopened{Issue: issue, At: issue.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 265 | 268 | } else { | |
M internal/usecase/issues_integration_test.go +67 -0
| @@ -6,14 +6,17 @@ import ( | |||
| 6 | 6 | "context" | |
| 7 | 7 | "path/filepath" | |
| 8 | 8 | "testing" | |
| 9 | + | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/stretchr/testify/assert" | |
| 12 | + | "github.com/stretchr/testify/mock" | |
| 11 | 13 | "github.com/stretchr/testify/require" | |
| 12 | 14 | ||
| 13 | 15 | "bnema.dev/ermite/internal/adapters/eventbus" | |
| 14 | 16 | "bnema.dev/ermite/internal/adapters/kvstore" | |
| 15 | 17 | "bnema.dev/ermite/internal/domain" | |
| 16 | 18 | "bnema.dev/ermite/internal/ports" | |
| 19 | + | "bnema.dev/ermite/internal/ports/mocks" | |
| 17 | 20 | "bnema.dev/ermite/internal/usecase" | |
| 18 | 21 | ) | |
| 19 | 22 | ||
| @@ -46,6 +49,10 @@ func TestIssuesOverTheStore(t *testing.T) { | |||
| 46 | 49 | require.NoError(t, err) | |
| 47 | 50 | assert.EqualValues(t, 2, issue.Number, "the number follows the merge request") | |
| 48 | 51 | assert.Equal(t, "bot", issue.Author) | |
| 52 | + | repo, err := store.Get(ctx, n) | |
| 53 | + | require.NoError(t, err) | |
| 54 | + | assert.True(t, repo.ActiveAt.Equal(now)) | |
| 55 | + | assert.True(t, repo.PushedAt.IsZero()) | |
| 49 | 56 | ||
| 50 | 57 | kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1) | |
| 51 | 58 | require.NoError(t, err) | |
| @@ -85,3 +92,63 @@ func TestIssuesOverTheStore(t *testing.T) { | |||
| 85 | 92 | } | |
| 86 | 93 | assert.Equal(t, []string{"issue.opened", "issue.commented", "issue.closed", "issue.commented", "issue.reopened"}, kinds) | |
| 87 | 94 | } | |
| 95 | + | ||
| 96 | + | // Moderated submissions are private work until approval makes them public activity. | |
| 97 | + | func TestModeratedIssueActivityOverStore(t *testing.T) { | |
| 98 | + | ctx := t.Context() | |
| 99 | + | store, err := kvstore.Open(ctx, t.TempDir()) | |
| 100 | + | require.NoError(t, err) | |
| 101 | + | t.Cleanup(func() { require.NoError(t, store.Close(ctx)) }) | |
| 102 | + | n := name(t, "proj") | |
| 103 | + | initial := now.Add(-time.Hour) | |
| 104 | + | require.NoError(t, store.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPublic, CreatedAt: initial, ActiveAt: initial, Settings: withIssues(domain.IssuesPublic)})) | |
| 105 | + | clock := mocks.NewClock(t) | |
| 106 | + | at := now | |
| 107 | + | clock.EXPECT().Now().RunAndReturn(func() time.Time { return at }).Maybe() | |
| 108 | + | keys, err := usecase.NewIssueKeys(ctx, store.IssueKeys()) | |
| 109 | + | require.NoError(t, err) | |
| 110 | + | limiter := mocks.NewSubmissionLimiter(t) | |
| 111 | + | limiter.EXPECT().Recent(mock.Anything, mock.Anything).Return(0, 0).Maybe() | |
| 112 | + | limiter.EXPECT().Reserve(mock.Anything, mock.Anything).Return(true, time.Duration(0), func() {}).Maybe() | |
| 113 | + | bus := mocks.NewEventBus(t) | |
| 114 | + | var kinds []string | |
| 115 | + | bus.EXPECT().Publish(mock.Anything, mock.Anything).Run(func(_ context.Context, ev domain.Event) { | |
| 116 | + | repo, err := store.Get(ctx, n) | |
| 117 | + | require.NoError(t, err) | |
| 118 | + | if ev.EventKind() == "issue.opened" { | |
| 119 | + | require.Equal(t, at, repo.ActiveAt) | |
| 120 | + | } else { | |
| 121 | + | require.Equal(t, initial, repo.ActiveAt) | |
| 122 | + | } | |
| 123 | + | kinds = append(kinds, ev.EventKind()) | |
| 124 | + | }).Maybe() | |
| 125 | + | submissions := usecase.NewIssueSubmissions(usecase.IssueSubmissionsConfig{Meta: store, Pending: store.PendingIssues(), Bans: store.IssueBans(), Limiter: limiter, Keys: keys, Bus: bus, Clock: clock, BaseDifficulty: testBits, MaxDifficulty: testBits}) | |
| 126 | + | moderation := usecase.NewIssueModeration(usecase.IssueModerationConfig{Meta: store, Pending: store.PendingIssues(), Bans: store.IssueBans(), Bus: bus, Clock: clock}) | |
| 127 | + | submit := func() domain.PendingIssueID { | |
| 128 | + | ch, err := submissions.Challenge(ctx, n, "client") | |
| 129 | + | require.NoError(t, err) | |
| 130 | + | sub := ports.IssueSubmission{Token: ch.Token, FormToken: ch.FormToken, Solution: solve(t, ch), Title: "Bug", Body: "Details"} | |
| 131 | + | at = at.Add(usecase.MinFillTime) | |
| 132 | + | require.NoError(t, submissions.Submit(ctx, n, "client", sub)) | |
| 133 | + | repo, err := store.Get(ctx, n) | |
| 134 | + | require.NoError(t, err) | |
| 135 | + | require.Equal(t, initial, repo.ActiveAt) | |
| 136 | + | page, err := store.PendingIssues().List(ctx, n, ports.ListPendingParams{PageSize: 10}) | |
| 137 | + | require.NoError(t, err) | |
| 138 | + | require.Len(t, page.Issues, 1) | |
| 139 | + | return page.Issues[0].ID | |
| 140 | + | } | |
| 141 | + | id := submit() | |
| 142 | + | require.NoError(t, moderation.Reject(ctx, domain.Owner(), n, id, false)) | |
| 143 | + | repo, err := store.Get(ctx, n) | |
| 144 | + | require.NoError(t, err) | |
| 145 | + | require.Equal(t, initial, repo.ActiveAt) | |
| 146 | + | id = submit() | |
| 147 | + | _, err = moderation.Approve(ctx, domain.Owner(), n, id) | |
| 148 | + | require.NoError(t, err) | |
| 149 | + | repo, err = store.Get(ctx, n) | |
| 150 | + | require.NoError(t, err) | |
| 151 | + | require.Equal(t, at, repo.ActiveAt) | |
| 152 | + | require.True(t, repo.PushedAt.IsZero()) | |
| 153 | + | require.Equal(t, []string{"issue.submitted", "issue.pending_removed", "issue.submitted", "issue.opened"}, kinds) | |
| 154 | + | } | |
M internal/usecase/issues_test.go +2 -0
| @@ -39,6 +39,7 @@ func newIssueUnit(t *testing.T, vis domain.Visibility, mode domain.IssuesMode) * | |||
| 39 | 39 | clock := mocks.NewClock(t) | |
| 40 | 40 | clock.EXPECT().Now().Return(now).Maybe() | |
| 41 | 41 | u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil).Maybe() | |
| 42 | + | expectRepoMutation(t, u.meta, u.repo, mock.MatchedBy(func(r domain.Repo) bool { return r.ActiveAt.Equal(now) && r.PushedAt.IsZero() })).Maybe() | |
| 42 | 43 | u.issues = usecase.NewIssues(usecase.IssuesConfig{Meta: u.meta, Store: u.store, MergeRequests: u.mrs, Bus: u.bus, Clock: clock}) | |
| 43 | 44 | return u | |
| 44 | 45 | } | |
| @@ -116,6 +117,7 @@ func TestIssueCreateAndTransitions(t *testing.T) { | |||
| 116 | 117 | got, err := u.issues.Create(ctx, p, u.repo.Name, ports.CreateIssueParams{Title: " Crash ", Body: "why"}) | |
| 117 | 118 | require.NoError(t, err) | |
| 118 | 119 | assert.EqualValues(t, 7, got.Number) | |
| 120 | + | u.meta.AssertCalled(t, "Mutate", mock.Anything, u.repo.Name, mock.Anything) | |
| 119 | 121 | }) | |
| 120 | 122 | ||
| 121 | 123 | t.Run("invalid text stores nothing", func(t *testing.T) { | |
M internal/usecase/mergerequests.go +5 -0
| @@ -254,6 +254,7 @@ func (m *MergeRequests) Create(ctx context.Context, p domain.Principal, name dom | |||
| 254 | 254 | if err != nil { | |
| 255 | 255 | return domain.MergeRequest{}, err | |
| 256 | 256 | } | |
| 257 | + | recordActivity(ctx, m.meta, repo.Name, now) | |
| 257 | 258 | m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 258 | 259 | if m.pulls != nil { | |
| 259 | 260 | // The commits the opening is for are the ones of now. | |
| @@ -377,6 +378,7 @@ func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name | |||
| 377 | 378 | if err != nil { | |
| 378 | 379 | return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err) | |
| 379 | 380 | } | |
| 381 | + | recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt) | |
| 380 | 382 | m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 381 | 383 | return m.withHead(ctx, mr) | |
| 382 | 384 | } | |
| @@ -656,6 +658,7 @@ func (m *MergeRequests) AddComment(ctx context.Context, p domain.Principal, name | |||
| 656 | 658 | log := zerowrap.FromCtx(ctx) | |
| 657 | 659 | log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID). | |
| 658 | 660 | Int64("reply_to", c.ReplyTo).Str("verdict", c.Verdict.String()).Bool("on_line", !general).Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("comment added") | |
| 661 | + | recordActivity(ctx, m.meta, name, c.CreatedAt) | |
| 659 | 662 | m.bus.Publish(ctx, domain.CommentAdded{ | |
| 660 | 663 | Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | |
| 661 | 664 | }) | |
| @@ -796,6 +799,7 @@ func (m *MergeRequests) Close(ctx context.Context, p domain.Principal, name doma | |||
| 796 | 799 | log := zerowrap.FromCtx(ctx) | |
| 797 | 800 | log.Info().Str("repo", name.String()).Int64("number", number). | |
| 798 | 801 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request closed") | |
| 802 | + | recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt) | |
| 799 | 803 | m.bus.Publish(ctx, domain.MergeRequestClosed{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 800 | 804 | if m.pulls != nil { | |
| 801 | 805 | m.pulls.OnPullRequest(ctx, name, ports.PullRequestEvent{Number: number, Action: domain.PullClosed, Head: mr.HeadID, Base: mr.BaseID}, p) | |
| @@ -998,6 +1002,7 @@ func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name doma | |||
| 998 | 1002 | } | |
| 999 | 1003 | } | |
| 1000 | 1004 | at := mr.UpdatedAt | |
| 1005 | + | recordActivity(ctx, m.meta, repo.Name, at) | |
| 1001 | 1006 | m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 1002 | 1007 | m.bus.Publish(ctx, domain.PushEvent{ | |
| 1003 | 1008 | Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates, | |
M internal/usecase/mergerequests_integration_test.go +11 -0
| @@ -1448,3 +1448,14 @@ func TestNoteThreadsRangesAndVerdicts(t *testing.T) { | |||
| 1448 | 1448 | _, err = r.mrs.DeleteComment(r.ctx, root, r.repo, mr.Number, changes.ID) | |
| 1449 | 1449 | assert.ErrorIs(t, err, domain.ErrPreconditionFailed) | |
| 1450 | 1450 | } | |
| 1451 | + | ||
| 1452 | + | func TestMergeRequestRecordsRepositoryActivity(t *testing.T) { | |
| 1453 | + | r := newMRRig(t) | |
| 1454 | + | r.seed() | |
| 1455 | + | r.feature("activity") | |
| 1456 | + | _, err := r.mrs.Create(r.ctx, root, r.repo, ports.CreateMergeRequestParams{Title: "Activity", Source: "activity", Target: "main"}) | |
| 1457 | + | require.NoError(t, err) | |
| 1458 | + | repo, err := r.store.Get(r.ctx, r.repo) | |
| 1459 | + | require.NoError(t, err) | |
| 1460 | + | assert.True(t, repo.ActiveAt.Equal(now)) | |
| 1461 | + | } | |
M internal/usecase/pushes.go +21 -29
| @@ -15,9 +15,6 @@ import ( | |||
| 15 | 15 | "bnema.dev/ermite/internal/ports" | |
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | - | // pushRetries bounds the retries of a metadata update that loses a race. | |
| 19 | - | const pushRetries = 5 | |
| 20 | - | ||
| 21 | 18 | // Pushes are the use cases around a Git push. | |
| 22 | 19 | type Pushes struct { | |
| 23 | 20 | meta ports.RepoMetaStore | |
| @@ -209,7 +206,7 @@ func (u *Pushes) checkWorkflows(ctx context.Context, name domain.RepoName, updat | |||
| 209 | 206 | return nil | |
| 210 | 207 | } | |
| 211 | 208 | ||
| 212 | - | // Record is the post-receive step of an accepted push: it sets pushed_at, | |
| 209 | + | // Record is the post-receive step of an accepted push: it sets pushed_at and active_at, | |
| 213 | 210 | // repairs HEAD when it points to a missing branch, tells the reader that the | |
| 214 | 211 | // repository changed, queues the workflows the push triggers, publishes a | |
| 215 | 212 | // PushEvent and schedules maintenance. Every | |
| @@ -245,36 +242,31 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep | |||
| 245 | 242 | return errors.Join(errs...) | |
| 246 | 243 | } | |
| 247 | 244 | ||
| 248 | - | // touchPushedAt stores pushed_at, retrying when another write got in between. | |
| 245 | + | // touchPushedAt stores pushed_at and active_at, preserving later timestamps | |
| 246 | + | // and retrying when another write got in between. | |
| 249 | 247 | // A repository deleted meanwhile is not an error. It returns the repository | |
| 250 | - | // as read, or the zero value when it is unknown. A push (Pushes.Record) and a | |
| 251 | - | // merge, which moves a branch too, both record through it. | |
| 248 | + | // as stored after mutation, or the zero value when it is unknown. A push | |
| 249 | + | // (Pushes.Record) and a merge, which moves a branch too, both record through it. | |
| 252 | 250 | func touchPushedAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) { | |
| 253 | - | for range pushRetries { | |
| 254 | - | repo, err := meta.Get(ctx, name) | |
| 255 | - | if errors.Is(err, domain.ErrNotFound) { | |
| 256 | - | return domain.Repo{}, nil | |
| 257 | - | } | |
| 258 | - | if err != nil { | |
| 259 | - | return domain.Repo{}, fmt.Errorf("get repository %q: %w", name, err) | |
| 260 | - | } | |
| 261 | - | if !now.After(repo.PushedAt) { | |
| 262 | - | return repo, nil // a later push already recorded | |
| 263 | - | } | |
| 264 | - | repo.PushedAt = now | |
| 265 | - | err = meta.Update(ctx, repo) | |
| 266 | - | if errors.Is(err, domain.ErrConflict) { | |
| 267 | - | continue | |
| 251 | + | repo, err := meta.Mutate(ctx, name, func(repo *domain.Repo) (bool, error) { | |
| 252 | + | changed := false | |
| 253 | + | if now.After(repo.PushedAt) { | |
| 254 | + | repo.PushedAt = now | |
| 255 | + | changed = true | |
| 268 | 256 | } | |
| 269 | - | if errors.Is(err, domain.ErrNotFound) { | |
| 270 | - | return domain.Repo{}, nil | |
| 257 | + | if now.After(repo.ActiveAt) { | |
| 258 | + | repo.ActiveAt = now | |
| 259 | + | changed = true | |
| 271 | 260 | } | |
| 272 | - | if err != nil { | |
| 273 | - | return repo, fmt.Errorf("update repository %q: %w", name, err) | |
| 274 | - | } | |
| 275 | - | return repo, nil | |
| 261 | + | return changed, nil | |
| 262 | + | }) | |
| 263 | + | if errors.Is(err, domain.ErrNotFound) { | |
| 264 | + | return domain.Repo{}, nil | |
| 265 | + | } | |
| 266 | + | if err != nil { | |
| 267 | + | return repo, fmt.Errorf("update repository %q: %w", name, err) | |
| 276 | 268 | } | |
| 277 | - | return domain.Repo{}, fmt.Errorf("update pushed_at of %q: %w", name, domain.ErrConflict) | |
| 269 | + | return repo, nil | |
| 278 | 270 | } | |
| 279 | 271 | ||
| 280 | 272 | // fixDefaultBranch points HEAD to a branch that exists when it points to a | |
M internal/usecase/pushes_test.go +11 -16
| @@ -305,8 +305,7 @@ func TestPushesRecord(t *testing.T) { | |||
| 305 | 305 | f := newPushFixture(t) | |
| 306 | 306 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 307 | 307 | repo := domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: now.Add(-time.Hour)} | |
| 308 | - | f.meta.EXPECT().Get(ctx, n).Return(repo, nil) | |
| 309 | - | f.meta.EXPECT().Update(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: repo.CreatedAt, PushedAt: now}).Return(nil) | |
| 308 | + | expectRepoMutation(t, f.meta, repo, domain.Repo{Name: n, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, PushedAt: now, ActiveAt: now}).Once() | |
| 310 | 309 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | |
| 311 | 310 | f.bus.EXPECT().Publish(ctx, domain.PushEvent{Repo: n, Updates: ups, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}).Return() | |
| 312 | 311 | ||
| @@ -320,22 +319,18 @@ func TestPushesRecord(t *testing.T) { | |||
| 320 | 319 | fresh := mocks.NewRepoFreshener(t) | |
| 321 | 320 | fresh.EXPECT().Refresh(ctx, n).Return().Once() | |
| 322 | 321 | f.pushes.WithFreshener(fresh) | |
| 323 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n}, nil) | |
| 324 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil) | |
| 322 | + | expectPushMutation(t, f.meta, domain.Repo{Name: n}, nil) | |
| 325 | 323 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | |
| 326 | 324 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| 327 | 325 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | |
| 328 | 326 | }) | |
| 329 | 327 | ||
| 330 | - | t.Run("pushed_at retries after a conflict and never goes backwards", func(t *testing.T) { | |
| 328 | + | t.Run("pushed_at preserves a later push", func(t *testing.T) { | |
| 331 | 329 | t.Parallel() | |
| 332 | 330 | f := newPushFixture(t) | |
| 333 | 331 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 334 | - | repo := domain.Repo{Name: n} | |
| 335 | - | f.meta.EXPECT().Get(ctx, n).Return(repo, nil).Once() | |
| 336 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(domain.ErrConflict).Once() | |
| 337 | - | later := domain.Repo{Name: n, PushedAt: now.Add(time.Second)} | |
| 338 | - | f.meta.EXPECT().Get(ctx, n).Return(later, nil).Once() // a later push recorded meanwhile | |
| 332 | + | later := domain.Repo{Name: n, PushedAt: now.Add(time.Second), ActiveAt: now.Add(time.Second)} | |
| 333 | + | expectPushMutation(t, f.meta, later, nil) // a later push recorded meanwhile | |
| 339 | 334 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | |
| 340 | 335 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| 341 | 336 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | |
| @@ -345,7 +340,7 @@ func TestPushesRecord(t *testing.T) { | |||
| 345 | 340 | t.Parallel() | |
| 346 | 341 | f := newPushFixture(t) | |
| 347 | 342 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 348 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, domain.ErrNotFound) | |
| 343 | + | expectPushMutation(t, f.meta, domain.Repo{}, domain.ErrNotFound) | |
| 349 | 344 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | |
| 350 | 345 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| 351 | 346 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | |
| @@ -355,7 +350,7 @@ func TestPushesRecord(t *testing.T) { | |||
| 355 | 350 | t.Parallel() | |
| 356 | 351 | f := newPushFixture(t) | |
| 357 | 352 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 358 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, domain.ErrNotFound) | |
| 353 | + | expectPushMutation(t, f.meta, domain.Repo{}, domain.ErrNotFound) | |
| 359 | 354 | f.git.EXPECT().DefaultBranch(ctx, n).Return("", false, domain.ErrNotFound) | |
| 360 | 355 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| 361 | 356 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | |
| @@ -365,7 +360,7 @@ func TestPushesRecord(t *testing.T) { | |||
| 365 | 360 | t.Parallel() | |
| 366 | 361 | f := newPushFixture(t) | |
| 367 | 362 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 368 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, domain.ErrNotFound) | |
| 363 | + | expectPushMutation(t, f.meta, domain.Repo{}, domain.ErrNotFound) | |
| 369 | 364 | f.git.EXPECT().DefaultBranch(ctx, n).Return("trunk", false, nil) | |
| 370 | 365 | f.git.EXPECT().SetDefaultBranch(ctx, n, "main").Return(domain.ErrNotFound) | |
| 371 | 366 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| @@ -377,7 +372,7 @@ func TestPushesRecord(t *testing.T) { | |||
| 377 | 372 | f := newPushFixture(t) | |
| 378 | 373 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 379 | 374 | boom := errors.New("boom") | |
| 380 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, boom) | |
| 375 | + | expectPushMutation(t, f.meta, domain.Repo{}, boom) | |
| 381 | 376 | f.git.EXPECT().DefaultBranch(ctx, n).Return("", false, boom) | |
| 382 | 377 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| 383 | 378 | err := f.pushes.Record(ctx, p, n, ups) | |
| @@ -414,7 +409,7 @@ func TestPushesDefaultBranchRule(t *testing.T) { | |||
| 414 | 409 | t.Parallel() | |
| 415 | 410 | f := newPushFixture(t) | |
| 416 | 411 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 417 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now}, nil) | |
| 412 | + | expectPushMutation(t, f.meta, domain.Repo{Name: n, PushedAt: now, ActiveAt: now}, nil) | |
| 418 | 413 | f.git.EXPECT().DefaultBranch(ctx, n).Return(tt.head, tt.exists, nil) | |
| 419 | 414 | if tt.want != "" { | |
| 420 | 415 | f.git.EXPECT().SetDefaultBranch(ctx, n, tt.want).Return(nil) | |
| @@ -429,7 +424,7 @@ func TestPushesDefaultBranchRule(t *testing.T) { | |||
| 429 | 424 | f := newPushFixture(t) | |
| 430 | 425 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | |
| 431 | 426 | boom := errors.New("boom") | |
| 432 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n, PushedAt: now}, nil) | |
| 427 | + | expectPushMutation(t, f.meta, domain.Repo{Name: n, PushedAt: now, ActiveAt: now}, nil) | |
| 433 | 428 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", false, nil) | |
| 434 | 429 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom) | |
| 435 | 430 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
A internal/usecase/repo_mutation_test.go +34 -0
| @@ -0,0 +1,34 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package usecase_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/mock" | |
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite/internal/domain" | |
| 13 | + | "bnema.dev/ermite/internal/ports/mocks" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func expectRepoMutation(t *testing.T, meta *mocks.RepoMetaStore, repo domain.Repo, want any) *mocks.RepoMetaStore_Mutate_Call { | |
| 17 | + | t.Helper() | |
| 18 | + | return meta.EXPECT().Mutate(mock.Anything, repo.Name, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.RepoName, fn func(*domain.Repo) (bool, error)) (domain.Repo, error) { | |
| 19 | + | result := repo | |
| 20 | + | _, err := fn(&result) | |
| 21 | + | require.NoError(t, err) | |
| 22 | + | _, differences := (mock.Arguments{want}).Diff([]any{result}) | |
| 23 | + | require.Zero(t, differences, "unexpected mutated repository: %+v", result) | |
| 24 | + | return result, err | |
| 25 | + | }) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func expectPushMutation(t *testing.T, meta *mocks.RepoMetaStore, repo domain.Repo, err error) *mocks.RepoMetaStore_Mutate_Call { | |
| 29 | + | t.Helper() | |
| 30 | + | if err != nil { | |
| 31 | + | return meta.EXPECT().Mutate(mock.Anything, mock.Anything, mock.Anything).Return(repo, err) | |
| 32 | + | } | |
| 33 | + | return expectRepoMutation(t, meta, repo, mock.Anything) | |
| 34 | + | } | |
M internal/usecase/repos_write.go +6 -14
| @@ -155,11 +155,7 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||
| 155 | 155 | // updateMeta applies the metadata fields, retrying when a write (a push | |
| 156 | 156 | // recording pushed_at) gets in between. | |
| 157 | 157 | func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params ports.UpdateRepoParams) (domain.Repo, error) { | |
| 158 | - | for range pushRetries { | |
| 159 | - | repo, err := r.meta.Get(ctx, name) | |
| 160 | - | if err != nil { | |
| 161 | - | return domain.Repo{}, err | |
| 162 | - | } | |
| 158 | + | repo, err := r.meta.Mutate(ctx, name, func(repo *domain.Repo) (bool, error) { | |
| 163 | 159 | if params.Description != nil { | |
| 164 | 160 | repo.Description = *params.Description | |
| 165 | 161 | } | |
| @@ -181,16 +177,12 @@ func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params por | |||
| 181 | 177 | if params.Issues != nil { | |
| 182 | 178 | repo.Settings.Issues = *params.Issues | |
| 183 | 179 | } | |
| 184 | - | err = r.meta.Update(ctx, repo) | |
| 185 | - | if errors.Is(err, domain.ErrConflict) { | |
| 186 | - | continue | |
| 187 | - | } | |
| 188 | - | if err != nil { | |
| 189 | - | return domain.Repo{}, fmt.Errorf("update repository %q: %w", name, err) | |
| 190 | - | } | |
| 191 | - | return repo, nil | |
| 180 | + | return true, nil | |
| 181 | + | }) | |
| 182 | + | if err != nil { | |
| 183 | + | return domain.Repo{}, fmt.Errorf("update repository %q: %w", name, err) | |
| 192 | 184 | } | |
| 193 | - | return domain.Repo{}, fmt.Errorf("update repository %q: %w", name, domain.ErrConflict) | |
| 185 | + | return repo, nil | |
| 194 | 186 | } | |
| 195 | 187 | ||
| 196 | 188 | // Delete moves the repository directory to the trash, removes its metadata | |
M internal/usecase/repos_write_test.go +20 -21
| @@ -30,9 +30,9 @@ func TestUpdate(t *testing.T) { | |||
| 30 | 30 | t.Run("description and visibility are stored and RepoUpdated is published", func(t *testing.T) { | |
| 31 | 31 | t.Parallel() | |
| 32 | 32 | f := newFixture(t) | |
| 33 | - | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Twice() | |
| 33 | + | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | |
| 34 | 34 | want := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, Description: "new", Settings: domain.DefaultRepoSettings()} | |
| 35 | - | f.meta.EXPECT().Update(ctx, want).Return(nil) | |
| 35 | + | expectRepoMutation(t, f.meta, priv, want) | |
| 36 | 36 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | |
| 37 | 37 | Repo: n, At: now, Visibility: domain.VisibilityPublic, Previous: domain.VisibilityPrivate, | |
| 38 | 38 | }).Return().Once() | |
| @@ -47,11 +47,11 @@ func TestUpdate(t *testing.T) { | |||
| 47 | 47 | t.Run("topics are normalized and stored, and RepoUpdated is published", func(t *testing.T) { | |
| 48 | 48 | t.Parallel() | |
| 49 | 49 | f := newFixture(t) | |
| 50 | - | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Twice() | |
| 50 | + | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | |
| 51 | 51 | want := priv | |
| 52 | 52 | want.Topics = []string{"cli", "go"} | |
| 53 | 53 | want.Settings = domain.DefaultRepoSettings() | |
| 54 | - | f.meta.EXPECT().Update(ctx, want).Return(nil) | |
| 54 | + | expectRepoMutation(t, f.meta, priv, want) | |
| 55 | 55 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | |
| 56 | 56 | Repo: n, At: now, Visibility: domain.VisibilityPrivate, Previous: domain.VisibilityPrivate, | |
| 57 | 57 | }).Return().Once() | |
| @@ -66,10 +66,10 @@ func TestUpdate(t *testing.T) { | |||
| 66 | 66 | f := newFixture(t) | |
| 67 | 67 | withTopics := priv | |
| 68 | 68 | withTopics.Topics = []string{"go"} | |
| 69 | - | f.meta.EXPECT().Get(ctx, n).Return(withTopics, nil).Twice() | |
| 69 | + | f.meta.EXPECT().Get(ctx, n).Return(withTopics, nil).Once() | |
| 70 | 70 | want := priv | |
| 71 | 71 | want.Settings = domain.DefaultRepoSettings() | |
| 72 | - | f.meta.EXPECT().Update(ctx, want).Return(nil) | |
| 72 | + | expectRepoMutation(t, f.meta, withTopics, want) | |
| 73 | 73 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | |
| 74 | 74 | ||
| 75 | 75 | got, err := f.repos.Update(ctx, admin, n, ports.UpdateRepoParams{Topics: &[]string{}}) | |
| @@ -77,15 +77,12 @@ func TestUpdate(t *testing.T) { | |||
| 77 | 77 | assert.Nil(t, got.Topics) | |
| 78 | 78 | }) | |
| 79 | 79 | ||
| 80 | - | t.Run("a lost race on the metadata is retried", func(t *testing.T) { | |
| 81 | - | t.Parallel() | |
| 80 | + | t.Run("exhausted mutation conflicts are returned", func(t *testing.T) { | |
| 82 | 81 | f := newFixture(t) | |
| 83 | 82 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil) | |
| 84 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(domain.ErrConflict).Once() | |
| 85 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil).Once() | |
| 86 | - | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | |
| 83 | + | f.meta.EXPECT().Mutate(ctx, n, mock.Anything).Return(domain.Repo{}, domain.ErrConflict).Once() | |
| 87 | 84 | _, err := f.repos.Update(ctx, admin, n, ports.UpdateRepoParams{Description: ptr("x")}) | |
| 88 | - | require.NoError(t, err) | |
| 85 | + | require.ErrorIs(t, err, domain.ErrConflict) | |
| 89 | 86 | }) | |
| 90 | 87 | ||
| 91 | 88 | t.Run("the default branch is checked, then HEAD moves", func(t *testing.T) { | |
| @@ -106,9 +103,11 @@ func TestUpdate(t *testing.T) { | |||
| 106 | 103 | var order []string | |
| 107 | 104 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil) | |
| 108 | 105 | f.git.EXPECT().HasBranch(ctx, n, "dev").Return(true, nil) | |
| 109 | - | f.meta.EXPECT().Update(ctx, mock.Anything).RunAndReturn(func(context.Context, domain.Repo) error { | |
| 106 | + | f.meta.EXPECT().Mutate(ctx, n, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.RepoName, fn func(*domain.Repo) (bool, error)) (domain.Repo, error) { | |
| 110 | 107 | order = append(order, "meta") | |
| 111 | - | return nil | |
| 108 | + | repo := priv | |
| 109 | + | _, err := fn(&repo) | |
| 110 | + | return repo, err | |
| 112 | 111 | }) | |
| 113 | 112 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").RunAndReturn(func(context.Context, domain.RepoName, string) error { | |
| 114 | 113 | order = append(order, "head") | |
| @@ -127,7 +126,7 @@ func TestUpdate(t *testing.T) { | |||
| 127 | 126 | pub := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, Description: "old"} | |
| 128 | 127 | f.meta.EXPECT().Get(ctx, n).Return(pub, nil) | |
| 129 | 128 | f.git.EXPECT().HasBranch(ctx, n, "dev").Return(true, nil) | |
| 130 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil) | |
| 129 | + | expectRepoMutation(t, f.meta, pub, mock.Anything) | |
| 131 | 130 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom) | |
| 132 | 131 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | |
| 133 | 132 | Repo: n, At: now, Visibility: domain.VisibilityPrivate, Previous: domain.VisibilityPublic, | |
| @@ -250,10 +249,10 @@ func TestUpdateIssuesMode(t *testing.T) { | |||
| 250 | 249 | t.Run("public is stored and published", func(t *testing.T) { | |
| 251 | 250 | t.Parallel() | |
| 252 | 251 | f := newFixture(t) | |
| 253 | - | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Twice() | |
| 254 | - | f.meta.EXPECT().Update(ctx, mock.MatchedBy(func(r domain.Repo) bool { | |
| 252 | + | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | |
| 253 | + | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { | |
| 255 | 254 | return r.Settings.Issues == domain.IssuesPublic | |
| 256 | - | })).Return(nil) | |
| 255 | + | })) | |
| 257 | 256 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | |
| 258 | 257 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesPublic)}) | |
| 259 | 258 | require.NoError(t, err) | |
| @@ -271,10 +270,10 @@ func TestUpdateIssuesMode(t *testing.T) { | |||
| 271 | 270 | t.Run("owner is stored and published", func(t *testing.T) { | |
| 272 | 271 | t.Parallel() | |
| 273 | 272 | f := newFixture(t) | |
| 274 | - | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Twice() | |
| 275 | - | f.meta.EXPECT().Update(ctx, mock.MatchedBy(func(r domain.Repo) bool { | |
| 273 | + | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | |
| 274 | + | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { | |
| 276 | 275 | return r.Settings.Issues == domain.IssuesOwner && r.Settings.FailingChecksBlock | |
| 277 | - | })).Return(nil) | |
| 276 | + | })) | |
| 278 | 277 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | |
| 279 | 278 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesOwner)}) | |
| 280 | 279 | require.NoError(t, err) | |
M proto/ermite/storage/repo.proto +2 -0
| @@ -32,6 +32,8 @@ message Repo { | |||
| 32 | 32 | RepoSettings settings = 6; | |
| 33 | 33 | // Normalized and sorted; empty on repositories stored before topics. | |
| 34 | 34 | repeated string topics = 7; | |
| 35 | + | // Latest push, merge request or issue activity; unset until recorded. | |
| 36 | + | google.protobuf.Timestamp active_at = 8; | |
| 35 | 37 | } | |
| 36 | 38 | ||
| 37 | 39 | // SchemaVersion is stored under the key "meta/schema". | |