Merge 9ca7c686a53fa2b6c5d93929c63e54458ac77df7 into d8e7d5b03a01f5f16f8d49f12a7e132355b40f06
91 files · +2414 -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 | |
| @@ -258,13 +258,15 @@ func TestRepoSettingsKeepTheIssuesMode(t *testing.T) { | |||||
| 258 | assert.Equal(t, domain.IssuesOff, got.Settings.Issues) | 258 | assert.Equal(t, domain.IssuesOff, got.Settings.Issues) | ||
| 259 | for _, m := range []domain.IssuesMode{domain.IssuesOwner, domain.IssuesPublic, domain.IssuesOff} { | 259 | for _, m := range []domain.IssuesMode{domain.IssuesOwner, domain.IssuesPublic, domain.IssuesOff} { | ||
| 260 | got.Settings.Issues = m | 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 | again, err := s.Get(ctx, n) | 263 | again, err := s.Get(ctx, n) | ||
| 263 | require.NoError(t, err) | 264 | require.NoError(t, err) | ||
| 264 | assert.Equal(t, m, again.Settings.Issues) | 265 | assert.Equal(t, m, again.Settings.Issues) | ||
| 265 | } | 266 | } | ||
| 266 | got.Settings.Issues = "everyone" | 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 | // TestSchema4UpgradesTheTokensWithAllScopes: the tokens that held every | 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 | |
| @@ -442,13 +442,15 @@ func TestRepoSettings(t *testing.T) { | |||||
| 442 | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | 442 | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | ||
| 443 | 443 | ||||
| 444 | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false, Issues: domain.IssuesOwner} | 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 | again, err := s.Get(ctx, n) | 447 | again, err := s.Get(ctx, n) | ||
| 447 | require.NoError(t, err) | 448 | require.NoError(t, err) | ||
| 448 | assert.Equal(t, got.Settings, again.Settings) | 449 | assert.Equal(t, got.Settings, again.Settings) | ||
| 449 | 450 | ||||
| 450 | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{"rebase"}} | 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 | // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before | 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 | } | |
| @@ -64,31 +64,51 @@ func (s *Store) List(ctx context.Context) ([]domain.Repo, error) { | |||||
| 64 | return repos, nil | 64 | return repos, nil | ||
| 65 | } | 65 | } | ||
| 66 | 66 | ||||
| 67 | - | // Update replaces the stored repository. It returns domain.ErrNotFound if | 67 | + | const repoMutationTries = 8 |
| 68 | - | // there is none and domain.ErrConflict if another write changed it since it | 68 | + | |
| 69 | - | // was read. | 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 | 71 | + | //hexcheck:ignore suspicious-business-logic-in-adapter optimistic concurrency is a storage concern |
| 72 | - | func (s *Store) Update(ctx context.Context, repo domain.Repo) error { | 72 | + | func (s *Store) Mutate(ctx context.Context, name domain.RepoName, fn func(*domain.Repo) (bool, error)) (domain.Repo, error) { |
| 73 | - | val, err := encodeRepo(repo) | 73 | + | key := repoKey(name) |
| 74 | - | if err != nil { | 74 | + | for range repoMutationTries { |
| 75 | - | return err | 75 | + | if err := ctx.Err(); err != nil { |
| 76 | - | } | 76 | + | return domain.Repo{}, err |
| 77 | - | key := repoKey(repo.Name) | 77 | + | } |
| 78 | - | item, ok := s.db.Get(ctx, key) | 78 | + | item, ok := s.db.Get(ctx, key) |
| 79 | - | if !ok { | 79 | + | if !ok { |
| 80 | - | return domain.ErrNotFound | 80 | + | return domain.Repo{}, 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 | |||
| 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 | // guardRepoKey makes the batch depend on the repository: it requires the | 114 | // guardRepoKey makes the batch depend on the repository: it requires the | ||
| @@ -208,6 +228,7 @@ func encodeRepo(r domain.Repo) ([]byte, error) { | |||||
| 208 | Visibility: vis, | 228 | Visibility: vis, | ||
| 209 | CreatedAt: encodeTime(r.CreatedAt), | 229 | CreatedAt: encodeTime(r.CreatedAt), | ||
| 210 | PushedAt: encodeTime(r.PushedAt), | 230 | PushedAt: encodeTime(r.PushedAt), | ||
| 231 | + | ActiveAt: encodeTime(r.ActiveAt), | |||
| 211 | Settings: pbSettings, | 232 | Settings: pbSettings, | ||
| 212 | } | 233 | } | ||
| 213 | return mustMarshal(pb), nil | 234 | return mustMarshal(pb), nil | ||
| @@ -229,6 +250,7 @@ func decodeRepo(val []byte) (domain.Repo, error) { | |||||
| 229 | Visibility: decodeVisibility(pb.GetVisibility()), | 250 | Visibility: decodeVisibility(pb.GetVisibility()), | ||
| 230 | CreatedAt: decodeTime(pb.GetCreatedAt()), | 251 | CreatedAt: decodeTime(pb.GetCreatedAt()), | ||
| 231 | PushedAt: decodeTime(pb.GetPushedAt()), | 252 | PushedAt: decodeTime(pb.GetPushedAt()), | ||
| 253 | + | ActiveAt: decodeTime(pb.GetActiveAt()), | |||
| 232 | Settings: decodeSettings(pb.GetSettings()), | 254 | Settings: decodeSettings(pb.GetSettings()), | ||
| 233 | }, nil | 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 | + | } | |
| @@ -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) | |
| @@ -43,7 +43,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||||
| 43 | s := openStore(t, t.TempDir()) | 43 | s := openStore(t, t.TempDir()) | ||
| 44 | 44 | ||||
| 45 | b := domain.Repo{Name: name(t, "b"), Description: "second", Topics: []string{"cli", "go"}, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()} | 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 | require.NoError(t, s.Create(ctx, b)) | 47 | require.NoError(t, s.Create(ctx, b)) | ||
| 48 | require.NoError(t, s.Create(ctx, a)) | 48 | require.NoError(t, s.Create(ctx, a)) | ||
| 49 | 49 | ||||
| @@ -54,6 +54,7 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||||
| 54 | require.NoError(t, err) | 54 | require.NoError(t, err) | ||
| 55 | assert.Equal(t, a, got) | 55 | assert.Equal(t, a, got) | ||
| 56 | assert.True(t, got.PushedAt.Equal(a.PushedAt)) | 56 | assert.True(t, got.PushedAt.Equal(a.PushedAt)) | ||
| 57 | + | assert.True(t, got.ActiveAt.Equal(a.ActiveAt)) | |||
| 57 | 58 | ||||
| 58 | list, err := s.List(ctx) | 59 | list, err := s.List(ctx) | ||
| 59 | require.NoError(t, err) | 60 | require.NoError(t, err) | ||
| @@ -65,13 +66,15 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||||
| 65 | b.Description = "changed" | 66 | b.Description = "changed" | ||
| 66 | b.Topics = []string{"forge"} | 67 | b.Topics = []string{"forge"} | ||
| 67 | b.Visibility = domain.VisibilityPrivate | 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 | got, err = s.Get(ctx, b.Name) | 71 | got, err = s.Get(ctx, b.Name) | ||
| 70 | require.NoError(t, err) | 72 | require.NoError(t, err) | ||
| 71 | assert.Equal(t, b, got) | 73 | assert.Equal(t, b, got) | ||
| 72 | 74 | ||||
| 73 | b.Topics = nil | 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 | got, err = s.Get(ctx, b.Name) | 78 | got, err = s.Get(ctx, b.Name) | ||
| 76 | require.NoError(t, err) | 79 | require.NoError(t, err) | ||
| 77 | assert.Nil(t, got.Topics, "cleared topics read back as nil") | 80 | assert.Nil(t, got.Topics, "cleared topics read back as nil") | ||
| @@ -80,7 +83,8 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||||
| 80 | _, err = s.Get(ctx, b.Name) | 83 | _, err = s.Get(ctx, b.Name) | ||
| 81 | assert.ErrorIs(t, err, domain.ErrNotFound) | 84 | assert.ErrorIs(t, err, domain.ErrNotFound) | ||
| 82 | require.NoError(t, s.Delete(ctx, b.Name), "deleting a missing repository is not an error") | 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 | _, err = s.Get(ctx, name(t, "never")) | 89 | _, err = s.Get(ctx, name(t, "never")) | ||
| 86 | assert.ErrorIs(t, err, domain.ErrNotFound) | 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 != "" { | |
| @@ -37,7 +37,7 @@ templ CloneBlock(c Clone) { | |||||
| 37 | // right its figures and CI state over its newest commits. Its id is the | 37 | // right its figures and CI state over its newest commits. Its id is the | ||
| 38 | // target of SSE swaps; the name link comes first, `enter` follows it. | 38 | // target of SSE swaps; the name link comes first, `enter` follows it. | ||
| 39 | templ HomeRowView(r HomeRow) { | 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 | <div class="repo-main"> | 41 | <div class="repo-main"> | ||
| 42 | <h2 class="repo-name"> | 42 | <h2 class="repo-name"> | ||
| 43 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | 43 | <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a> | ||
| @@ -45,7 +45,7 @@ templ HomeRowView(r HomeRow) { | |||||
| 45 | <span class="pill">private</span> | 45 | <span class="pill">private</span> | ||
| 46 | } | 46 | } | ||
| 47 | if r.Fresh { | 47 | if r.Fresh { | ||
| 48 | - | <span class="pill new">new push</span> | 48 | + | <span class="pill new">new activity</span> |
| 49 | } | 49 | } | ||
| 50 | </h2> | 50 | </h2> | ||
| 51 | if r.Desc != "" { | 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 | } | |
| @@ -72,9 +72,7 @@ templ Commit(l Layout, p CommitPage) { | |||||
| 72 | <div class="diff-tools" role="group" aria-label="Diff view"> | 72 | <div class="diff-tools" role="group" aria-label="Diff view"> | ||
| 73 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | 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 | <span class="grow"></span> | 74 | <span class="grow"></span> | ||
| 75 | - | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | 75 | + | @diffViewMenu(p.DiffView) |
| 76 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |||
| 77 | - | @collapseButton() | |||
| 78 | </div> | 76 | </div> | ||
| 79 | if p.Invisible { | 77 | if p.Invisible { | ||
| 80 | <p class="notice"> | 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 | // viewLink is a button of the diff tools. It is a link, so that the split | 133 | // viewLink is a button of the diff tools. It is a link, so that the split | ||
| 116 | // layout works without JavaScript; with it, and when both layouts are in the | 134 | // layout works without JavaScript; with it, and when both layouts are in the | ||
| 117 | // page, a click only flips the layout. | 135 | // page, a click only flips the layout. | ||
| @@ -203,7 +221,7 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 203 | <details | 221 | <details | ||
| 204 | class={ "dfile", "lnw-" + itoa(f.Digits) } | 222 | class={ "dfile", "lnw-" + itoa(f.Digits) } | ||
| 205 | id={ f.ID } | 223 | id={ f.ID } | ||
| 206 | - | data-file | 224 | + | data-file={ f.Key } |
| 207 | if f.Open { | 225 | if f.Open { | ||
| 208 | open | 226 | open | ||
| 209 | } | 227 | } | ||
M internal/adapters/web/diff_templ.go +523 -437 generated file: collapsed
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 | |
| @@ -50,6 +50,8 @@ import ( | |||||
| 50 | // | 50 | // | ||
| 51 | // home-list data: <li id="repo-<name>">…</li>, or <li id="repo-<name>" data-gone></li> | 51 | // home-list data: <li id="repo-<name>">…</li>, or <li id="repo-<name>" data-gone></li> | ||
| 52 | // for a repository that is deleted or no longer visible | 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 | // repo-last-push data: <dd id="last-push-<name>">…</dd> | 55 | // repo-last-push data: <dd id="last-push-<name>">…</dd> | ||
| 54 | // refs-list data: <tbody id="ref-rows">…</tbody>, and again with | 56 | // refs-list data: <tbody id="ref-rows">…</tbody>, and again with | ||
| 55 | // <dl id="refs-kv">…</dl>, the facts of the rail | 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 | |
| @@ -61,6 +61,14 @@ func grouped(n int) string { | |||||
| 61 | return string(b) | 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 | func plural(n int, one, many string) string { | 72 | func plural(n int, one, many string) string { | ||
| 65 | if n == 1 { | 73 | if n == 1 { | ||
| 66 | return "1 " + one | 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: | |
| @@ -112,6 +112,24 @@ func (l *liveStream) home(ctx context.Context, we ports.WatchEvent) ([]fragment, | |||||
| 112 | name = ev.Repo.String() | 112 | name = ev.Repo.String() | ||
| 113 | case domain.PushEvent: | 113 | case domain.PushEvent: | ||
| 114 | name, fresh = ev.Repo.String(), true | 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 | case domain.RunUpdated: | 133 | case domain.RunUpdated: | ||
| 116 | name = ev.Run.Repo.String() // the CI state of the row | 134 | name = ev.Run.Repo.String() // the CI state of the row | ||
| 117 | case domain.JobUpdated: | 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 | + | } | |
| @@ -56,12 +56,12 @@ func (s *Server) listRepos(ctx context.Context) ([]ports.RepoView, error) { | |||||
| 56 | // homeSorts are the orders the home list offers, in order. The query also | 56 | // homeSorts are the orders the home list offers, in order. The query also | ||
| 57 | // accepts "desc" and "branch", which the page does not link to. | 57 | // accepts "desc" and "branch", which the page does not link to. | ||
| 58 | var homeSorts = []struct{ key, label string }{ | 58 | var homeSorts = []struct{ key, label string }{ | ||
| 59 | - | {"push", "last push"}, | 59 | + | {"activity", "last activity"}, |
| 60 | {"name", "name"}, | 60 | {"name", "name"}, | ||
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | // homeKeys are the keys of ?sort= that sortRepos knows. | 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 | // sortRepos orders repos by the column key; ties keep the order by name. | 66 | // sortRepos orders repos by the column key; ties keep the order by name. | ||
| 67 | func sortRepos(repos []ports.RepoView, key string, desc bool) { | 67 | func sortRepos(repos []ports.RepoView, key string, desc bool) { | ||
| @@ -71,8 +71,8 @@ func sortRepos(repos []ports.RepoView, key string, desc bool) { | |||||
| 71 | return strings.ToLower(a.Description) < strings.ToLower(b.Description) | 71 | return strings.ToLower(a.Description) < strings.ToLower(b.Description) | ||
| 72 | case "branch": | 72 | case "branch": | ||
| 73 | return a.DefaultBranch < b.DefaultBranch | 73 | return a.DefaultBranch < b.DefaultBranch | ||
| 74 | - | case "push": | 74 | + | case "activity": |
| 75 | - | return a.PushedAt.Before(b.PushedAt) | 75 | + | return repoActiveAt(a).Before(repoActiveAt(b)) |
| 76 | } | 76 | } | ||
| 77 | return a.Name.String() < b.Name.String() | 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 | // by default. | 88 | // by default. | ||
| 89 | func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) { | 89 | func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) { | ||
| 90 | repos, err := s.listRepos(ctx) | 90 | repos, err := s.listRepos(ctx) | ||
| 91 | if err != nil { | 91 | if err != nil { | ||
| 92 | return nil, 0, err | 92 | return nil, 0, err | ||
| 93 | } | 93 | } | ||
| 94 | - | key, desc := "push", true | 94 | + | key, desc := "activity", true |
| 95 | for _, k := range homeKeys { | 95 | for _, k := range homeKeys { | ||
| 96 | if r.URL.Query().Get("sort") == k { | 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 | if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" { | 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 | sortRepos(repos, key, desc) | 103 | sortRepos(repos, key, desc) | ||
| 104 | 104 | ||||
| 105 | now := s.cfg.Now() | 105 | now := s.cfg.Now() | ||
| 106 | - | p := HomePage{} | 106 | + | p := HomePage{Sort: key, Desc: desc} |
| 107 | for _, c := range homeSorts { | 107 | for _, c := range homeSorts { | ||
| 108 | sl := SortLink{Label: c.label} | 108 | sl := SortLink{Label: c.label} | ||
| 109 | next := "asc" | 109 | next := "asc" | ||
| 110 | - | if c.key == "push" { | 110 | + | if c.key == "activity" { |
| 111 | next = "desc" | 111 | next = "desc" | ||
| 112 | } | 112 | } | ||
| 113 | if c.key == key { | 113 | if c.key == key { | ||
| @@ -121,11 +121,10 @@ func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, in | |||||
| 121 | p.Sorts = append(p.Sorts, sl) | 121 | p.Sorts = append(p.Sorts, sl) | ||
| 122 | } | 122 | } | ||
| 123 | p.Rows = s.homeRows(ctx, repos, now) | 123 | p.Rows = s.homeRows(ctx, repos, now) | ||
| 124 | + | l := s.base() | |||
| 124 | if n, show := s.inboxCount(ctx); show { | 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 | l.Title = s.instance | 128 | l.Title = s.instance | ||
| 130 | l.Live, l.LiveURL = true, liveURL("home", "") | 129 | l.Live, l.LiveURL = true, liveURL("home", "") | ||
| 131 | l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...) | 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 | Desc: r.Description, Topics: r.Topics, Cursor: cursor, | 267 | Desc: r.Description, Topics: r.Topics, Cursor: cursor, | ||
| 269 | Private: signed && r.Visibility == domain.VisibilityPrivate, | 268 | Private: signed && r.Visibility == domain.VisibilityPrivate, | ||
| 270 | } | 269 | } | ||
| 271 | - | if !r.PushedAt.IsZero() { | 270 | + | if at := repoActiveAt(r); !at.IsZero() { |
| 272 | - | row.Active, row.ActiveAt = ago(now, r.PushedAt), rfc3339(r.PushedAt) | 271 | + | row.Active, row.ActiveAt = ago(now, at), rfc3339(at) |
| 273 | } | 272 | } | ||
| 274 | return row | 273 | return row | ||
| 275 | } | 274 | } | ||
| @@ -285,3 +284,14 @@ func repoCmds(repos []ports.RepoView, except string) []CmdItem { | |||||
| 285 | } | 284 | } | ||
| 286 | return out | 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 +49 -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,51 @@ 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 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev}) | |
| 381 | + | require.NoError(t, err) | |
| 382 | + | require.Len(t, got, 1) | |
| 383 | + | assert.Equal(t, "home-list", got[0].event) | |
| 384 | + | assert.Contains(t, got[0].html, `id="repo-proj"`) | |
| 385 | + | assert.Contains(t, got[0].html, `data-activity`) | |
| 386 | + | assert.Contains(t, got[0].html, "new activity") | |
| 387 | + | }) | |
| 388 | + | } | |
| 389 | + | for _, ev := range []domain.Event{domain.IssueSubmitted{Repo: repo.Name}, domain.IssuePendingRemoved{Repo: repo.Name}} { | |
| 390 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev}) | |
| 391 | + | require.NoError(t, err) | |
| 392 | + | assert.Empty(t, got) | |
| 393 | + | } | |
| 394 | + | } | |
| @@ -252,7 +252,7 @@ func TestRunUpdatedRefreshesTheHomeRow(t *testing.T) { | |||||
| 252 | assert.Equal(t, "home-list", got[0].event) | 252 | assert.Equal(t, "home-list", got[0].event) | ||
| 253 | assert.Contains(t, got[0].html, `id="repo-proj"`) | 253 | assert.Contains(t, got[0].html, `id="repo-proj"`) | ||
| 254 | assert.Contains(t, got[0].html, "passing") | 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 | // A job that ended refreshes the row too; one in progress does not. | 257 | // A job that ended refreshes the row too; one in progress does not. | ||
| 258 | got, err = (&liveStream{s: srv}).render(t.Context(), ports.WatchEvent{Event: domain.JobUpdated{Job: domain.CIJob{Repo: repo, Phase: domain.CIPhaseRunning}}}) | 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,51 @@ func TestHomeOffersTheLayouts(t *testing.T) { | |||||
| 344 | require.NotEqual(t, -1, store) | 344 | require.NotEqual(t, -1, store) | ||
| 345 | assert.Less(t, store, app, "the store loads before the app script") | 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 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev}) | |||
| 381 | + | require.NoError(t, err) | |||
| 382 | + | require.Len(t, got, 1) | |||
| 383 | + | assert.Equal(t, "home-list", got[0].event) | |||
| 384 | + | assert.Contains(t, got[0].html, `id="repo-proj"`) | |||
| 385 | + | assert.Contains(t, got[0].html, `data-activity`) | |||
| 386 | + | assert.Contains(t, got[0].html, "new activity") | |||
| 387 | + | }) | |||
| 388 | + | } | |||
| 389 | + | for _, ev := range []domain.Event{domain.IssueSubmitted{Repo: repo.Name}, domain.IssuePendingRemoved{Repo: repo.Name}} { | |||
| 390 | + | got, err := (&liveStream{s: srv}).home(t.Context(), ports.WatchEvent{Event: ev}) | |||
| 391 | + | require.NoError(t, err) | |||
| 392 | + | assert.Empty(t, got) | |||
| 393 | + | } | |||
| 394 | + | } | |||
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, "") | |
| @@ -136,32 +136,34 @@ templ issueNewRail() { | |||||
| 136 | // owner, the form to comment and the button that closes or reopens it. | 136 | // owner, the form to comment and the button that closes or reopens it. | ||
| 137 | templ IssueShow(l Layout, p IssuePage) { | 137 | templ IssueShow(l Layout, p IssuePage) { | ||
| 138 | @Shell(l, issueRail(p)) { | 138 | @Shell(l, issueRail(p)) { | ||
| 139 | - | <header class="show mr-h"> | 139 | + | <section class="mr-card"> |
| 140 | - | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | 140 | + | <header class="show mr-h"> |
| 141 | - | <dl class="kv show-kv"> | 141 | + | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> |
| 142 | - | <dt>state</dt> | 142 | + | <dl class="kv show-kv"> |
| 143 | - | <dd class="mr-state" data-state={ p.State }>{ p.State }</dd> | 143 | + | <dt>state</dt> |
| 144 | - | <dt>opened</dt> | 144 | + | <dd class="mr-state" data-state={ p.State }>{ p.State }</dd> |
| 145 | - | <dd> | 145 | + | <dt>opened</dt> |
| 146 | - | { p.Opened.Name } | 146 | + | <dd> |
| 147 | - | if p.Opened.WhenAt != "" { | 147 | + | { p.Opened.Name } |
| 148 | - | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | 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> | 156 | + | </dl> |
| 151 | - | if p.UpdatedAt != "" { | 157 | + | </header> |
| 152 | - | <dt>updated</dt> | 158 | + | if p.Desc != "" { |
| 153 | - | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | 159 | + | <article class="mr-desc prose" aria-label="description" hx-ignore> |
| 154 | - | } | 160 | + | @templ.Raw(string(p.Desc)) |
| 155 | - | </dl> | 161 | + | </article> |
| 156 | - | </header> | 162 | + | } |
| 163 | + | </section> | |||
| 157 | if p.Live { | 164 | if p.Live { | ||
| 158 | @LiveSlot("mr-notice") | 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 | <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2> | 167 | <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(len(p.Comments)) }</span></h2> | ||
| 166 | for _, c := range p.Comments { | 168 | for _, c := range p.Comments { | ||
| 167 | @commentView(c, "") | 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 | + | }); | |
| @@ -8,6 +8,7 @@ import { test } from 'node:test'; | |||||
| 8 | import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs'; | 8 | import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs'; | ||
| 9 | 9 | ||||
| 10 | // The few things ermite.js uses that dom.mjs does not have. | 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 | El.prototype.replaceWith = function (n) { | 12 | El.prototype.replaceWith = function (n) { | ||
| 12 | const p = this.parent; | 13 | const p = this.parent; | ||
| 13 | const i = p.nodes.indexOf(this); | 14 | const i = p.nodes.indexOf(this); | ||
| @@ -333,3 +334,27 @@ test('the preference is read from storage when the page loads', async () => { | |||||
| 333 | assert.deepEqual(dom.body.events, []); | 334 | assert.deepEqual(dom.body.events, []); | ||
| 334 | store.set('keys-off', ''); | 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(); | |
| @@ -32,6 +32,7 @@ templ Keys() { | |||||
| 32 | k-split from:body -> ermite.layout('split'); | 32 | k-split from:body -> ermite.layout('split'); | ||
| 33 | k-unified from:body -> ermite.layout('unified'); | 33 | k-unified from:body -> ermite.layout('unified'); | ||
| 34 | k-collapse from:body -> ermite.collapse(); | 34 | k-collapse from:body -> ermite.collapse(); | ||
| 35 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 35 | k-expand from:body -> ermite.expandFile(); | 36 | k-expand from:body -> ermite.expandFile(); | ||
| 36 | k-expandAll from:body -> ermite.expandAll(); | 37 | k-expandAll from:body -> ermite.expandAll(); | ||
| 37 | k-permalink from:body -> ermite.permalink(); | 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 | } | |
| @@ -910,10 +910,15 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||||
| 910 | ln.Selected = true | 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 | if ln := find(on.path, side, on.line); ln != nil { | 918 | if ln := find(on.path, side, on.line); ln != nil { | ||
| 914 | ln.Form = &CommentForm{ | 919 | ln.Form = &CommentForm{ | ||
| 915 | StartLine: optionalLine(on.start), ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side, | 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 | Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true, | 922 | Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true, | ||
| 918 | From: windowStart(fileAt[on.path]), | 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"> | |
| @@ -109,41 +109,43 @@ templ mrListRail(p MRListPage) { | |||||
| 109 | // form, the checks, the settings, the commits and the files. | 109 | // form, the checks, the settings, the commits and the files. | ||
| 110 | templ MergeRequest(l Layout, p MRPage) { | 110 | templ MergeRequest(l Layout, p MRPage) { | ||
| 111 | @Shell(l, mrRail(p)) { | 111 | @Shell(l, mrRail(p)) { | ||
| 112 | - | <header class="show mr-h"> | 112 | + | <section class="mr-card"> |
| 113 | - | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | 113 | + | <header class="show mr-h"> |
| 114 | - | <dl class="kv show-kv"> | 114 | + | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> |
| 115 | - | <dt>state</dt> | 115 | + | <dl class="kv show-kv"> |
| 116 | - | <dd class="mr-state" data-state={ p.State }> | 116 | + | <dt>state</dt> |
| 117 | - | { p.State } | 117 | + | <dd class="mr-state" data-state={ p.State }> |
| 118 | - | if p.MergedShort != "" { | 118 | + | { p.State } |
| 119 | - | { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode } | 119 | + | if p.MergedShort != "" { |
| 120 | - | } | 120 | + | { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode } |
| 121 | - | </dd> | 121 | + | } |
| 122 | - | <dt>branch</dt> | 122 | + | </dd> |
| 123 | - | <dd> | 123 | + | <dt>branch</dt> |
| 124 | - | @branchLink(p.Source, p.SourceHref) | 124 | + | <dd> |
| 125 | - | { " → " } | 125 | + | @branchLink(p.Source, p.SourceHref) |
| 126 | - | @branchLink(p.Target, p.TargetHref) | 126 | + | { " → " } |
| 127 | - | </dd> | 127 | + | @branchLink(p.Target, p.TargetHref) |
| 128 | - | <dt>opened</dt> | 128 | + | </dd> |
| 129 | - | <dd> | 129 | + | <dt>opened</dt> |
| 130 | - | { p.Opened.Name } | 130 | + | <dd> |
| 131 | - | if p.Opened.WhenAt != "" { | 131 | + | { p.Opened.Name } |
| 132 | - | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | 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> | 140 | + | </dl> |
| 135 | - | if p.UpdatedAt != "" { | 141 | + | </header> |
| 136 | - | <dt>updated</dt> | 142 | + | if p.Desc != "" { |
| 137 | - | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | 143 | + | <article class="mr-desc prose" aria-label="description" hx-ignore> |
| 138 | - | } | 144 | + | @templ.Raw(string(p.Desc)) |
| 139 | - | </dl> | 145 | + | </article> |
| 140 | - | </header> | 146 | + | } |
| 147 | + | </section> | |||
| 141 | @LiveSlot("mr-notice") | 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 | if p.Since != nil { | 149 | if p.Since != nil { | ||
| 148 | <nav class="since" aria-label="Diff range"> | 150 | <nav class="since" aria-label="Diff range"> | ||
| 149 | <a | 151 | <a | ||
| @@ -431,9 +433,7 @@ templ mrRail(p MRPage) { | |||||
| 431 | templ mrBar(p MRPage) { | 433 | templ mrBar(p MRPage) { | ||
| 432 | <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge"> | 434 | <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge"> | ||
| 433 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | 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) | 436 | + | @diffViewMenu(p.DiffView) |
| 435 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |||
| 436 | - | @collapseButton() | |||
| 437 | <span class="grow"></span> | 437 | <span class="grow"></span> | ||
| 438 | @MRChecksSum(p.ChecksSum) | 438 | @MRChecksSum(p.ChecksSum) | ||
| 439 | if p.Box != nil { | 439 | if p.Box != nil { | ||
| @@ -562,9 +562,7 @@ templ Compare(l Layout, p ComparePage) { | |||||
| 562 | <div class="diff-tools" role="group" aria-label="Diff view"> | 562 | <div class="diff-tools" role="group" aria-label="Diff view"> | ||
| 563 | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | 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 | <span class="grow"></span> | 564 | <span class="grow"></span> | ||
| 565 | - | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | 565 | + | @diffViewMenu(p.DiffView) |
| 566 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |||
| 567 | - | @collapseButton() | |||
| 568 | </div> | 566 | </div> | ||
| 569 | if p.Invisible { | 567 | if p.Invisible { | ||
| 570 | <p class="notice"> | 568 | <p class="notice"> | ||
M internal/adapters/web/mr_templ.go +107 -123
| @@ -553,14 +553,14 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 553 | 553 | }() | |
| 554 | 554 | } | |
| 555 | 555 | ctx = templ.InitializeContext(ctx) | |
| 556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<header class=\"show mr-h\"><h1><span class=\"mute\">") | |
| 556 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">") | |
| 557 | 557 | if templ_7745c5c3_Err != nil { | |
| 558 | 558 | return templ_7745c5c3_Err | |
| 559 | 559 | } | |
| 560 | 560 | var templ_7745c5c3_Var26 string | |
| 561 | 561 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number) | |
| 562 | 562 | if templ_7745c5c3_Err != nil { | |
| 563 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 113, Col: 36} | |
| 563 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 37} | |
| 564 | 564 | } | |
| 565 | 565 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 566 | 566 | if templ_7745c5c3_Err != nil { | |
| @@ -573,7 +573,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 573 | 573 | var templ_7745c5c3_Var27 string | |
| 574 | 574 | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 575 | 575 | if templ_7745c5c3_Err != nil { | |
| 576 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 113, Col: 55} | |
| 576 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 56} | |
| 577 | 577 | } | |
| 578 | 578 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 579 | 579 | if templ_7745c5c3_Err != nil { | |
| @@ -586,7 +586,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 586 | 586 | var templ_7745c5c3_Var28 string | |
| 587 | 587 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State) | |
| 588 | 588 | if templ_7745c5c3_Err != nil { | |
| 589 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 116, Col: 45} | |
| 589 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 46} | |
| 590 | 590 | } | |
| 591 | 591 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | |
| 592 | 592 | if templ_7745c5c3_Err != nil { | |
| @@ -599,7 +599,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 599 | 599 | var templ_7745c5c3_Var29 string | |
| 600 | 600 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State) | |
| 601 | 601 | if templ_7745c5c3_Err != nil { | |
| 602 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 14} | |
| 602 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 118, Col: 15} | |
| 603 | 603 | } | |
| 604 | 604 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 605 | 605 | if templ_7745c5c3_Err != nil { | |
| @@ -613,7 +613,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 613 | 613 | var templ_7745c5c3_Var30 string | |
| 614 | 614 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 615 | 615 | if templ_7745c5c3_Err != nil { | |
| 616 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 14} | |
| 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 15} | |
| 617 | 617 | } | |
| 618 | 618 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 619 | 619 | if templ_7745c5c3_Err != nil { | |
| @@ -626,7 +626,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 626 | 626 | var templ_7745c5c3_Var31 templ.SafeURL | |
| 627 | 627 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref)) | |
| 628 | 628 | if templ_7745c5c3_Err != nil { | |
| 629 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 53} | |
| 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 54} | |
| 630 | 630 | } | |
| 631 | 631 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 632 | 632 | if templ_7745c5c3_Err != nil { | |
| @@ -639,7 +639,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 639 | 639 | var templ_7745c5c3_Var32 string | |
| 640 | 640 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort) | |
| 641 | 641 | if templ_7745c5c3_Err != nil { | |
| 642 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 71} | |
| 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 72} | |
| 643 | 643 | } | |
| 644 | 644 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 645 | 645 | if templ_7745c5c3_Err != nil { | |
| @@ -652,7 +652,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 652 | 652 | var templ_7745c5c3_Var33 string | |
| 653 | 653 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode) | |
| 654 | 654 | if templ_7745c5c3_Err != nil { | |
| 655 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 99} | |
| 655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 100} | |
| 656 | 656 | } | |
| 657 | 657 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 658 | 658 | if templ_7745c5c3_Err != nil { | |
| @@ -670,7 +670,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 670 | 670 | var templ_7745c5c3_Var34 string | |
| 671 | 671 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ") | |
| 672 | 672 | if templ_7745c5c3_Err != nil { | |
| 673 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 125, Col: 14} | |
| 673 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 126, Col: 15} | |
| 674 | 674 | } | |
| 675 | 675 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 676 | 676 | if templ_7745c5c3_Err != nil { | |
| @@ -687,7 +687,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 687 | 687 | var templ_7745c5c3_Var35 string | |
| 688 | 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name) | |
| 689 | 689 | if templ_7745c5c3_Err != nil { | |
| 690 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 130, Col: 20} | |
| 690 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 131, Col: 21} | |
| 691 | 691 | } | |
| 692 | 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 693 | 693 | if templ_7745c5c3_Err != nil { | |
| @@ -701,7 +701,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 701 | 701 | var templ_7745c5c3_Var36 string | |
| 702 | 702 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 703 | 703 | if templ_7745c5c3_Err != nil { | |
| 704 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 14} | |
| 704 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 15} | |
| 705 | 705 | } | |
| 706 | 706 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 707 | 707 | if templ_7745c5c3_Err != nil { | |
| @@ -714,7 +714,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 714 | 714 | var templ_7745c5c3_Var37 string | |
| 715 | 715 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt) | |
| 716 | 716 | if templ_7745c5c3_Err != nil { | |
| 717 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 48} | |
| 717 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 49} | |
| 718 | 718 | } | |
| 719 | 719 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | |
| 720 | 720 | if templ_7745c5c3_Err != nil { | |
| @@ -727,7 +727,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 727 | 727 | var templ_7745c5c3_Var38 string | |
| 728 | 728 | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs) | |
| 729 | 729 | if templ_7745c5c3_Err != nil { | |
| 730 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 71} | |
| 730 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 72} | |
| 731 | 731 | } | |
| 732 | 732 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) | |
| 733 | 733 | if templ_7745c5c3_Err != nil { | |
| @@ -740,7 +740,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 740 | 740 | var templ_7745c5c3_Var39 string | |
| 741 | 741 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When) | |
| 742 | 742 | if templ_7745c5c3_Err != nil { | |
| 743 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 89} | |
| 743 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 90} | |
| 744 | 744 | } | |
| 745 | 745 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 746 | 746 | if templ_7745c5c3_Err != nil { | |
| @@ -763,7 +763,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 763 | 763 | var templ_7745c5c3_Var40 string | |
| 764 | 764 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt) | |
| 765 | 765 | if templ_7745c5c3_Err != nil { | |
| 766 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 137, Col: 37} | |
| 766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 38} | |
| 767 | 767 | } | |
| 768 | 768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | |
| 769 | 769 | if templ_7745c5c3_Err != nil { | |
| @@ -776,7 +776,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 776 | 776 | var templ_7745c5c3_Var41 string | |
| 777 | 777 | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated) | |
| 778 | 778 | if templ_7745c5c3_Err != nil { | |
| 779 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 137, Col: 51} | |
| 779 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 52} | |
| 780 | 780 | } | |
| 781 | 781 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 782 | 782 | if templ_7745c5c3_Err != nil { | |
| @@ -791,16 +791,8 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 791 | 791 | if templ_7745c5c3_Err != nil { | |
| 792 | 792 | return templ_7745c5c3_Err | |
| 793 | 793 | } | |
| 794 | - | templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer) | |
| 795 | - | if templ_7745c5c3_Err != nil { | |
| 796 | - | return templ_7745c5c3_Err | |
| 797 | - | } | |
| 798 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " ") | |
| 799 | - | if templ_7745c5c3_Err != nil { | |
| 800 | - | return templ_7745c5c3_Err | |
| 801 | - | } | |
| 802 | 794 | if p.Desc != "" { | |
| 803 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>") | |
| 795 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>") | |
| 804 | 796 | if templ_7745c5c3_Err != nil { | |
| 805 | 797 | return templ_7745c5c3_Err | |
| 806 | 798 | } | |
| @@ -808,11 +800,19 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 808 | 800 | if templ_7745c5c3_Err != nil { | |
| 809 | 801 | return templ_7745c5c3_Err | |
| 810 | 802 | } | |
| 811 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</article>") | |
| 803 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</article>") | |
| 812 | 804 | if templ_7745c5c3_Err != nil { | |
| 813 | 805 | return templ_7745c5c3_Err | |
| 814 | 806 | } | |
| 815 | 807 | } | |
| 808 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</section>") | |
| 809 | + | if templ_7745c5c3_Err != nil { | |
| 810 | + | return templ_7745c5c3_Err | |
| 811 | + | } | |
| 812 | + | templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer) | |
| 813 | + | if templ_7745c5c3_Err != nil { | |
| 814 | + | return templ_7745c5c3_Err | |
| 815 | + | } | |
| 816 | 816 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ") | |
| 817 | 817 | if templ_7745c5c3_Err != nil { | |
| 818 | 818 | return templ_7745c5c3_Err | |
| @@ -825,7 +825,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 825 | 825 | var templ_7745c5c3_Var42 templ.SafeURL | |
| 826 | 826 | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref)) | |
| 827 | 827 | if templ_7745c5c3_Err != nil { | |
| 828 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 150, Col: 42} | |
| 828 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 152, Col: 42} | |
| 829 | 829 | } | |
| 830 | 830 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 831 | 831 | if templ_7745c5c3_Err != nil { | |
| @@ -848,7 +848,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 848 | 848 | var templ_7745c5c3_Var43 string | |
| 849 | 849 | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 850 | 850 | if templ_7745c5c3_Err != nil { | |
| 851 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 154, Col: 49} | |
| 851 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 49} | |
| 852 | 852 | } | |
| 853 | 853 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 854 | 854 | if templ_7745c5c3_Err != nil { | |
| @@ -861,7 +861,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 861 | 861 | var templ_7745c5c3_Var44 templ.SafeURL | |
| 862 | 862 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref)) | |
| 863 | 863 | if templ_7745c5c3_Err != nil { | |
| 864 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 44} | |
| 864 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 158, Col: 44} | |
| 865 | 865 | } | |
| 866 | 866 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 867 | 867 | if templ_7745c5c3_Err != nil { | |
| @@ -884,7 +884,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 884 | 884 | var templ_7745c5c3_Var45 string | |
| 885 | 885 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort) | |
| 886 | 886 | if templ_7745c5c3_Err != nil { | |
| 887 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 64} | |
| 887 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 162, Col: 64} | |
| 888 | 888 | } | |
| 889 | 889 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 890 | 890 | if templ_7745c5c3_Err != nil { | |
| @@ -897,7 +897,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 897 | 897 | var templ_7745c5c3_Var46 string | |
| 898 | 898 | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort) | |
| 899 | 899 | if templ_7745c5c3_Err != nil { | |
| 900 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 90} | |
| 900 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 162, Col: 90} | |
| 901 | 901 | } | |
| 902 | 902 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 903 | 903 | if templ_7745c5c3_Err != nil { | |
| @@ -920,7 +920,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 920 | 920 | var templ_7745c5c3_Var47 string | |
| 921 | 921 | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote) | |
| 922 | 922 | if templ_7745c5c3_Err != nil { | |
| 923 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 164, Col: 34} | |
| 923 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 34} | |
| 924 | 924 | } | |
| 925 | 925 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | |
| 926 | 926 | if templ_7745c5c3_Err != nil { | |
| @@ -961,7 +961,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 961 | 961 | var templ_7745c5c3_Var48 string | |
| 962 | 962 | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 963 | 963 | if templ_7745c5c3_Err != nil { | |
| 964 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 174, Col: 29} | |
| 964 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 176, Col: 29} | |
| 965 | 965 | } | |
| 966 | 966 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 967 | 967 | if templ_7745c5c3_Err != nil { | |
| @@ -984,7 +984,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 984 | 984 | var templ_7745c5c3_Var49 string | |
| 985 | 985 | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 986 | 986 | if templ_7745c5c3_Err != nil { | |
| 987 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 180, Col: 22} | |
| 987 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 182, Col: 22} | |
| 988 | 988 | } | |
| 989 | 989 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | |
| 990 | 990 | if templ_7745c5c3_Err != nil { | |
| @@ -1002,7 +1002,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1002 | 1002 | var templ_7745c5c3_Var50 string | |
| 1003 | 1003 | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1004 | 1004 | if templ_7745c5c3_Err != nil { | |
| 1005 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 182, Col: 22} | |
| 1005 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 184, Col: 22} | |
| 1006 | 1006 | } | |
| 1007 | 1007 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | |
| 1008 | 1008 | if templ_7745c5c3_Err != nil { | |
| @@ -1042,7 +1042,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1042 | 1042 | var templ_7745c5c3_Var51 string | |
| 1043 | 1043 | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | |
| 1044 | 1044 | if templ_7745c5c3_Err != nil { | |
| 1045 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 195, Col: 97} | |
| 1045 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 97} | |
| 1046 | 1046 | } | |
| 1047 | 1047 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 1048 | 1048 | if templ_7745c5c3_Err != nil { | |
| @@ -1055,7 +1055,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1055 | 1055 | var templ_7745c5c3_Var52 string | |
| 1056 | 1056 | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | |
| 1057 | 1057 | if templ_7745c5c3_Err != nil { | |
| 1058 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 195, Col: 126} | |
| 1058 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 126} | |
| 1059 | 1059 | } | |
| 1060 | 1060 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | |
| 1061 | 1061 | if templ_7745c5c3_Err != nil { | |
| @@ -1136,7 +1136,7 @@ func branchLink(name, href string) templ.Component { | |||
| 1136 | 1136 | var templ_7745c5c3_Var54 templ.SafeURL | |
| 1137 | 1137 | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 1138 | 1138 | if templ_7745c5c3_Err != nil { | |
| 1139 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 213, Col: 31} | |
| 1139 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 31} | |
| 1140 | 1140 | } | |
| 1141 | 1141 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1142 | 1142 | if templ_7745c5c3_Err != nil { | |
| @@ -1149,7 +1149,7 @@ func branchLink(name, href string) templ.Component { | |||
| 1149 | 1149 | var templ_7745c5c3_Var55 string | |
| 1150 | 1150 | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1151 | 1151 | if templ_7745c5c3_Err != nil { | |
| 1152 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 213, Col: 40} | |
| 1152 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 40} | |
| 1153 | 1153 | } | |
| 1154 | 1154 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1155 | 1155 | if templ_7745c5c3_Err != nil { | |
| @@ -1163,7 +1163,7 @@ func branchLink(name, href string) templ.Component { | |||
| 1163 | 1163 | var templ_7745c5c3_Var56 string | |
| 1164 | 1164 | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1165 | 1165 | if templ_7745c5c3_Err != nil { | |
| 1166 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 8} | |
| 1166 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 8} | |
| 1167 | 1167 | } | |
| 1168 | 1168 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | |
| 1169 | 1169 | if templ_7745c5c3_Err != nil { | |
| @@ -1244,7 +1244,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1244 | 1244 | var templ_7745c5c3_Var59 string | |
| 1245 | 1245 | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | |
| 1246 | 1246 | if templ_7745c5c3_Err != nil { | |
| 1247 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 42} | |
| 1247 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 42} | |
| 1248 | 1248 | } | |
| 1249 | 1249 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59) | |
| 1250 | 1250 | if templ_7745c5c3_Err != nil { | |
| @@ -1257,7 +1257,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1257 | 1257 | var templ_7745c5c3_Var60 string | |
| 1258 | 1258 | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | |
| 1259 | 1259 | if templ_7745c5c3_Err != nil { | |
| 1260 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 81} | |
| 1260 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 81} | |
| 1261 | 1261 | } | |
| 1262 | 1262 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60) | |
| 1263 | 1263 | if templ_7745c5c3_Err != nil { | |
| @@ -1280,7 +1280,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1280 | 1280 | var templ_7745c5c3_Var61 string | |
| 1281 | 1281 | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1282 | 1282 | if templ_7745c5c3_Err != nil { | |
| 1283 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 33} | |
| 1283 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 33} | |
| 1284 | 1284 | } | |
| 1285 | 1285 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1286 | 1286 | if templ_7745c5c3_Err != nil { | |
| @@ -1294,7 +1294,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1294 | 1294 | var templ_7745c5c3_Var62 string | |
| 1295 | 1295 | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1296 | 1296 | if templ_7745c5c3_Err != nil { | |
| 1297 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 9} | |
| 1297 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 9} | |
| 1298 | 1298 | } | |
| 1299 | 1299 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1300 | 1300 | if templ_7745c5c3_Err != nil { | |
| @@ -1307,7 +1307,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1307 | 1307 | var templ_7745c5c3_Var63 string | |
| 1308 | 1308 | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1309 | 1309 | if templ_7745c5c3_Err != nil { | |
| 1310 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 49} | |
| 1310 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 49} | |
| 1311 | 1311 | } | |
| 1312 | 1312 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | |
| 1313 | 1313 | if templ_7745c5c3_Err != nil { | |
| @@ -1320,7 +1320,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1320 | 1320 | var templ_7745c5c3_Var64 string | |
| 1321 | 1321 | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | |
| 1322 | 1322 | if templ_7745c5c3_Err != nil { | |
| 1323 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 65} | |
| 1323 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 65} | |
| 1324 | 1324 | } | |
| 1325 | 1325 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) | |
| 1326 | 1326 | if templ_7745c5c3_Err != nil { | |
| @@ -1333,7 +1333,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1333 | 1333 | var templ_7745c5c3_Var65 string | |
| 1334 | 1334 | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1335 | 1335 | if templ_7745c5c3_Err != nil { | |
| 1336 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 76} | |
| 1336 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 76} | |
| 1337 | 1337 | } | |
| 1338 | 1338 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1339 | 1339 | if templ_7745c5c3_Err != nil { | |
| @@ -1348,7 +1348,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1348 | 1348 | var templ_7745c5c3_Var66 string | |
| 1349 | 1349 | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1350 | 1350 | if templ_7745c5c3_Err != nil { | |
| 1351 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 9} | |
| 1351 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 9} | |
| 1352 | 1352 | } | |
| 1353 | 1353 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | |
| 1354 | 1354 | if templ_7745c5c3_Err != nil { | |
| @@ -1363,7 +1363,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1363 | 1363 | var templ_7745c5c3_Var67 string | |
| 1364 | 1364 | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1365 | 1365 | if templ_7745c5c3_Err != nil { | |
| 1366 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9} | |
| 1366 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 246, Col: 9} | |
| 1367 | 1367 | } | |
| 1368 | 1368 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) | |
| 1369 | 1369 | if templ_7745c5c3_Err != nil { | |
| @@ -1378,7 +1378,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1378 | 1378 | var templ_7745c5c3_Var68 string | |
| 1379 | 1379 | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1380 | 1380 | if templ_7745c5c3_Err != nil { | |
| 1381 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9} | |
| 1381 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9} | |
| 1382 | 1382 | } | |
| 1383 | 1383 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1384 | 1384 | if templ_7745c5c3_Err != nil { | |
| @@ -1391,7 +1391,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1391 | 1391 | var templ_7745c5c3_Var69 string | |
| 1392 | 1392 | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | |
| 1393 | 1393 | if templ_7745c5c3_Err != nil { | |
| 1394 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 47} | |
| 1394 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 47} | |
| 1395 | 1395 | } | |
| 1396 | 1396 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1397 | 1397 | if templ_7745c5c3_Err != nil { | |
| @@ -1466,7 +1466,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1466 | 1466 | var templ_7745c5c3_Var70 string | |
| 1467 | 1467 | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict) | |
| 1468 | 1468 | if templ_7745c5c3_Err != nil { | |
| 1469 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 271, Col: 36} | |
| 1469 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 36} | |
| 1470 | 1470 | } | |
| 1471 | 1471 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1472 | 1472 | if templ_7745c5c3_Err != nil { | |
| @@ -1485,7 +1485,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1485 | 1485 | var templ_7745c5c3_Var71 string | |
| 1486 | 1486 | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range) | |
| 1487 | 1487 | if templ_7745c5c3_Err != nil { | |
| 1488 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 31} | |
| 1488 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 31} | |
| 1489 | 1489 | } | |
| 1490 | 1490 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1491 | 1491 | if templ_7745c5c3_Err != nil { | |
| @@ -1504,7 +1504,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1504 | 1504 | var templ_7745c5c3_Var72 string | |
| 1505 | 1505 | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote) | |
| 1506 | 1506 | if templ_7745c5c3_Err != nil { | |
| 1507 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 277, Col: 65} | |
| 1507 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 279, Col: 65} | |
| 1508 | 1508 | } | |
| 1509 | 1509 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72) | |
| 1510 | 1510 | if templ_7745c5c3_Err != nil { | |
| @@ -1523,7 +1523,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1523 | 1523 | var templ_7745c5c3_Var73 templ.SafeURL | |
| 1524 | 1524 | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action)) | |
| 1525 | 1525 | if templ_7745c5c3_Err != nil { | |
| 1526 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 280, Col: 92} | |
| 1526 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 92} | |
| 1527 | 1527 | } | |
| 1528 | 1528 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | |
| 1529 | 1529 | if templ_7745c5c3_Err != nil { | |
| @@ -1536,7 +1536,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1536 | 1536 | var templ_7745c5c3_Var74 string | |
| 1537 | 1537 | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From)) | |
| 1538 | 1538 | if templ_7745c5c3_Err != nil { | |
| 1539 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 280, Col: 153} | |
| 1539 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 153} | |
| 1540 | 1540 | } | |
| 1541 | 1541 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1542 | 1542 | if templ_7745c5c3_Err != nil { | |
| @@ -1549,7 +1549,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1549 | 1549 | var templ_7745c5c3_Var75 string | |
| 1550 | 1550 | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text) | |
| 1551 | 1551 | if templ_7745c5c3_Err != nil { | |
| 1552 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 280, Col: 209} | |
| 1552 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 209} | |
| 1553 | 1553 | } | |
| 1554 | 1554 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1555 | 1555 | if templ_7745c5c3_Err != nil { | |
| @@ -1568,7 +1568,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1568 | 1568 | var templ_7745c5c3_Var76 templ.SafeURL | |
| 1569 | 1569 | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action)) | |
| 1570 | 1570 | if templ_7745c5c3_Err != nil { | |
| 1571 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 283, Col: 96} | |
| 1571 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 96} | |
| 1572 | 1572 | } | |
| 1573 | 1573 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | |
| 1574 | 1574 | if templ_7745c5c3_Err != nil { | |
| @@ -1581,7 +1581,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1581 | 1581 | var templ_7745c5c3_Var77 string | |
| 1582 | 1582 | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From)) | |
| 1583 | 1583 | if templ_7745c5c3_Err != nil { | |
| 1584 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 283, Col: 159} | |
| 1584 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 159} | |
| 1585 | 1585 | } | |
| 1586 | 1586 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) | |
| 1587 | 1587 | if templ_7745c5c3_Err != nil { | |
| @@ -1620,7 +1620,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1620 | 1620 | var templ_7745c5c3_Var78 templ.SafeURL | |
| 1621 | 1621 | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | |
| 1622 | 1622 | if templ_7745c5c3_Err != nil { | |
| 1623 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 77} | |
| 1623 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 77} | |
| 1624 | 1624 | } | |
| 1625 | 1625 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | |
| 1626 | 1626 | if templ_7745c5c3_Err != nil { | |
| @@ -1633,7 +1633,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1633 | 1633 | var templ_7745c5c3_Var79 string | |
| 1634 | 1634 | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | |
| 1635 | 1635 | if templ_7745c5c3_Err != nil { | |
| 1636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 74} | |
| 1636 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 74} | |
| 1637 | 1637 | } | |
| 1638 | 1638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | |
| 1639 | 1639 | if templ_7745c5c3_Err != nil { | |
| @@ -1651,7 +1651,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1651 | 1651 | var templ_7745c5c3_Var80 string | |
| 1652 | 1652 | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From)) | |
| 1653 | 1653 | if templ_7745c5c3_Err != nil { | |
| 1654 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 66} | |
| 1654 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 299, Col: 66} | |
| 1655 | 1655 | } | |
| 1656 | 1656 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1657 | 1657 | if templ_7745c5c3_Err != nil { | |
| @@ -1669,7 +1669,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1669 | 1669 | var templ_7745c5c3_Var81 string | |
| 1670 | 1670 | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | |
| 1671 | 1671 | if templ_7745c5c3_Err != nil { | |
| 1672 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 299, Col: 55} | |
| 1672 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 301, Col: 55} | |
| 1673 | 1673 | } | |
| 1674 | 1674 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | |
| 1675 | 1675 | if templ_7745c5c3_Err != nil { | |
| @@ -1724,7 +1724,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1724 | 1724 | var templ_7745c5c3_Var83 templ.SafeURL | |
| 1725 | 1725 | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | |
| 1726 | 1726 | if templ_7745c5c3_Err != nil { | |
| 1727 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 314, Col: 67} | |
| 1727 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 316, Col: 67} | |
| 1728 | 1728 | } | |
| 1729 | 1729 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) | |
| 1730 | 1730 | if templ_7745c5c3_Err != nil { | |
| @@ -1737,7 +1737,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1737 | 1737 | var templ_7745c5c3_Var84 string | |
| 1738 | 1738 | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1739 | 1739 | if templ_7745c5c3_Err != nil { | |
| 1740 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 28} | |
| 1740 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 28} | |
| 1741 | 1741 | } | |
| 1742 | 1742 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) | |
| 1743 | 1743 | if templ_7745c5c3_Err != nil { | |
| @@ -1750,7 +1750,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1750 | 1750 | var templ_7745c5c3_Var85 string | |
| 1751 | 1751 | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | |
| 1752 | 1752 | if templ_7745c5c3_Err != nil { | |
| 1753 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 40} | |
| 1753 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 40} | |
| 1754 | 1754 | } | |
| 1755 | 1755 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85)) | |
| 1756 | 1756 | if templ_7745c5c3_Err != nil { | |
| @@ -1763,7 +1763,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1763 | 1763 | var templ_7745c5c3_Var86 string | |
| 1764 | 1764 | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1765 | 1765 | if templ_7745c5c3_Err != nil { | |
| 1766 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 21} | |
| 1766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 21} | |
| 1767 | 1767 | } | |
| 1768 | 1768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86) | |
| 1769 | 1769 | if templ_7745c5c3_Err != nil { | |
| @@ -1802,7 +1802,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1802 | 1802 | var templ_7745c5c3_Var87 string | |
| 1803 | 1803 | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 1804 | 1804 | if templ_7745c5c3_Err != nil { | |
| 1805 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 332, Col: 50} | |
| 1805 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 50} | |
| 1806 | 1806 | } | |
| 1807 | 1807 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87) | |
| 1808 | 1808 | if templ_7745c5c3_Err != nil { | |
| @@ -1815,7 +1815,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1815 | 1815 | var templ_7745c5c3_Var88 string | |
| 1816 | 1816 | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | |
| 1817 | 1817 | if templ_7745c5c3_Err != nil { | |
| 1818 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 333, Col: 50} | |
| 1818 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 50} | |
| 1819 | 1819 | } | |
| 1820 | 1820 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88) | |
| 1821 | 1821 | if templ_7745c5c3_Err != nil { | |
| @@ -1828,7 +1828,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1828 | 1828 | var templ_7745c5c3_Var89 string | |
| 1829 | 1829 | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | |
| 1830 | 1830 | if templ_7745c5c3_Err != nil { | |
| 1831 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 50} | |
| 1831 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 50} | |
| 1832 | 1832 | } | |
| 1833 | 1833 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89) | |
| 1834 | 1834 | if templ_7745c5c3_Err != nil { | |
| @@ -1841,7 +1841,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1841 | 1841 | var templ_7745c5c3_Var90 string | |
| 1842 | 1842 | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine) | |
| 1843 | 1843 | if templ_7745c5c3_Err != nil { | |
| 1844 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 86} | |
| 1844 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 337, Col: 86} | |
| 1845 | 1845 | } | |
| 1846 | 1846 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90) | |
| 1847 | 1847 | if templ_7745c5c3_Err != nil { | |
| @@ -1860,7 +1860,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1860 | 1860 | var templ_7745c5c3_Var91 string | |
| 1861 | 1861 | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo) | |
| 1862 | 1862 | if templ_7745c5c3_Err != nil { | |
| 1863 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 338, Col: 57} | |
| 1863 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 340, Col: 57} | |
| 1864 | 1864 | } | |
| 1865 | 1865 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91) | |
| 1866 | 1866 | if templ_7745c5c3_Err != nil { | |
| @@ -1885,7 +1885,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1885 | 1885 | var templ_7745c5c3_Var92 string | |
| 1886 | 1886 | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From)) | |
| 1887 | 1887 | if templ_7745c5c3_Err != nil { | |
| 1888 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 344, Col: 56} | |
| 1888 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 56} | |
| 1889 | 1889 | } | |
| 1890 | 1890 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92) | |
| 1891 | 1891 | if templ_7745c5c3_Err != nil { | |
| @@ -1908,7 +1908,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1908 | 1908 | var templ_7745c5c3_Var93 templ.SafeURL | |
| 1909 | 1909 | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | |
| 1910 | 1910 | if templ_7745c5c3_Err != nil { | |
| 1911 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 49} | |
| 1911 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 351, Col: 49} | |
| 1912 | 1912 | } | |
| 1913 | 1913 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93)) | |
| 1914 | 1914 | if templ_7745c5c3_Err != nil { | |
| @@ -1955,7 +1955,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1955 | 1955 | var templ_7745c5c3_Var95 string | |
| 1956 | 1956 | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 1957 | 1957 | if templ_7745c5c3_Err != nil { | |
| 1958 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 358, Col: 49} | |
| 1958 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 360, Col: 49} | |
| 1959 | 1959 | } | |
| 1960 | 1960 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | |
| 1961 | 1961 | if templ_7745c5c3_Err != nil { | |
| @@ -1986,7 +1986,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1986 | 1986 | var templ_7745c5c3_Var96 templ.SafeURL | |
| 1987 | 1987 | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href)) | |
| 1988 | 1988 | if templ_7745c5c3_Err != nil { | |
| 1989 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 371, Col: 37} | |
| 1989 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 37} | |
| 1990 | 1990 | } | |
| 1991 | 1991 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | |
| 1992 | 1992 | if templ_7745c5c3_Err != nil { | |
| @@ -1999,7 +1999,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 1999 | 1999 | var templ_7745c5c3_Var97 string | |
| 2000 | 2000 | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author) | |
| 2001 | 2001 | if templ_7745c5c3_Err != nil { | |
| 2002 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 371, Col: 50} | |
| 2002 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 50} | |
| 2003 | 2003 | } | |
| 2004 | 2004 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) | |
| 2005 | 2005 | if templ_7745c5c3_Err != nil { | |
| @@ -2012,7 +2012,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2012 | 2012 | var templ_7745c5c3_Var98 string | |
| 2013 | 2013 | templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label) | |
| 2014 | 2014 | if templ_7745c5c3_Err != nil { | |
| 2015 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 371, Col: 63} | |
| 2015 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 63} | |
| 2016 | 2016 | } | |
| 2017 | 2017 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98)) | |
| 2018 | 2018 | if templ_7745c5c3_Err != nil { | |
| @@ -2049,7 +2049,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2049 | 2049 | var templ_7745c5c3_Var99 string | |
| 2050 | 2050 | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | |
| 2051 | 2051 | if templ_7745c5c3_Err != nil { | |
| 2052 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 71} | |
| 2052 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 384, Col: 71} | |
| 2053 | 2053 | } | |
| 2054 | 2054 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 2055 | 2055 | if templ_7745c5c3_Err != nil { | |
| @@ -2077,7 +2077,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2077 | 2077 | var templ_7745c5c3_Var100 templ.SafeURL | |
| 2078 | 2078 | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 2079 | 2079 | if templ_7745c5c3_Err != nil { | |
| 2080 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 36} | |
| 2080 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 36} | |
| 2081 | 2081 | } | |
| 2082 | 2082 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 2083 | 2083 | if templ_7745c5c3_Err != nil { | |
| @@ -2090,7 +2090,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2090 | 2090 | var templ_7745c5c3_Var101 string | |
| 2091 | 2091 | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2092 | 2092 | if templ_7745c5c3_Err != nil { | |
| 2093 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 48} | |
| 2093 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 48} | |
| 2094 | 2094 | } | |
| 2095 | 2095 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | |
| 2096 | 2096 | if templ_7745c5c3_Err != nil { | |
| @@ -2103,7 +2103,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2103 | 2103 | var templ_7745c5c3_Var102 string | |
| 2104 | 2104 | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2105 | 2105 | if templ_7745c5c3_Err != nil { | |
| 2106 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 32} | |
| 2106 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 389, Col: 32} | |
| 2107 | 2107 | } | |
| 2108 | 2108 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) | |
| 2109 | 2109 | if templ_7745c5c3_Err != nil { | |
| @@ -2126,7 +2126,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2126 | 2126 | var templ_7745c5c3_Var103 string | |
| 2127 | 2127 | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2128 | 2128 | if templ_7745c5c3_Err != nil { | |
| 2129 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 392, Col: 16} | |
| 2129 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 16} | |
| 2130 | 2130 | } | |
| 2131 | 2131 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) | |
| 2132 | 2132 | if templ_7745c5c3_Err != nil { | |
| @@ -2140,7 +2140,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2140 | 2140 | var templ_7745c5c3_Var104 string | |
| 2141 | 2141 | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2142 | 2142 | if templ_7745c5c3_Err != nil { | |
| 2143 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 15} | |
| 2143 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 15} | |
| 2144 | 2144 | } | |
| 2145 | 2145 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | |
| 2146 | 2146 | if templ_7745c5c3_Err != nil { | |
| @@ -2153,7 +2153,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2153 | 2153 | var templ_7745c5c3_Var105 string | |
| 2154 | 2154 | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2155 | 2155 | if templ_7745c5c3_Err != nil { | |
| 2156 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 42} | |
| 2156 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 42} | |
| 2157 | 2157 | } | |
| 2158 | 2158 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105) | |
| 2159 | 2159 | if templ_7745c5c3_Err != nil { | |
| @@ -2166,7 +2166,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2166 | 2166 | var templ_7745c5c3_Var106 string | |
| 2167 | 2167 | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2168 | 2168 | if templ_7745c5c3_Err != nil { | |
| 2169 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 53} | |
| 2169 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 53} | |
| 2170 | 2170 | } | |
| 2171 | 2171 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) | |
| 2172 | 2172 | if templ_7745c5c3_Err != nil { | |
| @@ -2181,7 +2181,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2181 | 2181 | var templ_7745c5c3_Var107 string | |
| 2182 | 2182 | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | |
| 2183 | 2183 | if templ_7745c5c3_Err != nil { | |
| 2184 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 18} | |
| 2184 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 18} | |
| 2185 | 2185 | } | |
| 2186 | 2186 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | |
| 2187 | 2187 | if templ_7745c5c3_Err != nil { | |
| @@ -2211,7 +2211,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2211 | 2211 | var templ_7745c5c3_Var108 string | |
| 2212 | 2212 | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | |
| 2213 | 2213 | if templ_7745c5c3_Err != nil { | |
| 2214 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 407, Col: 34} | |
| 2214 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 409, Col: 34} | |
| 2215 | 2215 | } | |
| 2216 | 2216 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) | |
| 2217 | 2217 | if templ_7745c5c3_Err != nil { | |
| @@ -2229,7 +2229,7 @@ func mrRail(p MRPage) templ.Component { | |||
| 2229 | 2229 | var templ_7745c5c3_Var109 string | |
| 2230 | 2230 | templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | |
| 2231 | 2231 | if templ_7745c5c3_Err != nil { | |
| 2232 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 414, Col: 36} | |
| 2232 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 36} | |
| 2233 | 2233 | } | |
| 2234 | 2234 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) | |
| 2235 | 2235 | if templ_7745c5c3_Err != nil { | |
| @@ -2290,7 +2290,7 @@ func mrBar(p MRPage) templ.Component { | |||
| 2290 | 2290 | var templ_7745c5c3_Var111 string | |
| 2291 | 2291 | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2292 | 2292 | if templ_7745c5c3_Err != nil { | |
| 2293 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 55} | |
| 2293 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 55} | |
| 2294 | 2294 | } | |
| 2295 | 2295 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | |
| 2296 | 2296 | if templ_7745c5c3_Err != nil { | |
| @@ -2303,7 +2303,7 @@ func mrBar(p MRPage) templ.Component { | |||
| 2303 | 2303 | var templ_7745c5c3_Var112 string | |
| 2304 | 2304 | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2305 | 2305 | if templ_7745c5c3_Err != nil { | |
| 2306 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 95} | |
| 2306 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 95} | |
| 2307 | 2307 | } | |
| 2308 | 2308 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | |
| 2309 | 2309 | if templ_7745c5c3_Err != nil { | |
| @@ -2316,7 +2316,7 @@ func mrBar(p MRPage) templ.Component { | |||
| 2316 | 2316 | var templ_7745c5c3_Var113 string | |
| 2317 | 2317 | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2318 | 2318 | if templ_7745c5c3_Err != nil { | |
| 2319 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 140} | |
| 2319 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 140} | |
| 2320 | 2320 | } | |
| 2321 | 2321 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | |
| 2322 | 2322 | if templ_7745c5c3_Err != nil { | |
| @@ -2326,15 +2326,7 @@ func mrBar(p MRPage) templ.Component { | |||
| 2326 | 2326 | if templ_7745c5c3_Err != nil { | |
| 2327 | 2327 | return templ_7745c5c3_Err | |
| 2328 | 2328 | } | |
| 2329 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 2330 | - | if templ_7745c5c3_Err != nil { | |
| 2331 | - | return templ_7745c5c3_Err | |
| 2332 | - | } | |
| 2333 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 2334 | - | if templ_7745c5c3_Err != nil { | |
| 2335 | - | return templ_7745c5c3_Err | |
| 2336 | - | } | |
| 2337 | - | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |
| 2329 | + | templ_7745c5c3_Err = diffViewMenu(p.DiffView).Render(ctx, templ_7745c5c3_Buffer) | |
| 2338 | 2330 | if templ_7745c5c3_Err != nil { | |
| 2339 | 2331 | return templ_7745c5c3_Err | |
| 2340 | 2332 | } | |
| @@ -2998,15 +2990,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2998 | 2990 | if templ_7745c5c3_Err != nil { | |
| 2999 | 2991 | return templ_7745c5c3_Err | |
| 3000 | 2992 | } | |
| 3001 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 3002 | - | if templ_7745c5c3_Err != nil { | |
| 3003 | - | return templ_7745c5c3_Err | |
| 3004 | - | } | |
| 3005 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 3006 | - | if templ_7745c5c3_Err != nil { | |
| 3007 | - | return templ_7745c5c3_Err | |
| 3008 | - | } | |
| 3009 | - | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |
| 2993 | + | templ_7745c5c3_Err = diffViewMenu(p.DiffView).Render(ctx, templ_7745c5c3_Buffer) | |
| 3010 | 2994 | if templ_7745c5c3_Err != nil { | |
| 3011 | 2995 | return templ_7745c5c3_Err | |
| 3012 | 2996 | } | |
| @@ -3032,7 +3016,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 3032 | 3016 | var templ_7745c5c3_Var148 string | |
| 3033 | 3017 | templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 3034 | 3018 | if templ_7745c5c3_Err != nil { | |
| 3035 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 23} | |
| 3019 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 576, Col: 23} | |
| 3036 | 3020 | } | |
| 3037 | 3021 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148) | |
| 3038 | 3022 | if templ_7745c5c3_Err != nil { | |
| @@ -3050,7 +3034,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 3050 | 3034 | var templ_7745c5c3_Var149 string | |
| 3051 | 3035 | templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 3052 | 3036 | if templ_7745c5c3_Err != nil { | |
| 3053 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 580, Col: 23} | |
| 3037 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 23} | |
| 3054 | 3038 | } | |
| 3055 | 3039 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149) | |
| 3056 | 3040 | if templ_7745c5c3_Err != nil { | |
| @@ -3120,7 +3104,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3120 | 3104 | var templ_7745c5c3_Var151 string | |
| 3121 | 3105 | templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 3122 | 3106 | if templ_7745c5c3_Err != nil { | |
| 3123 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 67} | |
| 3107 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 67} | |
| 3124 | 3108 | } | |
| 3125 | 3109 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151)) | |
| 3126 | 3110 | if templ_7745c5c3_Err != nil { | |
| @@ -3138,7 +3122,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3138 | 3122 | var templ_7745c5c3_Var152 templ.SafeURL | |
| 3139 | 3123 | templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 3140 | 3124 | if templ_7745c5c3_Err != nil { | |
| 3141 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 600, Col: 37} | |
| 3125 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 37} | |
| 3142 | 3126 | } | |
| 3143 | 3127 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152)) | |
| 3144 | 3128 | if templ_7745c5c3_Err != nil { | |
| @@ -3151,7 +3135,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3151 | 3135 | var templ_7745c5c3_Var153 string | |
| 3152 | 3136 | templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 3153 | 3137 | if templ_7745c5c3_Err != nil { | |
| 3154 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 600, Col: 49} | |
| 3138 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 49} | |
| 3155 | 3139 | } | |
| 3156 | 3140 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153)) | |
| 3157 | 3141 | if templ_7745c5c3_Err != nil { | |
| @@ -3164,7 +3148,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3164 | 3148 | var templ_7745c5c3_Var154 string | |
| 3165 | 3149 | templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 3166 | 3150 | if templ_7745c5c3_Err != nil { | |
| 3167 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 32} | |
| 3151 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 599, Col: 32} | |
| 3168 | 3152 | } | |
| 3169 | 3153 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154)) | |
| 3170 | 3154 | if templ_7745c5c3_Err != nil { | |
| @@ -3177,7 +3161,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3177 | 3161 | var templ_7745c5c3_Var155 string | |
| 3178 | 3162 | templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 3179 | 3163 | if templ_7745c5c3_Err != nil { | |
| 3180 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 16} | |
| 3164 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 16} | |
| 3181 | 3165 | } | |
| 3182 | 3166 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155)) | |
| 3183 | 3167 | if templ_7745c5c3_Err != nil { | |
| @@ -3191,7 +3175,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3191 | 3175 | var templ_7745c5c3_Var156 string | |
| 3192 | 3176 | templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 3193 | 3177 | if templ_7745c5c3_Err != nil { | |
| 3194 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 15} | |
| 3178 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 15} | |
| 3195 | 3179 | } | |
| 3196 | 3180 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156)) | |
| 3197 | 3181 | if templ_7745c5c3_Err != nil { | |
| @@ -3204,7 +3188,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3204 | 3188 | var templ_7745c5c3_Var157 string | |
| 3205 | 3189 | templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 3206 | 3190 | if templ_7745c5c3_Err != nil { | |
| 3207 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 42} | |
| 3191 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 42} | |
| 3208 | 3192 | } | |
| 3209 | 3193 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157) | |
| 3210 | 3194 | if templ_7745c5c3_Err != nil { | |
| @@ -3217,7 +3201,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3217 | 3201 | var templ_7745c5c3_Var158 string | |
| 3218 | 3202 | templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 3219 | 3203 | if templ_7745c5c3_Err != nil { | |
| 3220 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 53} | |
| 3204 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 53} | |
| 3221 | 3205 | } | |
| 3222 | 3206 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158)) | |
| 3223 | 3207 | if templ_7745c5c3_Err != nil { | |
| @@ -3245,7 +3229,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3245 | 3229 | var templ_7745c5c3_Var159 string | |
| 3246 | 3230 | templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 3247 | 3231 | if templ_7745c5c3_Err != nil { | |
| 3248 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 612, Col: 47} | |
| 3232 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 610, Col: 47} | |
| 3249 | 3233 | } | |
| 3250 | 3234 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159)) | |
| 3251 | 3235 | if templ_7745c5c3_Err != nil { | |
| @@ -3268,7 +3252,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 3268 | 3252 | var templ_7745c5c3_Var160 string | |
| 3269 | 3253 | templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 3270 | 3254 | if templ_7745c5c3_Err != nil { | |
| 3271 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 617, Col: 66} | |
| 3255 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 615, Col: 66} | |
| 3272 | 3256 | } | |
| 3273 | 3257 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160)) | |
| 3274 | 3258 | if templ_7745c5c3_Err != nil { | |
| @@ -553,14 +553,14 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 553 | }() | 553 | }() | ||
| 554 | } | 554 | } | ||
| 555 | ctx = templ.InitializeContext(ctx) | 555 | ctx = templ.InitializeContext(ctx) | ||
| 556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<header class=\"show mr-h\"><h1><span class=\"mute\">") | 556 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">") |
| 557 | if templ_7745c5c3_Err != nil { | 557 | if templ_7745c5c3_Err != nil { | ||
| 558 | return templ_7745c5c3_Err | 558 | return templ_7745c5c3_Err | ||
| 559 | } | 559 | } | ||
| 560 | var templ_7745c5c3_Var26 string | 560 | var templ_7745c5c3_Var26 string | ||
| 561 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number) | 561 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number) | ||
| 562 | if templ_7745c5c3_Err != nil { | 562 | if templ_7745c5c3_Err != nil { | ||
| 563 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 113, Col: 36} | 563 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 37} |
| 564 | } | 564 | } | ||
| 565 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | 565 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | ||
| 566 | if templ_7745c5c3_Err != nil { | 566 | if templ_7745c5c3_Err != nil { | ||
| @@ -573,7 +573,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 573 | var templ_7745c5c3_Var27 string | 573 | var templ_7745c5c3_Var27 string | ||
| 574 | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | 574 | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | ||
| 575 | if templ_7745c5c3_Err != nil { | 575 | if templ_7745c5c3_Err != nil { | ||
| 576 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 113, Col: 55} | 576 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 56} |
| 577 | } | 577 | } | ||
| 578 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | 578 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | ||
| 579 | if templ_7745c5c3_Err != nil { | 579 | if templ_7745c5c3_Err != nil { | ||
| @@ -586,7 +586,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 586 | var templ_7745c5c3_Var28 string | 586 | var templ_7745c5c3_Var28 string | ||
| 587 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State) | 587 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State) | ||
| 588 | if templ_7745c5c3_Err != nil { | 588 | if templ_7745c5c3_Err != nil { | ||
| 589 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 116, Col: 45} | 589 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 46} |
| 590 | } | 590 | } | ||
| 591 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | 591 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | ||
| 592 | if templ_7745c5c3_Err != nil { | 592 | if templ_7745c5c3_Err != nil { | ||
| @@ -599,7 +599,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 599 | var templ_7745c5c3_Var29 string | 599 | var templ_7745c5c3_Var29 string | ||
| 600 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State) | 600 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State) | ||
| 601 | if templ_7745c5c3_Err != nil { | 601 | if templ_7745c5c3_Err != nil { | ||
| 602 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 14} | 602 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 118, Col: 15} |
| 603 | } | 603 | } | ||
| 604 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | 604 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | ||
| 605 | if templ_7745c5c3_Err != nil { | 605 | if templ_7745c5c3_Err != nil { | ||
| @@ -613,7 +613,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 613 | var templ_7745c5c3_Var30 string | 613 | var templ_7745c5c3_Var30 string | ||
| 614 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 614 | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | ||
| 615 | if templ_7745c5c3_Err != nil { | 615 | if templ_7745c5c3_Err != nil { | ||
| 616 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 14} | 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 15} |
| 617 | } | 617 | } | ||
| 618 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | 618 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | ||
| 619 | if templ_7745c5c3_Err != nil { | 619 | if templ_7745c5c3_Err != nil { | ||
| @@ -626,7 +626,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 626 | var templ_7745c5c3_Var31 templ.SafeURL | 626 | var templ_7745c5c3_Var31 templ.SafeURL | ||
| 627 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref)) | 627 | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref)) | ||
| 628 | if templ_7745c5c3_Err != nil { | 628 | if templ_7745c5c3_Err != nil { | ||
| 629 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 53} | 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 54} |
| 630 | } | 630 | } | ||
| 631 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | 631 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | ||
| 632 | if templ_7745c5c3_Err != nil { | 632 | if templ_7745c5c3_Err != nil { | ||
| @@ -639,7 +639,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 639 | var templ_7745c5c3_Var32 string | 639 | var templ_7745c5c3_Var32 string | ||
| 640 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort) | 640 | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort) | ||
| 641 | if templ_7745c5c3_Err != nil { | 641 | if templ_7745c5c3_Err != nil { | ||
| 642 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 71} | 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 72} |
| 643 | } | 643 | } | ||
| 644 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | 644 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | ||
| 645 | if templ_7745c5c3_Err != nil { | 645 | if templ_7745c5c3_Err != nil { | ||
| @@ -652,7 +652,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 652 | var templ_7745c5c3_Var33 string | 652 | var templ_7745c5c3_Var33 string | ||
| 653 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode) | 653 | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode) | ||
| 654 | if templ_7745c5c3_Err != nil { | 654 | if templ_7745c5c3_Err != nil { | ||
| 655 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 99} | 655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 100} |
| 656 | } | 656 | } | ||
| 657 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | 657 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | ||
| 658 | if templ_7745c5c3_Err != nil { | 658 | if templ_7745c5c3_Err != nil { | ||
| @@ -670,7 +670,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 670 | var templ_7745c5c3_Var34 string | 670 | var templ_7745c5c3_Var34 string | ||
| 671 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ") | 671 | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ") | ||
| 672 | if templ_7745c5c3_Err != nil { | 672 | if templ_7745c5c3_Err != nil { | ||
| 673 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 125, Col: 14} | 673 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 126, Col: 15} |
| 674 | } | 674 | } | ||
| 675 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | 675 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | ||
| 676 | if templ_7745c5c3_Err != nil { | 676 | if templ_7745c5c3_Err != nil { | ||
| @@ -687,7 +687,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 687 | var templ_7745c5c3_Var35 string | 687 | var templ_7745c5c3_Var35 string | ||
| 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name) | 688 | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name) | ||
| 689 | if templ_7745c5c3_Err != nil { | 689 | if templ_7745c5c3_Err != nil { | ||
| 690 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 130, Col: 20} | 690 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 131, Col: 21} |
| 691 | } | 691 | } | ||
| 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | 692 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | ||
| 693 | if templ_7745c5c3_Err != nil { | 693 | if templ_7745c5c3_Err != nil { | ||
| @@ -701,7 +701,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 701 | var templ_7745c5c3_Var36 string | 701 | var templ_7745c5c3_Var36 string | ||
| 702 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 702 | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | ||
| 703 | if templ_7745c5c3_Err != nil { | 703 | if templ_7745c5c3_Err != nil { | ||
| 704 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 14} | 704 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 15} |
| 705 | } | 705 | } | ||
| 706 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | 706 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | ||
| 707 | if templ_7745c5c3_Err != nil { | 707 | if templ_7745c5c3_Err != nil { | ||
| @@ -714,7 +714,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 714 | var templ_7745c5c3_Var37 string | 714 | var templ_7745c5c3_Var37 string | ||
| 715 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt) | 715 | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt) | ||
| 716 | if templ_7745c5c3_Err != nil { | 716 | if templ_7745c5c3_Err != nil { | ||
| 717 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 48} | 717 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 49} |
| 718 | } | 718 | } | ||
| 719 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | 719 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | ||
| 720 | if templ_7745c5c3_Err != nil { | 720 | if templ_7745c5c3_Err != nil { | ||
| @@ -727,7 +727,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 727 | var templ_7745c5c3_Var38 string | 727 | var templ_7745c5c3_Var38 string | ||
| 728 | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs) | 728 | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs) | ||
| 729 | if templ_7745c5c3_Err != nil { | 729 | if templ_7745c5c3_Err != nil { | ||
| 730 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 71} | 730 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 72} |
| 731 | } | 731 | } | ||
| 732 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) | 732 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) | ||
| 733 | if templ_7745c5c3_Err != nil { | 733 | if templ_7745c5c3_Err != nil { | ||
| @@ -740,7 +740,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 740 | var templ_7745c5c3_Var39 string | 740 | var templ_7745c5c3_Var39 string | ||
| 741 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When) | 741 | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When) | ||
| 742 | if templ_7745c5c3_Err != nil { | 742 | if templ_7745c5c3_Err != nil { | ||
| 743 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 89} | 743 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 90} |
| 744 | } | 744 | } | ||
| 745 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | 745 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | ||
| 746 | if templ_7745c5c3_Err != nil { | 746 | if templ_7745c5c3_Err != nil { | ||
| @@ -763,7 +763,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 763 | var templ_7745c5c3_Var40 string | 763 | var templ_7745c5c3_Var40 string | ||
| 764 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt) | 764 | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt) | ||
| 765 | if templ_7745c5c3_Err != nil { | 765 | if templ_7745c5c3_Err != nil { | ||
| 766 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 137, Col: 37} | 766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 38} |
| 767 | } | 767 | } | ||
| 768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | 768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | ||
| 769 | if templ_7745c5c3_Err != nil { | 769 | if templ_7745c5c3_Err != nil { | ||
| @@ -776,7 +776,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 776 | var templ_7745c5c3_Var41 string | 776 | var templ_7745c5c3_Var41 string | ||
| 777 | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated) | 777 | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated) | ||
| 778 | if templ_7745c5c3_Err != nil { | 778 | if templ_7745c5c3_Err != nil { | ||
| 779 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 137, Col: 51} | 779 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 52} |
| 780 | } | 780 | } | ||
| 781 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | 781 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | ||
| 782 | if templ_7745c5c3_Err != nil { | 782 | if templ_7745c5c3_Err != nil { | ||
| @@ -791,16 +791,8 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 791 | if templ_7745c5c3_Err != nil { | 791 | if templ_7745c5c3_Err != nil { | ||
| 792 | return templ_7745c5c3_Err | 792 | return templ_7745c5c3_Err | ||
| 793 | } | 793 | } | ||
| 794 | - | templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer) | |||
| 795 | - | if templ_7745c5c3_Err != nil { | |||
| 796 | - | return templ_7745c5c3_Err | |||
| 797 | - | } | |||
| 798 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " ") | |||
| 799 | - | if templ_7745c5c3_Err != nil { | |||
| 800 | - | return templ_7745c5c3_Err | |||
| 801 | - | } | |||
| 802 | if p.Desc != "" { | 794 | if p.Desc != "" { | ||
| 803 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>") | 795 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>") |
| 804 | if templ_7745c5c3_Err != nil { | 796 | if templ_7745c5c3_Err != nil { | ||
| 805 | return templ_7745c5c3_Err | 797 | return templ_7745c5c3_Err | ||
| 806 | } | 798 | } | ||
| @@ -808,11 +800,19 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 808 | if templ_7745c5c3_Err != nil { | 800 | if templ_7745c5c3_Err != nil { | ||
| 809 | return templ_7745c5c3_Err | 801 | return templ_7745c5c3_Err | ||
| 810 | } | 802 | } | ||
| 811 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</article>") | 803 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</article>") |
| 812 | if templ_7745c5c3_Err != nil { | 804 | if templ_7745c5c3_Err != nil { | ||
| 813 | return templ_7745c5c3_Err | 805 | return templ_7745c5c3_Err | ||
| 814 | } | 806 | } | ||
| 815 | } | 807 | } | ||
| 808 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</section>") | |||
| 809 | + | if templ_7745c5c3_Err != nil { | |||
| 810 | + | return templ_7745c5c3_Err | |||
| 811 | + | } | |||
| 812 | + | templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer) | |||
| 813 | + | if templ_7745c5c3_Err != nil { | |||
| 814 | + | return templ_7745c5c3_Err | |||
| 815 | + | } | |||
| 816 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ") | 816 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ") | ||
| 817 | if templ_7745c5c3_Err != nil { | 817 | if templ_7745c5c3_Err != nil { | ||
| 818 | return templ_7745c5c3_Err | 818 | return templ_7745c5c3_Err | ||
| @@ -825,7 +825,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 825 | var templ_7745c5c3_Var42 templ.SafeURL | 825 | var templ_7745c5c3_Var42 templ.SafeURL | ||
| 826 | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref)) | 826 | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref)) | ||
| 827 | if templ_7745c5c3_Err != nil { | 827 | if templ_7745c5c3_Err != nil { | ||
| 828 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 150, Col: 42} | 828 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 152, Col: 42} |
| 829 | } | 829 | } | ||
| 830 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | 830 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | ||
| 831 | if templ_7745c5c3_Err != nil { | 831 | if templ_7745c5c3_Err != nil { | ||
| @@ -848,7 +848,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 848 | var templ_7745c5c3_Var43 string | 848 | var templ_7745c5c3_Var43 string | ||
| 849 | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | 849 | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | ||
| 850 | if templ_7745c5c3_Err != nil { | 850 | if templ_7745c5c3_Err != nil { | ||
| 851 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 154, Col: 49} | 851 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 49} |
| 852 | } | 852 | } | ||
| 853 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | 853 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | ||
| 854 | if templ_7745c5c3_Err != nil { | 854 | if templ_7745c5c3_Err != nil { | ||
| @@ -861,7 +861,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 861 | var templ_7745c5c3_Var44 templ.SafeURL | 861 | var templ_7745c5c3_Var44 templ.SafeURL | ||
| 862 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref)) | 862 | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref)) | ||
| 863 | if templ_7745c5c3_Err != nil { | 863 | if templ_7745c5c3_Err != nil { | ||
| 864 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 44} | 864 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 158, Col: 44} |
| 865 | } | 865 | } | ||
| 866 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | 866 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | ||
| 867 | if templ_7745c5c3_Err != nil { | 867 | if templ_7745c5c3_Err != nil { | ||
| @@ -884,7 +884,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 884 | var templ_7745c5c3_Var45 string | 884 | var templ_7745c5c3_Var45 string | ||
| 885 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort) | 885 | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort) | ||
| 886 | if templ_7745c5c3_Err != nil { | 886 | if templ_7745c5c3_Err != nil { | ||
| 887 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 64} | 887 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 162, Col: 64} |
| 888 | } | 888 | } | ||
| 889 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | 889 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | ||
| 890 | if templ_7745c5c3_Err != nil { | 890 | if templ_7745c5c3_Err != nil { | ||
| @@ -897,7 +897,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 897 | var templ_7745c5c3_Var46 string | 897 | var templ_7745c5c3_Var46 string | ||
| 898 | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort) | 898 | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort) | ||
| 899 | if templ_7745c5c3_Err != nil { | 899 | if templ_7745c5c3_Err != nil { | ||
| 900 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 90} | 900 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 162, Col: 90} |
| 901 | } | 901 | } | ||
| 902 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | 902 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | ||
| 903 | if templ_7745c5c3_Err != nil { | 903 | if templ_7745c5c3_Err != nil { | ||
| @@ -920,7 +920,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 920 | var templ_7745c5c3_Var47 string | 920 | var templ_7745c5c3_Var47 string | ||
| 921 | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote) | 921 | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote) | ||
| 922 | if templ_7745c5c3_Err != nil { | 922 | if templ_7745c5c3_Err != nil { | ||
| 923 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 164, Col: 34} | 923 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 34} |
| 924 | } | 924 | } | ||
| 925 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | 925 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | ||
| 926 | if templ_7745c5c3_Err != nil { | 926 | if templ_7745c5c3_Err != nil { | ||
| @@ -961,7 +961,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 961 | var templ_7745c5c3_Var48 string | 961 | var templ_7745c5c3_Var48 string | ||
| 962 | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | 962 | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | ||
| 963 | if templ_7745c5c3_Err != nil { | 963 | if templ_7745c5c3_Err != nil { | ||
| 964 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 174, Col: 29} | 964 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 176, Col: 29} |
| 965 | } | 965 | } | ||
| 966 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | 966 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | ||
| 967 | if templ_7745c5c3_Err != nil { | 967 | if templ_7745c5c3_Err != nil { | ||
| @@ -984,7 +984,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 984 | var templ_7745c5c3_Var49 string | 984 | var templ_7745c5c3_Var49 string | ||
| 985 | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | 985 | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | ||
| 986 | if templ_7745c5c3_Err != nil { | 986 | if templ_7745c5c3_Err != nil { | ||
| 987 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 180, Col: 22} | 987 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 182, Col: 22} |
| 988 | } | 988 | } | ||
| 989 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | 989 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | ||
| 990 | if templ_7745c5c3_Err != nil { | 990 | if templ_7745c5c3_Err != nil { | ||
| @@ -1002,7 +1002,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 1002 | var templ_7745c5c3_Var50 string | 1002 | var templ_7745c5c3_Var50 string | ||
| 1003 | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | 1003 | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | ||
| 1004 | if templ_7745c5c3_Err != nil { | 1004 | if templ_7745c5c3_Err != nil { | ||
| 1005 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 182, Col: 22} | 1005 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 184, Col: 22} |
| 1006 | } | 1006 | } | ||
| 1007 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | 1007 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | ||
| 1008 | if templ_7745c5c3_Err != nil { | 1008 | if templ_7745c5c3_Err != nil { | ||
| @@ -1042,7 +1042,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 1042 | var templ_7745c5c3_Var51 string | 1042 | var templ_7745c5c3_Var51 string | ||
| 1043 | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | 1043 | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | ||
| 1044 | if templ_7745c5c3_Err != nil { | 1044 | if templ_7745c5c3_Err != nil { | ||
| 1045 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 195, Col: 97} | 1045 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 97} |
| 1046 | } | 1046 | } | ||
| 1047 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | 1047 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | ||
| 1048 | if templ_7745c5c3_Err != nil { | 1048 | if templ_7745c5c3_Err != nil { | ||
| @@ -1055,7 +1055,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||||
| 1055 | var templ_7745c5c3_Var52 string | 1055 | var templ_7745c5c3_Var52 string | ||
| 1056 | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | 1056 | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | ||
| 1057 | if templ_7745c5c3_Err != nil { | 1057 | if templ_7745c5c3_Err != nil { | ||
| 1058 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 195, Col: 126} | 1058 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 126} |
| 1059 | } | 1059 | } | ||
| 1060 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | 1060 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | ||
| 1061 | if templ_7745c5c3_Err != nil { | 1061 | if templ_7745c5c3_Err != nil { | ||
| @@ -1136,7 +1136,7 @@ func branchLink(name, href string) templ.Component { | |||||
| 1136 | var templ_7745c5c3_Var54 templ.SafeURL | 1136 | var templ_7745c5c3_Var54 templ.SafeURL | ||
| 1137 | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | 1137 | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | ||
| 1138 | if templ_7745c5c3_Err != nil { | 1138 | if templ_7745c5c3_Err != nil { | ||
| 1139 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 213, Col: 31} | 1139 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 31} |
| 1140 | } | 1140 | } | ||
| 1141 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | 1141 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | ||
| 1142 | if templ_7745c5c3_Err != nil { | 1142 | if templ_7745c5c3_Err != nil { | ||
| @@ -1149,7 +1149,7 @@ func branchLink(name, href string) templ.Component { | |||||
| 1149 | var templ_7745c5c3_Var55 string | 1149 | var templ_7745c5c3_Var55 string | ||
| 1150 | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(name) | 1150 | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(name) | ||
| 1151 | if templ_7745c5c3_Err != nil { | 1151 | if templ_7745c5c3_Err != nil { | ||
| 1152 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 213, Col: 40} | 1152 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 40} |
| 1153 | } | 1153 | } | ||
| 1154 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | 1154 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | ||
| 1155 | if templ_7745c5c3_Err != nil { | 1155 | if templ_7745c5c3_Err != nil { | ||
| @@ -1163,7 +1163,7 @@ func branchLink(name, href string) templ.Component { | |||||
| 1163 | var templ_7745c5c3_Var56 string | 1163 | var templ_7745c5c3_Var56 string | ||
| 1164 | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(name) | 1164 | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(name) | ||
| 1165 | if templ_7745c5c3_Err != nil { | 1165 | if templ_7745c5c3_Err != nil { | ||
| 1166 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 8} | 1166 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 8} |
| 1167 | } | 1167 | } | ||
| 1168 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | 1168 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | ||
| 1169 | if templ_7745c5c3_Err != nil { | 1169 | if templ_7745c5c3_Err != nil { | ||
| @@ -1244,7 +1244,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1244 | var templ_7745c5c3_Var59 string | 1244 | var templ_7745c5c3_Var59 string | ||
| 1245 | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | 1245 | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | ||
| 1246 | if templ_7745c5c3_Err != nil { | 1246 | if templ_7745c5c3_Err != nil { | ||
| 1247 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 42} | 1247 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 42} |
| 1248 | } | 1248 | } | ||
| 1249 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59) | 1249 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59) | ||
| 1250 | if templ_7745c5c3_Err != nil { | 1250 | if templ_7745c5c3_Err != nil { | ||
| @@ -1257,7 +1257,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1257 | var templ_7745c5c3_Var60 string | 1257 | var templ_7745c5c3_Var60 string | ||
| 1258 | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | 1258 | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | ||
| 1259 | if templ_7745c5c3_Err != nil { | 1259 | if templ_7745c5c3_Err != nil { | ||
| 1260 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 81} | 1260 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 81} |
| 1261 | } | 1261 | } | ||
| 1262 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60) | 1262 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60) | ||
| 1263 | if templ_7745c5c3_Err != nil { | 1263 | if templ_7745c5c3_Err != nil { | ||
| @@ -1280,7 +1280,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1280 | var templ_7745c5c3_Var61 string | 1280 | var templ_7745c5c3_Var61 string | ||
| 1281 | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | 1281 | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | ||
| 1282 | if templ_7745c5c3_Err != nil { | 1282 | if templ_7745c5c3_Err != nil { | ||
| 1283 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 33} | 1283 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 33} |
| 1284 | } | 1284 | } | ||
| 1285 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | 1285 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | ||
| 1286 | if templ_7745c5c3_Err != nil { | 1286 | if templ_7745c5c3_Err != nil { | ||
| @@ -1294,7 +1294,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1294 | var templ_7745c5c3_Var62 string | 1294 | var templ_7745c5c3_Var62 string | ||
| 1295 | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | 1295 | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | ||
| 1296 | if templ_7745c5c3_Err != nil { | 1296 | if templ_7745c5c3_Err != nil { | ||
| 1297 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 9} | 1297 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 9} |
| 1298 | } | 1298 | } | ||
| 1299 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | 1299 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | ||
| 1300 | if templ_7745c5c3_Err != nil { | 1300 | if templ_7745c5c3_Err != nil { | ||
| @@ -1307,7 +1307,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1307 | var templ_7745c5c3_Var63 string | 1307 | var templ_7745c5c3_Var63 string | ||
| 1308 | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 1308 | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | ||
| 1309 | if templ_7745c5c3_Err != nil { | 1309 | if templ_7745c5c3_Err != nil { | ||
| 1310 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 49} | 1310 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 49} |
| 1311 | } | 1311 | } | ||
| 1312 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | 1312 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | ||
| 1313 | if templ_7745c5c3_Err != nil { | 1313 | if templ_7745c5c3_Err != nil { | ||
| @@ -1320,7 +1320,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1320 | var templ_7745c5c3_Var64 string | 1320 | var templ_7745c5c3_Var64 string | ||
| 1321 | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | 1321 | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | ||
| 1322 | if templ_7745c5c3_Err != nil { | 1322 | if templ_7745c5c3_Err != nil { | ||
| 1323 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 65} | 1323 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 65} |
| 1324 | } | 1324 | } | ||
| 1325 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) | 1325 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) | ||
| 1326 | if templ_7745c5c3_Err != nil { | 1326 | if templ_7745c5c3_Err != nil { | ||
| @@ -1333,7 +1333,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1333 | var templ_7745c5c3_Var65 string | 1333 | var templ_7745c5c3_Var65 string | ||
| 1334 | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | 1334 | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | ||
| 1335 | if templ_7745c5c3_Err != nil { | 1335 | if templ_7745c5c3_Err != nil { | ||
| 1336 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 76} | 1336 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 76} |
| 1337 | } | 1337 | } | ||
| 1338 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | 1338 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | ||
| 1339 | if templ_7745c5c3_Err != nil { | 1339 | if templ_7745c5c3_Err != nil { | ||
| @@ -1348,7 +1348,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1348 | var templ_7745c5c3_Var66 string | 1348 | var templ_7745c5c3_Var66 string | ||
| 1349 | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | 1349 | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | ||
| 1350 | if templ_7745c5c3_Err != nil { | 1350 | if templ_7745c5c3_Err != nil { | ||
| 1351 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 9} | 1351 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 9} |
| 1352 | } | 1352 | } | ||
| 1353 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | 1353 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | ||
| 1354 | if templ_7745c5c3_Err != nil { | 1354 | if templ_7745c5c3_Err != nil { | ||
| @@ -1363,7 +1363,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1363 | var templ_7745c5c3_Var67 string | 1363 | var templ_7745c5c3_Var67 string | ||
| 1364 | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | 1364 | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | ||
| 1365 | if templ_7745c5c3_Err != nil { | 1365 | if templ_7745c5c3_Err != nil { | ||
| 1366 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9} | 1366 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 246, Col: 9} |
| 1367 | } | 1367 | } | ||
| 1368 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) | 1368 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) | ||
| 1369 | if templ_7745c5c3_Err != nil { | 1369 | if templ_7745c5c3_Err != nil { | ||
| @@ -1378,7 +1378,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1378 | var templ_7745c5c3_Var68 string | 1378 | var templ_7745c5c3_Var68 string | ||
| 1379 | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | 1379 | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | ||
| 1380 | if templ_7745c5c3_Err != nil { | 1380 | if templ_7745c5c3_Err != nil { | ||
| 1381 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9} | 1381 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9} |
| 1382 | } | 1382 | } | ||
| 1383 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | 1383 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | ||
| 1384 | if templ_7745c5c3_Err != nil { | 1384 | if templ_7745c5c3_Err != nil { | ||
| @@ -1391,7 +1391,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1391 | var templ_7745c5c3_Var69 string | 1391 | var templ_7745c5c3_Var69 string | ||
| 1392 | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | 1392 | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | ||
| 1393 | if templ_7745c5c3_Err != nil { | 1393 | if templ_7745c5c3_Err != nil { | ||
| 1394 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 47} | 1394 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 47} |
| 1395 | } | 1395 | } | ||
| 1396 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | 1396 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | ||
| 1397 | if templ_7745c5c3_Err != nil { | 1397 | if templ_7745c5c3_Err != nil { | ||
| @@ -1466,7 +1466,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1466 | var templ_7745c5c3_Var70 string | 1466 | var templ_7745c5c3_Var70 string | ||
| 1467 | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict) | 1467 | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict) | ||
| 1468 | if templ_7745c5c3_Err != nil { | 1468 | if templ_7745c5c3_Err != nil { | ||
| 1469 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 271, Col: 36} | 1469 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 36} |
| 1470 | } | 1470 | } | ||
| 1471 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | 1471 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | ||
| 1472 | if templ_7745c5c3_Err != nil { | 1472 | if templ_7745c5c3_Err != nil { | ||
| @@ -1485,7 +1485,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1485 | var templ_7745c5c3_Var71 string | 1485 | var templ_7745c5c3_Var71 string | ||
| 1486 | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range) | 1486 | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range) | ||
| 1487 | if templ_7745c5c3_Err != nil { | 1487 | if templ_7745c5c3_Err != nil { | ||
| 1488 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 31} | 1488 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 31} |
| 1489 | } | 1489 | } | ||
| 1490 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | 1490 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | ||
| 1491 | if templ_7745c5c3_Err != nil { | 1491 | if templ_7745c5c3_Err != nil { | ||
| @@ -1504,7 +1504,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1504 | var templ_7745c5c3_Var72 string | 1504 | var templ_7745c5c3_Var72 string | ||
| 1505 | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote) | 1505 | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote) | ||
| 1506 | if templ_7745c5c3_Err != nil { | 1506 | if templ_7745c5c3_Err != nil { | ||
| 1507 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 277, Col: 65} | 1507 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 279, Col: 65} |
| 1508 | } | 1508 | } | ||
| 1509 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72) | 1509 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72) | ||
| 1510 | if templ_7745c5c3_Err != nil { | 1510 | if templ_7745c5c3_Err != nil { | ||
| @@ -1523,7 +1523,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1523 | var templ_7745c5c3_Var73 templ.SafeURL | 1523 | var templ_7745c5c3_Var73 templ.SafeURL | ||
| 1524 | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action)) | 1524 | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action)) | ||
| 1525 | if templ_7745c5c3_Err != nil { | 1525 | if templ_7745c5c3_Err != nil { | ||
| 1526 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 280, Col: 92} | 1526 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 92} |
| 1527 | } | 1527 | } | ||
| 1528 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | 1528 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | ||
| 1529 | if templ_7745c5c3_Err != nil { | 1529 | if templ_7745c5c3_Err != nil { | ||
| @@ -1536,7 +1536,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1536 | var templ_7745c5c3_Var74 string | 1536 | var templ_7745c5c3_Var74 string | ||
| 1537 | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From)) | 1537 | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From)) | ||
| 1538 | if templ_7745c5c3_Err != nil { | 1538 | if templ_7745c5c3_Err != nil { | ||
| 1539 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 280, Col: 153} | 1539 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 153} |
| 1540 | } | 1540 | } | ||
| 1541 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | 1541 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | ||
| 1542 | if templ_7745c5c3_Err != nil { | 1542 | if templ_7745c5c3_Err != nil { | ||
| @@ -1549,7 +1549,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1549 | var templ_7745c5c3_Var75 string | 1549 | var templ_7745c5c3_Var75 string | ||
| 1550 | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text) | 1550 | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text) | ||
| 1551 | if templ_7745c5c3_Err != nil { | 1551 | if templ_7745c5c3_Err != nil { | ||
| 1552 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 280, Col: 209} | 1552 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 209} |
| 1553 | } | 1553 | } | ||
| 1554 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | 1554 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | ||
| 1555 | if templ_7745c5c3_Err != nil { | 1555 | if templ_7745c5c3_Err != nil { | ||
| @@ -1568,7 +1568,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1568 | var templ_7745c5c3_Var76 templ.SafeURL | 1568 | var templ_7745c5c3_Var76 templ.SafeURL | ||
| 1569 | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action)) | 1569 | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action)) | ||
| 1570 | if templ_7745c5c3_Err != nil { | 1570 | if templ_7745c5c3_Err != nil { | ||
| 1571 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 283, Col: 96} | 1571 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 96} |
| 1572 | } | 1572 | } | ||
| 1573 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | 1573 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) | ||
| 1574 | if templ_7745c5c3_Err != nil { | 1574 | if templ_7745c5c3_Err != nil { | ||
| @@ -1581,7 +1581,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1581 | var templ_7745c5c3_Var77 string | 1581 | var templ_7745c5c3_Var77 string | ||
| 1582 | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From)) | 1582 | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From)) | ||
| 1583 | if templ_7745c5c3_Err != nil { | 1583 | if templ_7745c5c3_Err != nil { | ||
| 1584 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 283, Col: 159} | 1584 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 159} |
| 1585 | } | 1585 | } | ||
| 1586 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) | 1586 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77) | ||
| 1587 | if templ_7745c5c3_Err != nil { | 1587 | if templ_7745c5c3_Err != nil { | ||
| @@ -1620,7 +1620,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1620 | var templ_7745c5c3_Var78 templ.SafeURL | 1620 | var templ_7745c5c3_Var78 templ.SafeURL | ||
| 1621 | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | 1621 | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | ||
| 1622 | if templ_7745c5c3_Err != nil { | 1622 | if templ_7745c5c3_Err != nil { | ||
| 1623 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 77} | 1623 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 77} |
| 1624 | } | 1624 | } | ||
| 1625 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | 1625 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) | ||
| 1626 | if templ_7745c5c3_Err != nil { | 1626 | if templ_7745c5c3_Err != nil { | ||
| @@ -1633,7 +1633,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1633 | var templ_7745c5c3_Var79 string | 1633 | var templ_7745c5c3_Var79 string | ||
| 1634 | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | 1634 | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | ||
| 1635 | if templ_7745c5c3_Err != nil { | 1635 | if templ_7745c5c3_Err != nil { | ||
| 1636 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 74} | 1636 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 74} |
| 1637 | } | 1637 | } | ||
| 1638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | 1638 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | ||
| 1639 | if templ_7745c5c3_Err != nil { | 1639 | if templ_7745c5c3_Err != nil { | ||
| @@ -1651,7 +1651,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1651 | var templ_7745c5c3_Var80 string | 1651 | var templ_7745c5c3_Var80 string | ||
| 1652 | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From)) | 1652 | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From)) | ||
| 1653 | if templ_7745c5c3_Err != nil { | 1653 | if templ_7745c5c3_Err != nil { | ||
| 1654 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 66} | 1654 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 299, Col: 66} |
| 1655 | } | 1655 | } | ||
| 1656 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | 1656 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | ||
| 1657 | if templ_7745c5c3_Err != nil { | 1657 | if templ_7745c5c3_Err != nil { | ||
| @@ -1669,7 +1669,7 @@ func commentView(c CommentView, suffix string) templ.Component { | |||||
| 1669 | var templ_7745c5c3_Var81 string | 1669 | var templ_7745c5c3_Var81 string | ||
| 1670 | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | 1670 | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | ||
| 1671 | if templ_7745c5c3_Err != nil { | 1671 | if templ_7745c5c3_Err != nil { | ||
| 1672 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 299, Col: 55} | 1672 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 301, Col: 55} |
| 1673 | } | 1673 | } | ||
| 1674 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | 1674 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) | ||
| 1675 | if templ_7745c5c3_Err != nil { | 1675 | if templ_7745c5c3_Err != nil { | ||
| @@ -1724,7 +1724,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1724 | var templ_7745c5c3_Var83 templ.SafeURL | 1724 | var templ_7745c5c3_Var83 templ.SafeURL | ||
| 1725 | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | 1725 | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | ||
| 1726 | if templ_7745c5c3_Err != nil { | 1726 | if templ_7745c5c3_Err != nil { | ||
| 1727 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 314, Col: 67} | 1727 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 316, Col: 67} |
| 1728 | } | 1728 | } | ||
| 1729 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) | 1729 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) | ||
| 1730 | if templ_7745c5c3_Err != nil { | 1730 | if templ_7745c5c3_Err != nil { | ||
| @@ -1737,7 +1737,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1737 | var templ_7745c5c3_Var84 string | 1737 | var templ_7745c5c3_Var84 string | ||
| 1738 | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | 1738 | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | ||
| 1739 | if templ_7745c5c3_Err != nil { | 1739 | if templ_7745c5c3_Err != nil { | ||
| 1740 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 28} | 1740 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 28} |
| 1741 | } | 1741 | } | ||
| 1742 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) | 1742 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84) | ||
| 1743 | if templ_7745c5c3_Err != nil { | 1743 | if templ_7745c5c3_Err != nil { | ||
| @@ -1750,7 +1750,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1750 | var templ_7745c5c3_Var85 string | 1750 | var templ_7745c5c3_Var85 string | ||
| 1751 | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | 1751 | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | ||
| 1752 | if templ_7745c5c3_Err != nil { | 1752 | if templ_7745c5c3_Err != nil { | ||
| 1753 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 40} | 1753 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 40} |
| 1754 | } | 1754 | } | ||
| 1755 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85)) | 1755 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85)) | ||
| 1756 | if templ_7745c5c3_Err != nil { | 1756 | if templ_7745c5c3_Err != nil { | ||
| @@ -1763,7 +1763,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1763 | var templ_7745c5c3_Var86 string | 1763 | var templ_7745c5c3_Var86 string | ||
| 1764 | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | 1764 | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | ||
| 1765 | if templ_7745c5c3_Err != nil { | 1765 | if templ_7745c5c3_Err != nil { | ||
| 1766 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 21} | 1766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 21} |
| 1767 | } | 1767 | } | ||
| 1768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86) | 1768 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86) | ||
| 1769 | if templ_7745c5c3_Err != nil { | 1769 | if templ_7745c5c3_Err != nil { | ||
| @@ -1802,7 +1802,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1802 | var templ_7745c5c3_Var87 string | 1802 | var templ_7745c5c3_Var87 string | ||
| 1803 | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | 1803 | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | ||
| 1804 | if templ_7745c5c3_Err != nil { | 1804 | if templ_7745c5c3_Err != nil { | ||
| 1805 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 332, Col: 50} | 1805 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 50} |
| 1806 | } | 1806 | } | ||
| 1807 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87) | 1807 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87) | ||
| 1808 | if templ_7745c5c3_Err != nil { | 1808 | if templ_7745c5c3_Err != nil { | ||
| @@ -1815,7 +1815,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1815 | var templ_7745c5c3_Var88 string | 1815 | var templ_7745c5c3_Var88 string | ||
| 1816 | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | 1816 | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | ||
| 1817 | if templ_7745c5c3_Err != nil { | 1817 | if templ_7745c5c3_Err != nil { | ||
| 1818 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 333, Col: 50} | 1818 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 50} |
| 1819 | } | 1819 | } | ||
| 1820 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88) | 1820 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88) | ||
| 1821 | if templ_7745c5c3_Err != nil { | 1821 | if templ_7745c5c3_Err != nil { | ||
| @@ -1828,7 +1828,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1828 | var templ_7745c5c3_Var89 string | 1828 | var templ_7745c5c3_Var89 string | ||
| 1829 | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | 1829 | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | ||
| 1830 | if templ_7745c5c3_Err != nil { | 1830 | if templ_7745c5c3_Err != nil { | ||
| 1831 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 50} | 1831 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 50} |
| 1832 | } | 1832 | } | ||
| 1833 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89) | 1833 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89) | ||
| 1834 | if templ_7745c5c3_Err != nil { | 1834 | if templ_7745c5c3_Err != nil { | ||
| @@ -1841,7 +1841,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1841 | var templ_7745c5c3_Var90 string | 1841 | var templ_7745c5c3_Var90 string | ||
| 1842 | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine) | 1842 | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine) | ||
| 1843 | if templ_7745c5c3_Err != nil { | 1843 | if templ_7745c5c3_Err != nil { | ||
| 1844 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 86} | 1844 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 337, Col: 86} |
| 1845 | } | 1845 | } | ||
| 1846 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90) | 1846 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90) | ||
| 1847 | if templ_7745c5c3_Err != nil { | 1847 | if templ_7745c5c3_Err != nil { | ||
| @@ -1860,7 +1860,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1860 | var templ_7745c5c3_Var91 string | 1860 | var templ_7745c5c3_Var91 string | ||
| 1861 | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo) | 1861 | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo) | ||
| 1862 | if templ_7745c5c3_Err != nil { | 1862 | if templ_7745c5c3_Err != nil { | ||
| 1863 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 338, Col: 57} | 1863 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 340, Col: 57} |
| 1864 | } | 1864 | } | ||
| 1865 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91) | 1865 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91) | ||
| 1866 | if templ_7745c5c3_Err != nil { | 1866 | if templ_7745c5c3_Err != nil { | ||
| @@ -1885,7 +1885,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1885 | var templ_7745c5c3_Var92 string | 1885 | var templ_7745c5c3_Var92 string | ||
| 1886 | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From)) | 1886 | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From)) | ||
| 1887 | if templ_7745c5c3_Err != nil { | 1887 | if templ_7745c5c3_Err != nil { | ||
| 1888 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 344, Col: 56} | 1888 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 56} |
| 1889 | } | 1889 | } | ||
| 1890 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92) | 1890 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92) | ||
| 1891 | if templ_7745c5c3_Err != nil { | 1891 | if templ_7745c5c3_Err != nil { | ||
| @@ -1908,7 +1908,7 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||||
| 1908 | var templ_7745c5c3_Var93 templ.SafeURL | 1908 | var templ_7745c5c3_Var93 templ.SafeURL | ||
| 1909 | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | 1909 | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | ||
| 1910 | if templ_7745c5c3_Err != nil { | 1910 | if templ_7745c5c3_Err != nil { | ||
| 1911 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 49} | 1911 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 351, Col: 49} |
| 1912 | } | 1912 | } | ||
| 1913 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93)) | 1913 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93)) | ||
| 1914 | if templ_7745c5c3_Err != nil { | 1914 | if templ_7745c5c3_Err != nil { | ||
| @@ -1955,7 +1955,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 1955 | var templ_7745c5c3_Var95 string | 1955 | var templ_7745c5c3_Var95 string | ||
| 1956 | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | 1956 | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | ||
| 1957 | if templ_7745c5c3_Err != nil { | 1957 | if templ_7745c5c3_Err != nil { | ||
| 1958 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 358, Col: 49} | 1958 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 360, Col: 49} |
| 1959 | } | 1959 | } | ||
| 1960 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | 1960 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | ||
| 1961 | if templ_7745c5c3_Err != nil { | 1961 | if templ_7745c5c3_Err != nil { | ||
| @@ -1986,7 +1986,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 1986 | var templ_7745c5c3_Var96 templ.SafeURL | 1986 | var templ_7745c5c3_Var96 templ.SafeURL | ||
| 1987 | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href)) | 1987 | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href)) | ||
| 1988 | if templ_7745c5c3_Err != nil { | 1988 | if templ_7745c5c3_Err != nil { | ||
| 1989 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 371, Col: 37} | 1989 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 37} |
| 1990 | } | 1990 | } | ||
| 1991 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | 1991 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | ||
| 1992 | if templ_7745c5c3_Err != nil { | 1992 | if templ_7745c5c3_Err != nil { | ||
| @@ -1999,7 +1999,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 1999 | var templ_7745c5c3_Var97 string | 1999 | var templ_7745c5c3_Var97 string | ||
| 2000 | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author) | 2000 | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author) | ||
| 2001 | if templ_7745c5c3_Err != nil { | 2001 | if templ_7745c5c3_Err != nil { | ||
| 2002 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 371, Col: 50} | 2002 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 50} |
| 2003 | } | 2003 | } | ||
| 2004 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) | 2004 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) | ||
| 2005 | if templ_7745c5c3_Err != nil { | 2005 | if templ_7745c5c3_Err != nil { | ||
| @@ -2012,7 +2012,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2012 | var templ_7745c5c3_Var98 string | 2012 | var templ_7745c5c3_Var98 string | ||
| 2013 | templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label) | 2013 | templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label) | ||
| 2014 | if templ_7745c5c3_Err != nil { | 2014 | if templ_7745c5c3_Err != nil { | ||
| 2015 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 371, Col: 63} | 2015 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 63} |
| 2016 | } | 2016 | } | ||
| 2017 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98)) | 2017 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98)) | ||
| 2018 | if templ_7745c5c3_Err != nil { | 2018 | if templ_7745c5c3_Err != nil { | ||
| @@ -2049,7 +2049,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2049 | var templ_7745c5c3_Var99 string | 2049 | var templ_7745c5c3_Var99 string | ||
| 2050 | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | 2050 | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | ||
| 2051 | if templ_7745c5c3_Err != nil { | 2051 | if templ_7745c5c3_Err != nil { | ||
| 2052 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 71} | 2052 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 384, Col: 71} |
| 2053 | } | 2053 | } | ||
| 2054 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | 2054 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | ||
| 2055 | if templ_7745c5c3_Err != nil { | 2055 | if templ_7745c5c3_Err != nil { | ||
| @@ -2077,7 +2077,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2077 | var templ_7745c5c3_Var100 templ.SafeURL | 2077 | var templ_7745c5c3_Var100 templ.SafeURL | ||
| 2078 | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | 2078 | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | ||
| 2079 | if templ_7745c5c3_Err != nil { | 2079 | if templ_7745c5c3_Err != nil { | ||
| 2080 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 36} | 2080 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 36} |
| 2081 | } | 2081 | } | ||
| 2082 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | 2082 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | ||
| 2083 | if templ_7745c5c3_Err != nil { | 2083 | if templ_7745c5c3_Err != nil { | ||
| @@ -2090,7 +2090,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2090 | var templ_7745c5c3_Var101 string | 2090 | var templ_7745c5c3_Var101 string | ||
| 2091 | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | 2091 | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | ||
| 2092 | if templ_7745c5c3_Err != nil { | 2092 | if templ_7745c5c3_Err != nil { | ||
| 2093 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 48} | 2093 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 48} |
| 2094 | } | 2094 | } | ||
| 2095 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | 2095 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | ||
| 2096 | if templ_7745c5c3_Err != nil { | 2096 | if templ_7745c5c3_Err != nil { | ||
| @@ -2103,7 +2103,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2103 | var templ_7745c5c3_Var102 string | 2103 | var templ_7745c5c3_Var102 string | ||
| 2104 | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | 2104 | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | ||
| 2105 | if templ_7745c5c3_Err != nil { | 2105 | if templ_7745c5c3_Err != nil { | ||
| 2106 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 32} | 2106 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 389, Col: 32} |
| 2107 | } | 2107 | } | ||
| 2108 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) | 2108 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) | ||
| 2109 | if templ_7745c5c3_Err != nil { | 2109 | if templ_7745c5c3_Err != nil { | ||
| @@ -2126,7 +2126,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2126 | var templ_7745c5c3_Var103 string | 2126 | var templ_7745c5c3_Var103 string | ||
| 2127 | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | 2127 | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | ||
| 2128 | if templ_7745c5c3_Err != nil { | 2128 | if templ_7745c5c3_Err != nil { | ||
| 2129 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 392, Col: 16} | 2129 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 16} |
| 2130 | } | 2130 | } | ||
| 2131 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) | 2131 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) | ||
| 2132 | if templ_7745c5c3_Err != nil { | 2132 | if templ_7745c5c3_Err != nil { | ||
| @@ -2140,7 +2140,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2140 | var templ_7745c5c3_Var104 string | 2140 | var templ_7745c5c3_Var104 string | ||
| 2141 | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 2141 | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | ||
| 2142 | if templ_7745c5c3_Err != nil { | 2142 | if templ_7745c5c3_Err != nil { | ||
| 2143 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 15} | 2143 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 15} |
| 2144 | } | 2144 | } | ||
| 2145 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | 2145 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | ||
| 2146 | if templ_7745c5c3_Err != nil { | 2146 | if templ_7745c5c3_Err != nil { | ||
| @@ -2153,7 +2153,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2153 | var templ_7745c5c3_Var105 string | 2153 | var templ_7745c5c3_Var105 string | ||
| 2154 | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 2154 | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | ||
| 2155 | if templ_7745c5c3_Err != nil { | 2155 | if templ_7745c5c3_Err != nil { | ||
| 2156 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 42} | 2156 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 42} |
| 2157 | } | 2157 | } | ||
| 2158 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105) | 2158 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105) | ||
| 2159 | if templ_7745c5c3_Err != nil { | 2159 | if templ_7745c5c3_Err != nil { | ||
| @@ -2166,7 +2166,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2166 | var templ_7745c5c3_Var106 string | 2166 | var templ_7745c5c3_Var106 string | ||
| 2167 | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | 2167 | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | ||
| 2168 | if templ_7745c5c3_Err != nil { | 2168 | if templ_7745c5c3_Err != nil { | ||
| 2169 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 53} | 2169 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 53} |
| 2170 | } | 2170 | } | ||
| 2171 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) | 2171 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) | ||
| 2172 | if templ_7745c5c3_Err != nil { | 2172 | if templ_7745c5c3_Err != nil { | ||
| @@ -2181,7 +2181,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2181 | var templ_7745c5c3_Var107 string | 2181 | var templ_7745c5c3_Var107 string | ||
| 2182 | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | 2182 | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | ||
| 2183 | if templ_7745c5c3_Err != nil { | 2183 | if templ_7745c5c3_Err != nil { | ||
| 2184 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 18} | 2184 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 18} |
| 2185 | } | 2185 | } | ||
| 2186 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | 2186 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | ||
| 2187 | if templ_7745c5c3_Err != nil { | 2187 | if templ_7745c5c3_Err != nil { | ||
| @@ -2211,7 +2211,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2211 | var templ_7745c5c3_Var108 string | 2211 | var templ_7745c5c3_Var108 string | ||
| 2212 | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | 2212 | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | ||
| 2213 | if templ_7745c5c3_Err != nil { | 2213 | if templ_7745c5c3_Err != nil { | ||
| 2214 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 407, Col: 34} | 2214 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 409, Col: 34} |
| 2215 | } | 2215 | } | ||
| 2216 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) | 2216 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) | ||
| 2217 | if templ_7745c5c3_Err != nil { | 2217 | if templ_7745c5c3_Err != nil { | ||
| @@ -2229,7 +2229,7 @@ func mrRail(p MRPage) templ.Component { | |||||
| 2229 | var templ_7745c5c3_Var109 string | 2229 | var templ_7745c5c3_Var109 string | ||
| 2230 | templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | 2230 | templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | ||
| 2231 | if templ_7745c5c3_Err != nil { | 2231 | if templ_7745c5c3_Err != nil { | ||
| 2232 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 414, Col: 36} | 2232 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 36} |
| 2233 | } | 2233 | } | ||
| 2234 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) | 2234 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) | ||
| 2235 | if templ_7745c5c3_Err != nil { | 2235 | if templ_7745c5c3_Err != nil { | ||
| @@ -2290,7 +2290,7 @@ func mrBar(p MRPage) templ.Component { | |||||
| 2290 | var templ_7745c5c3_Var111 string | 2290 | var templ_7745c5c3_Var111 string | ||
| 2291 | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | 2291 | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | ||
| 2292 | if templ_7745c5c3_Err != nil { | 2292 | if templ_7745c5c3_Err != nil { | ||
| 2293 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 55} | 2293 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 55} |
| 2294 | } | 2294 | } | ||
| 2295 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | 2295 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | ||
| 2296 | if templ_7745c5c3_Err != nil { | 2296 | if templ_7745c5c3_Err != nil { | ||
| @@ -2303,7 +2303,7 @@ func mrBar(p MRPage) templ.Component { | |||||
| 2303 | var templ_7745c5c3_Var112 string | 2303 | var templ_7745c5c3_Var112 string | ||
| 2304 | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | 2304 | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | ||
| 2305 | if templ_7745c5c3_Err != nil { | 2305 | if templ_7745c5c3_Err != nil { | ||
| 2306 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 95} | 2306 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 95} |
| 2307 | } | 2307 | } | ||
| 2308 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | 2308 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | ||
| 2309 | if templ_7745c5c3_Err != nil { | 2309 | if templ_7745c5c3_Err != nil { | ||
| @@ -2316,7 +2316,7 @@ func mrBar(p MRPage) templ.Component { | |||||
| 2316 | var templ_7745c5c3_Var113 string | 2316 | var templ_7745c5c3_Var113 string | ||
| 2317 | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | 2317 | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | ||
| 2318 | if templ_7745c5c3_Err != nil { | 2318 | if templ_7745c5c3_Err != nil { | ||
| 2319 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 140} | 2319 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 140} |
| 2320 | } | 2320 | } | ||
| 2321 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | 2321 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | ||
| 2322 | if templ_7745c5c3_Err != nil { | 2322 | if templ_7745c5c3_Err != nil { | ||
| @@ -2326,15 +2326,7 @@ func mrBar(p MRPage) templ.Component { | |||||
| 2326 | if templ_7745c5c3_Err != nil { | 2326 | if templ_7745c5c3_Err != nil { | ||
| 2327 | return templ_7745c5c3_Err | 2327 | return templ_7745c5c3_Err | ||
| 2328 | } | 2328 | } | ||
| 2329 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | 2329 | + | templ_7745c5c3_Err = diffViewMenu(p.DiffView).Render(ctx, templ_7745c5c3_Buffer) |
| 2330 | - | if templ_7745c5c3_Err != nil { | |||
| 2331 | - | return templ_7745c5c3_Err | |||
| 2332 | - | } | |||
| 2333 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |||
| 2334 | - | if templ_7745c5c3_Err != nil { | |||
| 2335 | - | return templ_7745c5c3_Err | |||
| 2336 | - | } | |||
| 2337 | - | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |||
| 2338 | if templ_7745c5c3_Err != nil { | 2330 | if templ_7745c5c3_Err != nil { | ||
| 2339 | return templ_7745c5c3_Err | 2331 | return templ_7745c5c3_Err | ||
| 2340 | } | 2332 | } | ||
| @@ -2998,15 +2990,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 2998 | if templ_7745c5c3_Err != nil { | 2990 | if templ_7745c5c3_Err != nil { | ||
| 2999 | return templ_7745c5c3_Err | 2991 | return templ_7745c5c3_Err | ||
| 3000 | } | 2992 | } | ||
| 3001 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | 2993 | + | templ_7745c5c3_Err = diffViewMenu(p.DiffView).Render(ctx, templ_7745c5c3_Buffer) |
| 3002 | - | if templ_7745c5c3_Err != nil { | |||
| 3003 | - | return templ_7745c5c3_Err | |||
| 3004 | - | } | |||
| 3005 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |||
| 3006 | - | if templ_7745c5c3_Err != nil { | |||
| 3007 | - | return templ_7745c5c3_Err | |||
| 3008 | - | } | |||
| 3009 | - | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |||
| 3010 | if templ_7745c5c3_Err != nil { | 2994 | if templ_7745c5c3_Err != nil { | ||
| 3011 | return templ_7745c5c3_Err | 2995 | return templ_7745c5c3_Err | ||
| 3012 | } | 2996 | } | ||
| @@ -3032,7 +3016,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 3032 | var templ_7745c5c3_Var148 string | 3016 | var templ_7745c5c3_Var148 string | ||
| 3033 | templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | 3017 | templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | ||
| 3034 | if templ_7745c5c3_Err != nil { | 3018 | if templ_7745c5c3_Err != nil { | ||
| 3035 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 23} | 3019 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 576, Col: 23} |
| 3036 | } | 3020 | } | ||
| 3037 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148) | 3021 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148) | ||
| 3038 | if templ_7745c5c3_Err != nil { | 3022 | if templ_7745c5c3_Err != nil { | ||
| @@ -3050,7 +3034,7 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 3050 | var templ_7745c5c3_Var149 string | 3034 | var templ_7745c5c3_Var149 string | ||
| 3051 | templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | 3035 | templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | ||
| 3052 | if templ_7745c5c3_Err != nil { | 3036 | if templ_7745c5c3_Err != nil { | ||
| 3053 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 580, Col: 23} | 3037 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 23} |
| 3054 | } | 3038 | } | ||
| 3055 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149) | 3039 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149) | ||
| 3056 | if templ_7745c5c3_Err != nil { | 3040 | if templ_7745c5c3_Err != nil { | ||
| @@ -3120,7 +3104,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3120 | var templ_7745c5c3_Var151 string | 3104 | var templ_7745c5c3_Var151 string | ||
| 3121 | templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | 3105 | templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | ||
| 3122 | if templ_7745c5c3_Err != nil { | 3106 | if templ_7745c5c3_Err != nil { | ||
| 3123 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 67} | 3107 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 67} |
| 3124 | } | 3108 | } | ||
| 3125 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151)) | 3109 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151)) | ||
| 3126 | if templ_7745c5c3_Err != nil { | 3110 | if templ_7745c5c3_Err != nil { | ||
| @@ -3138,7 +3122,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3138 | var templ_7745c5c3_Var152 templ.SafeURL | 3122 | var templ_7745c5c3_Var152 templ.SafeURL | ||
| 3139 | templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | 3123 | templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | ||
| 3140 | if templ_7745c5c3_Err != nil { | 3124 | if templ_7745c5c3_Err != nil { | ||
| 3141 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 600, Col: 37} | 3125 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 37} |
| 3142 | } | 3126 | } | ||
| 3143 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152)) | 3127 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152)) | ||
| 3144 | if templ_7745c5c3_Err != nil { | 3128 | if templ_7745c5c3_Err != nil { | ||
| @@ -3151,7 +3135,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3151 | var templ_7745c5c3_Var153 string | 3135 | var templ_7745c5c3_Var153 string | ||
| 3152 | templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | 3136 | templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | ||
| 3153 | if templ_7745c5c3_Err != nil { | 3137 | if templ_7745c5c3_Err != nil { | ||
| 3154 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 600, Col: 49} | 3138 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 49} |
| 3155 | } | 3139 | } | ||
| 3156 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153)) | 3140 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153)) | ||
| 3157 | if templ_7745c5c3_Err != nil { | 3141 | if templ_7745c5c3_Err != nil { | ||
| @@ -3164,7 +3148,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3164 | var templ_7745c5c3_Var154 string | 3148 | var templ_7745c5c3_Var154 string | ||
| 3165 | templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | 3149 | templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | ||
| 3166 | if templ_7745c5c3_Err != nil { | 3150 | if templ_7745c5c3_Err != nil { | ||
| 3167 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 32} | 3151 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 599, Col: 32} |
| 3168 | } | 3152 | } | ||
| 3169 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154)) | 3153 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154)) | ||
| 3170 | if templ_7745c5c3_Err != nil { | 3154 | if templ_7745c5c3_Err != nil { | ||
| @@ -3177,7 +3161,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3177 | var templ_7745c5c3_Var155 string | 3161 | var templ_7745c5c3_Var155 string | ||
| 3178 | templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | 3162 | templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | ||
| 3179 | if templ_7745c5c3_Err != nil { | 3163 | if templ_7745c5c3_Err != nil { | ||
| 3180 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 16} | 3164 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 16} |
| 3181 | } | 3165 | } | ||
| 3182 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155)) | 3166 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155)) | ||
| 3183 | if templ_7745c5c3_Err != nil { | 3167 | if templ_7745c5c3_Err != nil { | ||
| @@ -3191,7 +3175,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3191 | var templ_7745c5c3_Var156 string | 3175 | var templ_7745c5c3_Var156 string | ||
| 3192 | templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 3176 | templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | ||
| 3193 | if templ_7745c5c3_Err != nil { | 3177 | if templ_7745c5c3_Err != nil { | ||
| 3194 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 15} | 3178 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 15} |
| 3195 | } | 3179 | } | ||
| 3196 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156)) | 3180 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156)) | ||
| 3197 | if templ_7745c5c3_Err != nil { | 3181 | if templ_7745c5c3_Err != nil { | ||
| @@ -3204,7 +3188,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3204 | var templ_7745c5c3_Var157 string | 3188 | var templ_7745c5c3_Var157 string | ||
| 3205 | templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 3189 | templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | ||
| 3206 | if templ_7745c5c3_Err != nil { | 3190 | if templ_7745c5c3_Err != nil { | ||
| 3207 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 42} | 3191 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 42} |
| 3208 | } | 3192 | } | ||
| 3209 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157) | 3193 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157) | ||
| 3210 | if templ_7745c5c3_Err != nil { | 3194 | if templ_7745c5c3_Err != nil { | ||
| @@ -3217,7 +3201,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3217 | var templ_7745c5c3_Var158 string | 3201 | var templ_7745c5c3_Var158 string | ||
| 3218 | templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | 3202 | templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | ||
| 3219 | if templ_7745c5c3_Err != nil { | 3203 | if templ_7745c5c3_Err != nil { | ||
| 3220 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 53} | 3204 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 53} |
| 3221 | } | 3205 | } | ||
| 3222 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158)) | 3206 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158)) | ||
| 3223 | if templ_7745c5c3_Err != nil { | 3207 | if templ_7745c5c3_Err != nil { | ||
| @@ -3245,7 +3229,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3245 | var templ_7745c5c3_Var159 string | 3229 | var templ_7745c5c3_Var159 string | ||
| 3246 | templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | 3230 | templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | ||
| 3247 | if templ_7745c5c3_Err != nil { | 3231 | if templ_7745c5c3_Err != nil { | ||
| 3248 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 612, Col: 47} | 3232 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 610, Col: 47} |
| 3249 | } | 3233 | } | ||
| 3250 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159)) | 3234 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159)) | ||
| 3251 | if templ_7745c5c3_Err != nil { | 3235 | if templ_7745c5c3_Err != nil { | ||
| @@ -3268,7 +3252,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 3268 | var templ_7745c5c3_Var160 string | 3252 | var templ_7745c5c3_Var160 string | ||
| 3269 | templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | 3253 | templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | ||
| 3270 | if templ_7745c5c3_Err != nil { | 3254 | if templ_7745c5c3_Err != nil { | ||
| 3271 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 617, Col: 66} | 3255 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 615, Col: 66} |
| 3272 | } | 3256 | } | ||
| 3273 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160)) | 3257 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160)) | ||
| 3274 | if templ_7745c5c3_Err != nil { | 3258 | if templ_7745c5c3_Err != nil { | ||
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> | |
| @@ -28,6 +28,7 @@ templ OverlayHelp(mr bool) { | |||||
| 28 | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> | 28 | <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div> | ||
| 29 | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches & tags, merge requests, issues</dd></div> | 29 | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches & tags, merge requests, issues</dd></div> | ||
| 30 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | 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 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | 32 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | ||
| 32 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | 33 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | ||
| 33 | <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> | 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 | } | |
| @@ -2,6 +2,8 @@ | |||||
| 2 | 2 | ||||
| 3 | package web | 3 | package web | ||
| 4 | 4 | ||||
| 5 | + | import "fmt" | |||
| 6 | + | ||||
| 5 | // Home is the repository list. | 7 | // Home is the repository list. | ||
| 6 | templ Home(l Layout, p HomePage) { | 8 | templ Home(l Layout, p HomePage) { | ||
| 7 | @Shell(l, nil) { | 9 | @Shell(l, nil) { | ||
| @@ -9,14 +11,6 @@ templ Home(l Layout, p HomePage) { | |||||
| 9 | <div class="home" data-layout="cards"> | 11 | <div class="home" data-layout="cards"> | ||
| 10 | <section class="home-repos" aria-label="Repositories"> | 12 | <section class="home-repos" aria-label="Repositories"> | ||
| 11 | <div class="home-bar"> | 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 | <div class="filter"> | 14 | <div class="filter"> | ||
| 21 | <label for="home-filter" class="sr">filter</label> | 15 | <label for="home-filter" class="sr">filter</label> | ||
| 22 | <input | 16 | <input | ||
| @@ -57,7 +51,7 @@ templ Home(l Layout, p HomePage) { | |||||
| 57 | </button> | 51 | </button> | ||
| 58 | </div> | 52 | </div> | ||
| 59 | </div> | 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 | for _, r := range p.Rows { | 55 | for _, r := range p.Rows { | ||
| 62 | @HomeRowView(r) | 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> | |
| @@ -25,7 +25,7 @@ templ Shell(l Layout, rail templ.Component) { | |||||
| 25 | <script src={ l.Assets.HTMX } defer></script> | 25 | <script src={ l.Assets.HTMX } defer></script> | ||
| 26 | <script src={ l.Assets.Live } defer></script> | 26 | <script src={ l.Assets.Live } defer></script> | ||
| 27 | <script src={ l.Assets.SSE } defer></script> | 27 | <script src={ l.Assets.SSE } defer></script> | ||
| 28 | - | <script src={ l.Assets.Store } defer></script> | 28 | + | <script src={ l.Assets.Store }></script> |
| 29 | <script src={ l.Assets.App } defer></script> | 29 | <script src={ l.Assets.App } defer></script> | ||
| 30 | <script src={ l.Assets.Files } defer></script> | 30 | <script src={ l.Assets.Files } defer></script> | ||
| 31 | <script src={ l.Assets.Passkey } defer></script> | 31 | <script src={ l.Assets.Passkey } defer></script> | ||
| @@ -80,6 +80,19 @@ templ Header(l Layout) { | |||||
| 80 | <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)"> | 80 | <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)"> | ||
| 81 | <span class="brand-mark" aria-hidden="true"></span> | 81 | <span class="brand-mark" aria-hidden="true"></span> | ||
| 82 | </a> | 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 | if len(l.Crumbs) > 0 { | 96 | if len(l.Crumbs) > 0 { | ||
| 84 | <nav class="crumbs" aria-label="Path"> | 97 | <nav class="crumbs" aria-label="Path"> | ||
| 85 | <ol> | 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); } | |
| @@ -29,6 +29,7 @@ | |||||
| 29 | .i-cards { --i: url(icons/cards.svg); } | 29 | .i-cards { --i: url(icons/cards.svg); } | ||
| 30 | .i-split { --i: url(icons/split.svg); } | 30 | .i-split { --i: url(icons/split.svg); } | ||
| 31 | .i-compact { --i: url(icons/compact.svg); } | 31 | .i-compact { --i: url(icons/compact.svg); } | ||
| 32 | + | .i-mailbox { --i: url(icons/mailbox.svg); } | |||
| 32 | 33 | ||||
| 33 | .pill { | 34 | .pill { | ||
| 34 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | 35 | display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1); | ||
| @@ -77,6 +78,11 @@ | |||||
| 77 | } | 78 | } | ||
| 78 | /* the hermit by his fire: a grey picture in its own rounded square */ | 79 | /* the hermit by his fire: a grey picture in its own rounded square */ | ||
| 79 | .brand-mark { width: 32px; height: 32px; flex: none; background: url(icons/brand.svg) center / contain no-repeat; } | 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 | .crumbs { | 86 | .crumbs { | ||
| 81 | min-width: 0; flex: 1 1 auto; overflow: hidden; | 87 | min-width: 0; flex: 1 1 auto; overflow: hidden; | ||
| 82 | & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); } | 88 | & ol { display: flex; align-items: center; min-width: 0; font-family: var(--mono); font-size: var(--fs); } | ||
| @@ -133,6 +139,9 @@ | |||||
| 133 | align-content: start; | 139 | align-content: start; | ||
| 134 | } | 140 | } | ||
| 135 | .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); } | 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 | .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; } | 145 | .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; } | ||
| 137 | .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); } | 146 | .work-lede { grid-column: 1 / -1; margin-bottom: calc(-1 * var(--s2)); } | ||
| 138 | main > * + * { margin-top: var(--s4); } | 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; } | |
| @@ -79,13 +79,16 @@ | |||||
| 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; } | 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 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | 80 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | ||
| 81 | & .tx { white-space: pre; } | 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 | /* a run of hidden lines: the whole row loads them */ | 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 | & .gap button { | 88 | & .gap button { | ||
| 86 | display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer; | 89 | display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer; | ||
| 87 | - | font: inherit; color: var(--ink-dim); background: none; border: 0; | 90 | + | font: inherit; color: var(--ink-mute); background: none; border: 0; |
| 88 | - | border-block: 1px dashed var(--border); | 91 | + | border-block: 1px dashed color-mix(in srgb, var(--border) 60%, transparent); |
| 89 | } | 92 | } | ||
| 90 | & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); } | 93 | & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); } | ||
| 91 | & .gap button:disabled { cursor: progress; opacity: .6; } | 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 | } | |
| @@ -9,4 +9,29 @@ | |||||
| 9 | 9 | ||||
| 10 | @layer components { | 10 | @layer components { | ||
| 11 | .sel { outline: 1px solid var(--ink-dim); outline-offset: -1px; } | 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; } | |
| @@ -1,6 +1,12 @@ | |||||
| 1 | @layer components { | 1 | @layer components { | ||
| 2 | /* merge requests: the review page. Hue only where it carries a verdict. */ | 2 | /* merge requests: the review page. Hue only where it carries a verdict. */ | ||
| 3 | .mr-h .show-kv { margin-top: 0; } | 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 | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | 11 | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | ||
| 6 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | 12 | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | ||
| @@ -49,6 +55,15 @@ | |||||
| 49 | } | 55 | } | ||
| 50 | & .kv { margin: 0; } | 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 | .mmenu { | 67 | .mmenu { | ||
| 53 | & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; } | 68 | & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; } | ||
| 54 | & .d-modes label, & .d-check { | 69 | & .d-modes label, & .d-check { | ||
| @@ -207,7 +222,6 @@ | |||||
| 207 | & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); } | 222 | & .inbox-counts strong { color: var(--ink-hi); font-family: var(--mono); } | ||
| 208 | & .since { align-self: flex-start; max-width: 100%; } | 223 | & .since { align-self: flex-start; max-width: 100%; } | ||
| 209 | } | 224 | } | ||
| 210 | - | .inbox-btn { gap: var(--s2); } | |||
| 211 | 225 | ||||
| 212 | @media (max-width: 700px) { | 226 | @media (max-width: 700px) { | ||
| 213 | .list.mrs .c-who { display: none; } | 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> | |
| @@ -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); | |
| @@ -14,6 +14,7 @@ | |||||
| 14 | // 7. The service worker (ermiteInstall). | 14 | // 7. The service worker (ermiteInstall). | ||
| 15 | // 8. Chip lists kept on one line, with a … chip (ermiteFit). | 15 | // 8. Chip lists kept on one line, with a … chip (ermiteFit). | ||
| 16 | // 9. The layout of the home page, remembered in the browser (ermiteLayout). | 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 | // Preferences are kept by ermiteStore (store.js, loaded first). | 19 | // Preferences are kept by ermiteStore (store.js, loaded first). | ||
| 19 | (() => { | 20 | (() => { | ||
| @@ -400,6 +401,25 @@ | |||||
| 400 | // file whose lines are all loaded has no gap left. | 401 | // file whose lines are all loaded has no gap left. | ||
| 401 | window.ermiteGaps = (() => { | 402 | window.ermiteGaps = (() => { | ||
| 402 | const pending = new Map(); | 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 | // load fills the gap the button is in, in both tables of its file. | 423 | // load fills the gap the button is in, in both tables of its file. | ||
| 404 | function load(btn) { | 424 | function load(btn) { | ||
| 405 | const src = btn.dataset.src; | 425 | const src = btn.dataset.src; | ||
| @@ -424,6 +444,7 @@ | |||||
| 424 | if (!from || !at) continue; | 444 | if (!from || !at) continue; | ||
| 425 | const rows = [...from.children].map((r) => r.cloneNode(true)); | 445 | const rows = [...from.children].map((r) => r.cloneNode(true)); | ||
| 426 | n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length); | 446 | n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length); | ||
| 447 | + | selectLoaded(file, rows.filter((r) => !r.classList.contains('gap'))); | |||
| 427 | at.replaceWith(...rows); | 448 | at.replaceWith(...rows); | ||
| 428 | } | 449 | } | ||
| 429 | // The focus was on the button, which is gone: on the gap that is | 450 | // The focus was on the button, which is gone: on the gap that is | ||
| @@ -553,9 +574,44 @@ | |||||
| 553 | return { get: pref.get, set, apply, values }; | 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 | // 1. Keys. | 612 | // 1. Keys. | ||
| 557 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | 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 | // the merge request page (mr.js): the merge menu, mark read | 615 | // the merge request page (mr.js): the merge menu, mark read | ||
| 560 | M: 'merge', R: 'read' }; | 616 | M: 'merge', R: 'read' }; | ||
| 561 | // "g t" is the key "t": the tabs have one event each. | 617 | // "g t" is the key "t": the tabs have one event each. | ||
| @@ -571,7 +627,7 @@ | |||||
| 571 | const mr = window.ermiteMR; | 627 | const mr = window.ermiteMR; | ||
| 572 | if (e.key === 'Escape') { | 628 | if (e.key === 'Escape') { | ||
| 573 | pending = 0; | 629 | pending = 0; | ||
| 574 | - | if (mr?.closeMenus() || mr?.leaveFiles()) return; | 630 | + | if (closeMenus() || mr?.leaveFiles()) return; |
| 575 | if (typing) e.target.blur(); | 631 | if (typing) e.target.blur(); | ||
| 576 | return ermite.fire('esc'); | 632 | return ermite.fire('esc'); | ||
| 577 | } | 633 | } | ||
| @@ -611,6 +667,13 @@ | |||||
| 611 | if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; | 667 | if (!el || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; | ||
| 612 | e.preventDefault(); | 668 | e.preventDefault(); | ||
| 613 | ermite.fire(el.dataset.fire); | 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 | for (const a of $$('a[data-fire="refs"]')) { | 678 | for (const a of $$('a[data-fire="refs"]')) { | ||
| 616 | a.setAttribute('aria-haspopup', 'dialog'); | 679 | a.setAttribute('aria-haspopup', 'dialog'); | ||
| @@ -773,7 +836,8 @@ | |||||
| 773 | // internal/adapters/web/events.go). A fragment is a top-level element with an | 836 | // internal/adapters/web/events.go). A fragment is a top-level element with an | ||
| 774 | // id: it replaces the element of the page that has the same id. A fragment | 837 | // id: it replaces the element of the page that has the same id. A fragment | ||
| 775 | // marked data-gone removes it; a row of a repository the home page does not | 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 | // banner, a persistent status region) keeps its element and only gets the | 841 | // banner, a persistent status region) keeps its element and only gets the | ||
| 778 | // children of the fragment, so that screen readers announce the change. A gap, | 842 | // children of the fragment, so that screen readers announce the change. A gap, | ||
| 779 | // which says the page may have missed events or that what it shows changed, | 843 | // which says the page may have missed events or that what it shows changed, | ||
| @@ -872,6 +936,9 @@ window.ermiteLive = (() => { | |||||
| 872 | } | 936 | } | ||
| 873 | if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', ''); | 937 | if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', ''); | ||
| 874 | old.replaceWith(el); | 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 | window.htmx?.process(el); | 942 | window.htmx?.process(el); | ||
| 876 | fitIn(el); | 943 | fitIn(el); | ||
| 877 | if (had) refocus(el, had); | 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()); | |
| @@ -196,12 +196,6 @@ window.ermiteMR = (() => { | |||||
| 196 | $('[data-default]', m)?.focus(); | 196 | $('[data-default]', m)?.focus(); | ||
| 197 | return true; | 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 | // menuKey handles a key while the merge menu is open; true when it used it. | 199 | // menuKey handles a key while the merge menu is open; true when it used it. | ||
| 206 | function menuKey(e) { | 200 | function menuKey(e) { | ||
| 207 | const m = menu(); | 201 | const m = menu(); | ||
| @@ -311,13 +305,9 @@ window.ermiteMR = (() => { | |||||
| 311 | try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ } | 305 | try { sessionStorage.setItem('mr-last-line', lastLine); } catch { /* storage optional */ } | ||
| 312 | if (next) { e.preventDefault(); location.assign(next); } | 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 | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start()); | 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'); | |
| @@ -74,3 +74,8 @@ window.ermiteStore = (() => { | |||||
| 74 | 74 | ||||
| 75 | return { get, set, remove, watch, pref }; | 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 | |
| @@ -71,6 +71,9 @@ type Layout struct { | |||||
| 71 | MR bool | 71 | MR bool | ||
| 72 | // IssueForm loads the script of the public issue form (issue-form.js). | 72 | // IssueForm loads the script of the public issue form (issue-form.js). | ||
| 73 | IssueForm bool | 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 | // Link is a plain label + href pair. | 79 | // Link is a plain label + href pair. | ||
| @@ -93,7 +96,7 @@ type HomeRow struct { | |||||
| 93 | Stats *HomeStats | 96 | Stats *HomeStats | ||
| 94 | // Private marks a private repository; only set for a signed-in visitor. | 97 | // Private marks a private repository; only set for a signed-in visitor. | ||
| 95 | Private bool | 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 | Fresh bool | 100 | Fresh bool | ||
| 98 | // Commits are the newest of the default branch. NoCommits says there is | 101 | // Commits are the newest of the default branch. NoCommits says there is | ||
| 99 | // none (never pushed, or an empty branch); a failed read says nothing. | 102 | // none (never pushed, or an empty branch); a failed read says nothing. | ||
| @@ -135,11 +138,10 @@ type SortLink struct { | |||||
| 135 | 138 | ||||
| 136 | // HomePage is the repository list. | 139 | // HomePage is the repository list. | ||
| 137 | type HomePage struct { | 140 | type HomePage struct { | ||
| 141 | + | Sort string | |||
| 142 | + | Desc bool | |||
| 138 | Sorts []SortLink | 143 | Sorts []SortLink | ||
| 139 | Rows []HomeRow | 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 | // InboxButton is the link to the inbox with the number of open items; Count is | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -239,6 +239,12 @@ | |||||
| 239 | </div> | 239 | </div> | ||
| 240 | <div> | 240 | <div> | ||
| 241 | <dt> | 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 | <kbd>y</kbd> | 248 | <kbd>y</kbd> | ||
| 243 | </dt> | 249 | </dt> | ||
| 244 | <dd>copy a permalink pinned to the commit</dd> | 250 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -416,6 +422,7 @@ | |||||
| 416 | k-split from:body -> ermite.layout('split'); | 422 | k-split from:body -> ermite.layout('split'); | ||
| 417 | k-unified from:body -> ermite.layout('unified'); | 423 | k-unified from:body -> ermite.layout('unified'); | ||
| 418 | k-collapse from:body -> ermite.collapse(); | 424 | k-collapse from:body -> ermite.collapse(); | ||
| 425 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 419 | k-expand from:body -> ermite.expandFile(); | 426 | k-expand from:body -> ermite.expandFile(); | ||
| 420 | k-expandAll from:body -> ermite.expandAll(); | 427 | k-expandAll from:body -> ermite.expandAll(); | ||
| 421 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -309,6 +309,12 @@ | |||||
| 309 | </div> | 309 | </div> | ||
| 310 | <div> | 310 | <div> | ||
| 311 | <dt> | 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 | <kbd>y</kbd> | 318 | <kbd>y</kbd> | ||
| 313 | </dt> | 319 | </dt> | ||
| 314 | <dd>copy a permalink pinned to the commit</dd> | 320 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -486,6 +492,7 @@ | |||||
| 486 | k-split from:body -> ermite.layout('split'); | 492 | k-split from:body -> ermite.layout('split'); | ||
| 487 | k-unified from:body -> ermite.layout('unified'); | 493 | k-unified from:body -> ermite.layout('unified'); | ||
| 488 | k-collapse from:body -> ermite.collapse(); | 494 | k-collapse from:body -> ermite.collapse(); | ||
| 495 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 489 | k-expand from:body -> ermite.expandFile(); | 496 | k-expand from:body -> ermite.expandFile(); | ||
| 490 | k-expandAll from:body -> ermite.expandAll(); | 497 | k-expandAll from:body -> ermite.expandAll(); | ||
| 491 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -239,6 +239,12 @@ | |||||
| 239 | </div> | 239 | </div> | ||
| 240 | <div> | 240 | <div> | ||
| 241 | <dt> | 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 | <kbd>y</kbd> | 248 | <kbd>y</kbd> | ||
| 243 | </dt> | 249 | </dt> | ||
| 244 | <dd>copy a permalink pinned to the commit</dd> | 250 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -416,6 +422,7 @@ | |||||
| 416 | k-split from:body -> ermite.layout('split'); | 422 | k-split from:body -> ermite.layout('split'); | ||
| 417 | k-unified from:body -> ermite.layout('unified'); | 423 | k-unified from:body -> ermite.layout('unified'); | ||
| 418 | k-collapse from:body -> ermite.collapse(); | 424 | k-collapse from:body -> ermite.collapse(); | ||
| 425 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 419 | k-expand from:body -> ermite.expandFile(); | 426 | k-expand from:body -> ermite.expandFile(); | ||
| 420 | k-expandAll from:body -> ermite.expandAll(); | 427 | k-expandAll from:body -> ermite.expandAll(); | ||
| 421 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -241,6 +241,12 @@ | |||||
| 241 | </div> | 241 | </div> | ||
| 242 | <div> | 242 | <div> | ||
| 243 | <dt> | 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 | <kbd>y</kbd> | 250 | <kbd>y</kbd> | ||
| 245 | </dt> | 251 | </dt> | ||
| 246 | <dd>copy a permalink pinned to the commit</dd> | 252 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -418,6 +424,7 @@ | |||||
| 418 | k-split from:body -> ermite.layout('split'); | 424 | k-split from:body -> ermite.layout('split'); | ||
| 419 | k-unified from:body -> ermite.layout('unified'); | 425 | k-unified from:body -> ermite.layout('unified'); | ||
| 420 | k-collapse from:body -> ermite.collapse(); | 426 | k-collapse from:body -> ermite.collapse(); | ||
| 427 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 421 | k-expand from:body -> ermite.expandFile(); | 428 | k-expand from:body -> ermite.expandFile(); | ||
| 422 | k-expandAll from:body -> ermite.expandAll(); | 429 | k-expandAll from:body -> ermite.expandAll(); | ||
| 423 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -306,6 +306,12 @@ | |||||
| 306 | </div> | 306 | </div> | ||
| 307 | <div> | 307 | <div> | ||
| 308 | <dt> | 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 | <kbd>y</kbd> | 315 | <kbd>y</kbd> | ||
| 310 | </dt> | 316 | </dt> | ||
| 311 | <dd>copy a permalink pinned to the commit</dd> | 317 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -483,6 +489,7 @@ | |||||
| 483 | k-split from:body -> ermite.layout('split'); | 489 | k-split from:body -> ermite.layout('split'); | ||
| 484 | k-unified from:body -> ermite.layout('unified'); | 490 | k-unified from:body -> ermite.layout('unified'); | ||
| 485 | k-collapse from:body -> ermite.collapse(); | 491 | k-collapse from:body -> ermite.collapse(); | ||
| 492 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 486 | k-expand from:body -> ermite.expandFile(); | 493 | k-expand from:body -> ermite.expandFile(); | ||
| 487 | k-expandAll from:body -> ermite.expandAll(); | 494 | k-expandAll from:body -> ermite.expandAll(); | ||
| 488 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -264,6 +264,12 @@ | |||||
| 264 | </div> | 264 | </div> | ||
| 265 | <div> | 265 | <div> | ||
| 266 | <dt> | 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 | <kbd>y</kbd> | 273 | <kbd>y</kbd> | ||
| 268 | </dt> | 274 | </dt> | ||
| 269 | <dd>copy a permalink pinned to the commit</dd> | 275 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -441,6 +447,7 @@ | |||||
| 441 | k-split from:body -> ermite.layout('split'); | 447 | k-split from:body -> ermite.layout('split'); | ||
| 442 | k-unified from:body -> ermite.layout('unified'); | 448 | k-unified from:body -> ermite.layout('unified'); | ||
| 443 | k-collapse from:body -> ermite.collapse(); | 449 | k-collapse from:body -> ermite.collapse(); | ||
| 450 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 444 | k-expand from:body -> ermite.expandFile(); | 451 | k-expand from:body -> ermite.expandFile(); | ||
| 445 | k-expandAll from:body -> ermite.expandAll(); | 452 | k-expandAll from:body -> ermite.expandAll(); | ||
| 446 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -259,6 +259,12 @@ | |||||
| 259 | </div> | 259 | </div> | ||
| 260 | <div> | 260 | <div> | ||
| 261 | <dt> | 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 | <kbd>y</kbd> | 268 | <kbd>y</kbd> | ||
| 263 | </dt> | 269 | </dt> | ||
| 264 | <dd>copy a permalink pinned to the commit</dd> | 270 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -436,6 +442,7 @@ | |||||
| 436 | k-split from:body -> ermite.layout('split'); | 442 | k-split from:body -> ermite.layout('split'); | ||
| 437 | k-unified from:body -> ermite.layout('unified'); | 443 | k-unified from:body -> ermite.layout('unified'); | ||
| 438 | k-collapse from:body -> ermite.collapse(); | 444 | k-collapse from:body -> ermite.collapse(); | ||
| 445 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 439 | k-expand from:body -> ermite.expandFile(); | 446 | k-expand from:body -> ermite.expandFile(); | ||
| 440 | k-expandAll from:body -> ermite.expandAll(); | 447 | k-expandAll from:body -> ermite.expandAll(); | ||
| 441 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -239,6 +239,12 @@ | |||||
| 239 | </div> | 239 | </div> | ||
| 240 | <div> | 240 | <div> | ||
| 241 | <dt> | 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 | <kbd>y</kbd> | 248 | <kbd>y</kbd> | ||
| 243 | </dt> | 249 | </dt> | ||
| 244 | <dd>copy a permalink pinned to the commit</dd> | 250 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -416,6 +422,7 @@ | |||||
| 416 | k-split from:body -> ermite.layout('split'); | 422 | k-split from:body -> ermite.layout('split'); | ||
| 417 | k-unified from:body -> ermite.layout('unified'); | 423 | k-unified from:body -> ermite.layout('unified'); | ||
| 418 | k-collapse from:body -> ermite.collapse(); | 424 | k-collapse from:body -> ermite.collapse(); | ||
| 425 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 419 | k-expand from:body -> ermite.expandFile(); | 426 | k-expand from:body -> ermite.expandFile(); | ||
| 420 | k-expandAll from:body -> ermite.expandAll(); | 427 | k-expandAll from:body -> ermite.expandAll(); | ||
| 421 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -306,6 +306,12 @@ | |||||
| 306 | </div> | 306 | </div> | ||
| 307 | <div> | 307 | <div> | ||
| 308 | <dt> | 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 | <kbd>y</kbd> | 315 | <kbd>y</kbd> | ||
| 310 | </dt> | 316 | </dt> | ||
| 311 | <dd>copy a permalink pinned to the commit</dd> | 317 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -483,6 +489,7 @@ | |||||
| 483 | k-split from:body -> ermite.layout('split'); | 489 | k-split from:body -> ermite.layout('split'); | ||
| 484 | k-unified from:body -> ermite.layout('unified'); | 490 | k-unified from:body -> ermite.layout('unified'); | ||
| 485 | k-collapse from:body -> ermite.collapse(); | 491 | k-collapse from:body -> ermite.collapse(); | ||
| 492 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 486 | k-expand from:body -> ermite.expandFile(); | 493 | k-expand from:body -> ermite.expandFile(); | ||
| 487 | k-expandAll from:body -> ermite.expandAll(); | 494 | k-expandAll from:body -> ermite.expandAll(); | ||
| 488 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -125,6 +125,11 @@ | |||||
| 125 | </span> | 125 | </span> | ||
| 126 | <span class="grow"> | 126 | <span class="grow"> | ||
| 127 | </span> | 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 | <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> | 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 | </a> | 134 | </a> | ||
| 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> | 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 | <kbd aria-hidden="true">x</kbd> | 139 | <kbd aria-hidden="true">x</kbd> | ||
| 135 | </button> | 140 | </button> | ||
| 136 | </div> | 141 | </div> | ||
| 142 | + | </details> | |||
| 143 | + | </div> | |||
| 137 | <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> | 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 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 147 | <summary> | ||
| 141 | <span class="st">A</span> | 148 | <span class="st">A</span> | ||
| 142 | <span class="p">big.txt</span> | 149 | <span class="p">big.txt</span> | ||
| @@ -281,6 +288,12 @@ | |||||
| 281 | </div> | 288 | </div> | ||
| 282 | <div> | 289 | <div> | ||
| 283 | <dt> | 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 | <kbd>y</kbd> | 297 | <kbd>y</kbd> | ||
| 285 | </dt> | 298 | </dt> | ||
| 286 | <dd>copy a permalink pinned to the commit</dd> | 299 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -458,6 +471,7 @@ | |||||
| 458 | k-split from:body -> ermite.layout('split'); | 471 | k-split from:body -> ermite.layout('split'); | ||
| 459 | k-unified from:body -> ermite.layout('unified'); | 472 | k-unified from:body -> ermite.layout('unified'); | ||
| 460 | k-collapse from:body -> ermite.collapse(); | 473 | k-collapse from:body -> ermite.collapse(); | ||
| 474 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 461 | k-expand from:body -> ermite.expandFile(); | 475 | k-expand from:body -> ermite.expandFile(); | ||
| 462 | k-expandAll from:body -> ermite.expandAll(); | 476 | k-expandAll from:body -> ermite.expandAll(); | ||
| 463 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -121,6 +121,11 @@ | |||||
| 121 | </span> | 121 | </span> | ||
| 122 | <span class="grow"> | 122 | <span class="grow"> | ||
| 123 | </span> | 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 | <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 | <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 | </a> | 130 | </a> | ||
| 126 | <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 | <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 | <kbd aria-hidden="true">x</kbd> | 135 | <kbd aria-hidden="true">x</kbd> | ||
| 131 | </button> | 136 | </button> | ||
| 132 | </div> | 137 | </div> | ||
| 138 | + | </details> | |||
| 139 | + | </div> | |||
| 133 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 142 | <summary> | ||
| 136 | <span class="st">M</span> | 143 | <span class="st">M</span> | ||
| 137 | <span class="p">logo.bin</span> | 144 | <span class="p">logo.bin</span> | ||
| @@ -273,6 +280,12 @@ | |||||
| 273 | </div> | 280 | </div> | ||
| 274 | <div> | 281 | <div> | ||
| 275 | <dt> | 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 | <kbd>y</kbd> | 289 | <kbd>y</kbd> | ||
| 277 | </dt> | 290 | </dt> | ||
| 278 | <dd>copy a permalink pinned to the commit</dd> | 291 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -450,6 +463,7 @@ | |||||
| 450 | k-split from:body -> ermite.layout('split'); | 463 | k-split from:body -> ermite.layout('split'); | ||
| 451 | k-unified from:body -> ermite.layout('unified'); | 464 | k-unified from:body -> ermite.layout('unified'); | ||
| 452 | k-collapse from:body -> ermite.collapse(); | 465 | k-collapse from:body -> ermite.collapse(); | ||
| 466 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 453 | k-expand from:body -> ermite.expandFile(); | 467 | k-expand from:body -> ermite.expandFile(); | ||
| 454 | k-expandAll from:body -> ermite.expandAll(); | 468 | k-expandAll from:body -> ermite.expandAll(); | ||
| 455 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -120,6 +120,11 @@ | |||||
| 120 | </span> | 120 | </span> | ||
| 121 | <span class="grow"> | 121 | <span class="grow"> | ||
| 122 | </span> | 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 | <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> | 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 | </a> | 129 | </a> | ||
| 125 | <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 | <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 | <kbd aria-hidden="true">x</kbd> | 134 | <kbd aria-hidden="true">x</kbd> | ||
| 130 | </button> | 135 | </button> | ||
| 131 | </div> | 136 | </div> | ||
| 137 | + | </details> | |||
| 138 | + | </div> | |||
| 132 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 141 | <summary> | ||
| 135 | <span class="st">A</span> | 142 | <span class="st">A</span> | ||
| 136 | <span class="p">side.txt</span> | 143 | <span class="p">side.txt</span> | ||
| @@ -315,6 +322,12 @@ | |||||
| 315 | </div> | 322 | </div> | ||
| 316 | <div> | 323 | <div> | ||
| 317 | <dt> | 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 | <kbd>y</kbd> | 331 | <kbd>y</kbd> | ||
| 319 | </dt> | 332 | </dt> | ||
| 320 | <dd>copy a permalink pinned to the commit</dd> | 333 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -492,6 +505,7 @@ | |||||
| 492 | k-split from:body -> ermite.layout('split'); | 505 | k-split from:body -> ermite.layout('split'); | ||
| 493 | k-unified from:body -> ermite.layout('unified'); | 506 | k-unified from:body -> ermite.layout('unified'); | ||
| 494 | k-collapse from:body -> ermite.collapse(); | 507 | k-collapse from:body -> ermite.collapse(); | ||
| 508 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 495 | k-expand from:body -> ermite.expandFile(); | 509 | k-expand from:body -> ermite.expandFile(); | ||
| 496 | k-expandAll from:body -> ermite.expandAll(); | 510 | k-expandAll from:body -> ermite.expandAll(); | ||
| 497 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -127,6 +127,11 @@ And <b>html</b> & "quotes".</pre> | |||||
| 127 | </span> | 127 | </span> | ||
| 128 | <span class="grow"> | 128 | <span class="grow"> | ||
| 129 | </span> | 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 | <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> | 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 | </a> | 136 | </a> | ||
| 132 | <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> | 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 | <kbd aria-hidden="true">x</kbd> | 141 | <kbd aria-hidden="true">x</kbd> | ||
| 137 | </button> | 142 | </button> | ||
| 138 | </div> | 143 | </div> | ||
| 144 | + | </details> | |||
| 145 | + | </div> | |||
| 139 | <div id="diffs" data-diff="split"> | 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 | <summary> | 148 | <summary> | ||
| 142 | <span class="st">M</span> | 149 | <span class="st">M</span> | ||
| 143 | <span class="p">a.txt</span> | 150 | <span class="p">a.txt</span> | ||
| @@ -277,7 +284,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 277 | </table> | 284 | </table> | ||
| 278 | </div> | 285 | </div> | ||
| 279 | </details> | 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 | <summary> | 288 | <summary> | ||
| 282 | <span class="st">M</span> | 289 | <span class="st">M</span> | ||
| 283 | <span class="p">run.sh</span> | 290 | <span class="p">run.sh</span> | ||
| @@ -438,6 +445,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 438 | </div> | 445 | </div> | ||
| 439 | <div> | 446 | <div> | ||
| 440 | <dt> | 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 | <kbd>y</kbd> | 454 | <kbd>y</kbd> | ||
| 442 | </dt> | 455 | </dt> | ||
| 443 | <dd>copy a permalink pinned to the commit</dd> | 456 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -615,6 +628,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 615 | k-split from:body -> ermite.layout('split'); | 628 | k-split from:body -> ermite.layout('split'); | ||
| 616 | k-unified from:body -> ermite.layout('unified'); | 629 | k-unified from:body -> ermite.layout('unified'); | ||
| 617 | k-collapse from:body -> ermite.collapse(); | 630 | k-collapse from:body -> ermite.collapse(); | ||
| 631 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 618 | k-expand from:body -> ermite.expandFile(); | 632 | k-expand from:body -> ermite.expandFile(); | ||
| 619 | k-expandAll from:body -> ermite.expandAll(); | 633 | k-expandAll from:body -> ermite.expandAll(); | ||
| 620 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -127,6 +127,11 @@ And <b>html</b> & "quotes".</pre> | |||||
| 127 | </span> | 127 | </span> | ||
| 128 | <span class="grow"> | 128 | <span class="grow"> | ||
| 129 | </span> | 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 | <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> | 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 | </a> | 136 | </a> | ||
| 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> | 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 | <kbd aria-hidden="true">x</kbd> | 141 | <kbd aria-hidden="true">x</kbd> | ||
| 137 | </button> | 142 | </button> | ||
| 138 | </div> | 143 | </div> | ||
| 144 | + | </details> | |||
| 145 | + | </div> | |||
| 139 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 148 | <summary> | ||
| 142 | <span class="st">M</span> | 149 | <span class="st">M</span> | ||
| 143 | <span class="p">a.txt</span> | 150 | <span class="p">a.txt</span> | ||
| @@ -277,7 +284,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 277 | </table> | 284 | </table> | ||
| 278 | </div> | 285 | </div> | ||
| 279 | </details> | 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 | <summary> | 288 | <summary> | ||
| 282 | <span class="st">M</span> | 289 | <span class="st">M</span> | ||
| 283 | <span class="p">run.sh</span> | 290 | <span class="p">run.sh</span> | ||
| @@ -438,6 +445,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 438 | </div> | 445 | </div> | ||
| 439 | <div> | 446 | <div> | ||
| 440 | <dt> | 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 | <kbd>y</kbd> | 454 | <kbd>y</kbd> | ||
| 442 | </dt> | 455 | </dt> | ||
| 443 | <dd>copy a permalink pinned to the commit</dd> | 456 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -615,6 +628,7 @@ And <b>html</b> & "quotes".</pre> | |||||
| 615 | k-split from:body -> ermite.layout('split'); | 628 | k-split from:body -> ermite.layout('split'); | ||
| 616 | k-unified from:body -> ermite.layout('unified'); | 629 | k-unified from:body -> ermite.layout('unified'); | ||
| 617 | k-collapse from:body -> ermite.collapse(); | 630 | k-collapse from:body -> ermite.collapse(); | ||
| 631 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 618 | k-expand from:body -> ermite.expandFile(); | 632 | k-expand from:body -> ermite.expandFile(); | ||
| 619 | k-expandAll from:body -> ermite.expandAll(); | 633 | k-expandAll from:body -> ermite.expandAll(); | ||
| 620 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -121,6 +121,11 @@ | |||||
| 121 | </span> | 121 | </span> | ||
| 122 | <span class="grow"> | 122 | <span class="grow"> | ||
| 123 | </span> | 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 | <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 | <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 | </a> | 130 | </a> | ||
| 126 | <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 | <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 | <kbd aria-hidden="true">x</kbd> | 135 | <kbd aria-hidden="true">x</kbd> | ||
| 131 | </button> | 136 | </button> | ||
| 132 | </div> | 137 | </div> | ||
| 138 | + | </details> | |||
| 139 | + | </div> | |||
| 133 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 142 | <summary> | ||
| 136 | <span class="st">R</span> | 143 | <span class="st">R</span> | ||
| 137 | <span class="p">old.txt → docs-old.txt</span> | 144 | <span class="p">old.txt → docs-old.txt</span> | ||
| @@ -141,7 +148,7 @@ | |||||
| 141 | </summary> | 148 | </summary> | ||
| 142 | <p class="note pad">renamed, 100% similar</p> | 149 | <p class="note pad">renamed, 100% similar</p> | ||
| 143 | </details> | 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 | <summary> | 152 | <summary> | ||
| 146 | <span class="st">D</span> | 153 | <span class="st">D</span> | ||
| 147 | <span class="p">gone.txt</span> | 154 | <span class="p">gone.txt</span> | ||
| @@ -342,6 +349,12 @@ | |||||
| 342 | </div> | 349 | </div> | ||
| 343 | <div> | 350 | <div> | ||
| 344 | <dt> | 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 | <kbd>y</kbd> | 358 | <kbd>y</kbd> | ||
| 346 | </dt> | 359 | </dt> | ||
| 347 | <dd>copy a permalink pinned to the commit</dd> | 360 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -519,6 +532,7 @@ | |||||
| 519 | k-split from:body -> ermite.layout('split'); | 532 | k-split from:body -> ermite.layout('split'); | ||
| 520 | k-unified from:body -> ermite.layout('unified'); | 533 | k-unified from:body -> ermite.layout('unified'); | ||
| 521 | k-collapse from:body -> ermite.collapse(); | 534 | k-collapse from:body -> ermite.collapse(); | ||
| 535 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 522 | k-expand from:body -> ermite.expandFile(); | 536 | k-expand from:body -> ermite.expandFile(); | ||
| 523 | k-expandAll from:body -> ermite.expandAll(); | 537 | k-expandAll from:body -> ermite.expandAll(); | ||
| 524 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -119,6 +119,11 @@ | |||||
| 119 | </span> | 119 | </span> | ||
| 120 | <span class="grow"> | 120 | <span class="grow"> | ||
| 121 | </span> | 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 | <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> | 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 | </a> | 128 | </a> | ||
| 124 | <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 | <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 | <kbd aria-hidden="true">x</kbd> | 133 | <kbd aria-hidden="true">x</kbd> | ||
| 129 | </button> | 134 | </button> | ||
| 130 | </div> | 135 | </div> | ||
| 136 | + | </details> | |||
| 137 | + | </div> | |||
| 131 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 140 | <summary> | ||
| 134 | <span class="st">A</span> | 141 | <span class="st">A</span> | ||
| 135 | <span class="p">a.txt</span> | 142 | <span class="p">a.txt</span> | ||
| @@ -251,7 +258,7 @@ | |||||
| 251 | </table> | 258 | </table> | ||
| 252 | </div> | 259 | </div> | ||
| 253 | </details> | 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 | <summary> | 262 | <summary> | ||
| 256 | <span class="st">A</span> | 263 | <span class="st">A</span> | ||
| 257 | <span class="p">gone.txt</span> | 264 | <span class="p">gone.txt</span> | ||
| @@ -301,7 +308,7 @@ | |||||
| 301 | </table> | 308 | </table> | ||
| 302 | </div> | 309 | </div> | ||
| 303 | </details> | 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 | <summary> | 312 | <summary> | ||
| 306 | <span class="st">A</span> | 313 | <span class="st">A</span> | ||
| 307 | <span class="p">logo.bin</span> | 314 | <span class="p">logo.bin</span> | ||
| @@ -311,7 +318,7 @@ | |||||
| 311 | </summary> | 318 | </summary> | ||
| 312 | <p class="note pad">binary file: no text diff</p> | 319 | <p class="note pad">binary file: no text diff</p> | ||
| 313 | </details> | 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 | <summary> | 322 | <summary> | ||
| 316 | <span class="st">A</span> | 323 | <span class="st">A</span> | ||
| 317 | <span class="p">old.txt</span> | 324 | <span class="p">old.txt</span> | ||
| @@ -397,7 +404,7 @@ | |||||
| 397 | </table> | 404 | </table> | ||
| 398 | </div> | 405 | </div> | ||
| 399 | </details> | 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 | <summary> | 408 | <summary> | ||
| 402 | <span class="st">A</span> | 409 | <span class="st">A</span> | ||
| 403 | <span class="p">run.sh</span> | 410 | <span class="p">run.sh</span> | ||
| @@ -661,6 +668,12 @@ | |||||
| 661 | </div> | 668 | </div> | ||
| 662 | <div> | 669 | <div> | ||
| 663 | <dt> | 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 | <kbd>y</kbd> | 677 | <kbd>y</kbd> | ||
| 665 | </dt> | 678 | </dt> | ||
| 666 | <dd>copy a permalink pinned to the commit</dd> | 679 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -838,6 +851,7 @@ | |||||
| 838 | k-split from:body -> ermite.layout('split'); | 851 | k-split from:body -> ermite.layout('split'); | ||
| 839 | k-unified from:body -> ermite.layout('unified'); | 852 | k-unified from:body -> ermite.layout('unified'); | ||
| 840 | k-collapse from:body -> ermite.collapse(); | 853 | k-collapse from:body -> ermite.collapse(); | ||
| 854 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 841 | k-expand from:body -> ermite.expandFile(); | 855 | k-expand from:body -> ermite.expandFile(); | ||
| 842 | k-expandAll from:body -> ermite.expandAll(); | 856 | k-expandAll from:body -> ermite.expandAll(); | ||
| 843 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -122,6 +122,11 @@ | |||||
| 122 | </span> | 122 | </span> | ||
| 123 | <span class="grow"> | 123 | <span class="grow"> | ||
| 124 | </span> | 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 | <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> | 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 | </a> | 131 | </a> | ||
| 127 | <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> | 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 | <kbd aria-hidden="true">x</kbd> | 136 | <kbd aria-hidden="true">x</kbd> | ||
| 132 | </button> | 137 | </button> | ||
| 133 | </div> | 138 | </div> | ||
| 139 | + | </details> | |||
| 140 | + | </div> | |||
| 134 | <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> | 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 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 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 | <summary> | 144 | <summary> | ||
| 138 | <span class="st">A</span> | 145 | <span class="st">A</span> | ||
| 139 | <span class="p">evil.go</span> | 146 | <span class="p">evil.go</span> | ||
| @@ -490,6 +497,12 @@ | |||||
| 490 | </div> | 497 | </div> | ||
| 491 | <div> | 498 | <div> | ||
| 492 | <dt> | 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 | <kbd>y</kbd> | 506 | <kbd>y</kbd> | ||
| 494 | </dt> | 507 | </dt> | ||
| 495 | <dd>copy a permalink pinned to the commit</dd> | 508 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -667,6 +680,7 @@ | |||||
| 667 | k-split from:body -> ermite.layout('split'); | 680 | k-split from:body -> ermite.layout('split'); | ||
| 668 | k-unified from:body -> ermite.layout('unified'); | 681 | k-unified from:body -> ermite.layout('unified'); | ||
| 669 | k-collapse from:body -> ermite.collapse(); | 682 | k-collapse from:body -> ermite.collapse(); | ||
| 683 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 670 | k-expand from:body -> ermite.expandFile(); | 684 | k-expand from:body -> ermite.expandFile(); | ||
| 671 | k-expandAll from:body -> ermite.expandAll(); | 685 | k-expandAll from:body -> ermite.expandAll(); | ||
| 672 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -266,6 +266,12 @@ | |||||
| 266 | </div> | 266 | </div> | ||
| 267 | <div> | 267 | <div> | ||
| 268 | <dt> | 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 | <kbd>y</kbd> | 275 | <kbd>y</kbd> | ||
| 270 | </dt> | 276 | </dt> | ||
| 271 | <dd>copy a permalink pinned to the commit</dd> | 277 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -429,6 +435,7 @@ | |||||
| 429 | k-split from:body -> ermite.layout('split'); | 435 | k-split from:body -> ermite.layout('split'); | ||
| 430 | k-unified from:body -> ermite.layout('unified'); | 436 | k-unified from:body -> ermite.layout('unified'); | ||
| 431 | k-collapse from:body -> ermite.collapse(); | 437 | k-collapse from:body -> ermite.collapse(); | ||
| 438 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 432 | k-expand from:body -> ermite.expandFile(); | 439 | k-expand from:body -> ermite.expandFile(); | ||
| 433 | k-expandAll from:body -> ermite.expandAll(); | 440 | k-expandAll from:body -> ermite.expandAll(); | ||
| 434 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -281,6 +281,12 @@ | |||||
| 281 | </div> | 281 | </div> | ||
| 282 | <div> | 282 | <div> | ||
| 283 | <dt> | 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 | <kbd>y</kbd> | 290 | <kbd>y</kbd> | ||
| 285 | </dt> | 291 | </dt> | ||
| 286 | <dd>copy a permalink pinned to the commit</dd> | 292 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -444,6 +450,7 @@ | |||||
| 444 | k-split from:body -> ermite.layout('split'); | 450 | k-split from:body -> ermite.layout('split'); | ||
| 445 | k-unified from:body -> ermite.layout('unified'); | 451 | k-unified from:body -> ermite.layout('unified'); | ||
| 446 | k-collapse from:body -> ermite.collapse(); | 452 | k-collapse from:body -> ermite.collapse(); | ||
| 453 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 447 | k-expand from:body -> ermite.expandFile(); | 454 | k-expand from:body -> ermite.expandFile(); | ||
| 448 | k-expandAll from:body -> ermite.expandAll(); | 455 | k-expandAll from:body -> ermite.expandAll(); | ||
| 449 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -1070,6 +1070,12 @@ | |||||
| 1070 | </div> | 1070 | </div> | ||
| 1071 | <div> | 1071 | <div> | ||
| 1072 | <dt> | 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 | <kbd>y</kbd> | 1079 | <kbd>y</kbd> | ||
| 1074 | </dt> | 1080 | </dt> | ||
| 1075 | <dd>copy a permalink pinned to the commit</dd> | 1081 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -1233,6 +1239,7 @@ | |||||
| 1233 | k-split from:body -> ermite.layout('split'); | 1239 | k-split from:body -> ermite.layout('split'); | ||
| 1234 | k-unified from:body -> ermite.layout('unified'); | 1240 | k-unified from:body -> ermite.layout('unified'); | ||
| 1235 | k-collapse from:body -> ermite.collapse(); | 1241 | k-collapse from:body -> ermite.collapse(); | ||
| 1242 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 1236 | k-expand from:body -> ermite.expandFile(); | 1243 | k-expand from:body -> ermite.expandFile(); | ||
| 1237 | k-expandAll from:body -> ermite.expandAll(); | 1244 | k-expandAll from:body -> ermite.expandAll(); | ||
| 1238 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -415,6 +415,12 @@ | |||||
| 415 | </div> | 415 | </div> | ||
| 416 | <div> | 416 | <div> | ||
| 417 | <dt> | 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 | <kbd>y</kbd> | 424 | <kbd>y</kbd> | ||
| 419 | </dt> | 425 | </dt> | ||
| 420 | <dd>copy a permalink pinned to the commit</dd> | 426 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -598,6 +604,7 @@ | |||||
| 598 | k-split from:body -> ermite.layout('split'); | 604 | k-split from:body -> ermite.layout('split'); | ||
| 599 | k-unified from:body -> ermite.layout('unified'); | 605 | k-unified from:body -> ermite.layout('unified'); | ||
| 600 | k-collapse from:body -> ermite.collapse(); | 606 | k-collapse from:body -> ermite.collapse(); | ||
| 607 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 601 | k-expand from:body -> ermite.expandFile(); | 608 | k-expand from:body -> ermite.expandFile(); | ||
| 602 | k-expandAll from:body -> ermite.expandAll(); | 609 | k-expandAll from:body -> ermite.expandAll(); | ||
| 603 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -188,6 +188,12 @@ | |||||
| 188 | </div> | 188 | </div> | ||
| 189 | <div> | 189 | <div> | ||
| 190 | <dt> | 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 | <kbd>y</kbd> | 197 | <kbd>y</kbd> | ||
| 192 | </dt> | 198 | </dt> | ||
| 193 | <dd>copy a permalink pinned to the commit</dd> | 199 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -282,6 +288,7 @@ | |||||
| 282 | k-split from:body -> ermite.layout('split'); | 288 | k-split from:body -> ermite.layout('split'); | ||
| 283 | k-unified from:body -> ermite.layout('unified'); | 289 | k-unified from:body -> ermite.layout('unified'); | ||
| 284 | k-collapse from:body -> ermite.collapse(); | 290 | k-collapse from:body -> ermite.collapse(); | ||
| 291 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 285 | k-expand from:body -> ermite.expandFile(); | 292 | k-expand from:body -> ermite.expandFile(); | ||
| 286 | k-expandAll from:body -> ermite.expandAll(); | 293 | k-expandAll from:body -> ermite.expandAll(); | ||
| 287 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -61,7 +61,7 @@ | |||||
| 61 | </div> | 61 | </div> | ||
| 62 | <nav class="sort" aria-label="Sort repositories"> | 62 | <nav class="sort" aria-label="Sort repositories"> | ||
| 63 | <span class="mute">sort</span> | 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 | <span class="sr">, descending, select for ascending</span> | 65 | <span class="sr">, descending, select for ascending</span> | ||
| 66 | </a> | 66 | </a> | ||
| 67 | <a href="/?sort=name&dir=asc">name </a> | 67 | <a href="/?sort=name&dir=asc">name </a> | ||
| @@ -81,8 +81,31 @@ | |||||
| 81 | </button> | 81 | </button> | ||
| 82 | </div> | 82 | </div> | ||
| 83 | </div> | 83 | </div> | ||
| 84 | - | <ul class="repos" id="repo-rows" data-rows> | 84 | + | <ul class="repos" id="repo-rows" data-rows data-sort="activity" data-desc="true"> |
| 85 | - | <li id="repo-demo" class="repo" data-cursor> | 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 | <div class="repo-main"> | 109 | <div class="repo-main"> | ||
| 87 | <h2 class="repo-name"> | 110 | <h2 class="repo-name"> | ||
| 88 | <a href="/demo">demo</a> | 111 | <a href="/demo">demo</a> | ||
| @@ -148,27 +171,6 @@ | |||||
| 148 | </li> | 171 | </li> | ||
| 149 | </ul> | 172 | </ul> | ||
| 150 | </li> | 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 | </ul> | 174 | </ul> | ||
| 173 | </section> | 175 | </section> | ||
| 174 | </div> | 176 | </div> | ||
| @@ -289,6 +291,12 @@ | |||||
| 289 | </div> | 291 | </div> | ||
| 290 | <div> | 292 | <div> | ||
| 291 | <dt> | 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 | <kbd>y</kbd> | 300 | <kbd>y</kbd> | ||
| 293 | </dt> | 301 | </dt> | ||
| 294 | <dd>copy a permalink pinned to the commit</dd> | 302 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -337,15 +345,15 @@ | |||||
| 337 | <span class="k">go</span> | 345 | <span class="k">go</span> | ||
| 338 | </a> | 346 | </a> | ||
| 339 | </li> | 347 | </li> | ||
| 340 | - | <li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo"> | 348 | + | <li id="cmd-o1" role="option" aria-selected="false" data-k="repo quiet"> |
| 341 | - | <a href="/demo" tabindex="-1"> | 349 | + | <a href="/quiet" tabindex="-1"> |
| 342 | - | <span class="lbl">demo</span> | 350 | + | <span class="lbl">quiet</span> |
| 343 | <span class="k">repo</span> | 351 | <span class="k">repo</span> | ||
| 344 | </a> | 352 | </a> | ||
| 345 | </li> | 353 | </li> | ||
| 346 | - | <li id="cmd-o2" role="option" aria-selected="false" data-k="repo quiet"> | 354 | + | <li id="cmd-o2" role="option" aria-selected="false" data-k="repo demo"> |
| 347 | - | <a href="/quiet" tabindex="-1"> | 355 | + | <a href="/demo" tabindex="-1"> |
| 348 | - | <span class="lbl">quiet</span> | 356 | + | <span class="lbl">demo</span> |
| 349 | <span class="k">repo</span> | 357 | <span class="k">repo</span> | ||
| 350 | </a> | 358 | </a> | ||
| 351 | </li> | 359 | </li> | ||
| @@ -389,6 +397,7 @@ | |||||
| 389 | k-split from:body -> ermite.layout('split'); | 397 | k-split from:body -> ermite.layout('split'); | ||
| 390 | k-unified from:body -> ermite.layout('unified'); | 398 | k-unified from:body -> ermite.layout('unified'); | ||
| 391 | k-collapse from:body -> ermite.collapse(); | 399 | k-collapse from:body -> ermite.collapse(); | ||
| 400 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 392 | k-expand from:body -> ermite.expandFile(); | 401 | k-expand from:body -> ermite.expandFile(); | ||
| 393 | k-expandAll from:body -> ermite.expandAll(); | 402 | k-expandAll from:body -> ermite.expandAll(); | ||
| 394 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -177,6 +177,12 @@ | |||||
| 177 | </div> | 177 | </div> | ||
| 178 | <div> | 178 | <div> | ||
| 179 | <dt> | 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 | <kbd>y</kbd> | 186 | <kbd>y</kbd> | ||
| 181 | </dt> | 187 | </dt> | ||
| 182 | <dd>copy a permalink pinned to the commit</dd> | 188 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -265,6 +271,7 @@ | |||||
| 265 | k-split from:body -> ermite.layout('split'); | 271 | k-split from:body -> ermite.layout('split'); | ||
| 266 | k-unified from:body -> ermite.layout('unified'); | 272 | k-unified from:body -> ermite.layout('unified'); | ||
| 267 | k-collapse from:body -> ermite.collapse(); | 273 | k-collapse from:body -> ermite.collapse(); | ||
| 274 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 268 | k-expand from:body -> ermite.expandFile(); | 275 | k-expand from:body -> ermite.expandFile(); | ||
| 269 | k-expandAll from:body -> ermite.expandAll(); | 276 | k-expandAll from:body -> ermite.expandAll(); | ||
| 270 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -177,6 +177,12 @@ | |||||
| 177 | </div> | 177 | </div> | ||
| 178 | <div> | 178 | <div> | ||
| 179 | <dt> | 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 | <kbd>y</kbd> | 186 | <kbd>y</kbd> | ||
| 181 | </dt> | 187 | </dt> | ||
| 182 | <dd>copy a permalink pinned to the commit</dd> | 188 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -265,6 +271,7 @@ | |||||
| 265 | k-split from:body -> ermite.layout('split'); | 271 | k-split from:body -> ermite.layout('split'); | ||
| 266 | k-unified from:body -> ermite.layout('unified'); | 272 | k-unified from:body -> ermite.layout('unified'); | ||
| 267 | k-collapse from:body -> ermite.collapse(); | 273 | k-collapse from:body -> ermite.collapse(); | ||
| 274 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 268 | k-expand from:body -> ermite.expandFile(); | 275 | k-expand from:body -> ermite.expandFile(); | ||
| 269 | k-expandAll from:body -> ermite.expandAll(); | 276 | k-expandAll from:body -> ermite.expandAll(); | ||
| 270 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -297,6 +297,12 @@ | |||||
| 297 | </div> | 297 | </div> | ||
| 298 | <div> | 298 | <div> | ||
| 299 | <dt> | 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 | <kbd>y</kbd> | 306 | <kbd>y</kbd> | ||
| 301 | </dt> | 307 | </dt> | ||
| 302 | <dd>copy a permalink pinned to the commit</dd> | 308 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -490,6 +496,7 @@ | |||||
| 490 | k-split from:body -> ermite.layout('split'); | 496 | k-split from:body -> ermite.layout('split'); | ||
| 491 | k-unified from:body -> ermite.layout('unified'); | 497 | k-unified from:body -> ermite.layout('unified'); | ||
| 492 | k-collapse from:body -> ermite.collapse(); | 498 | k-collapse from:body -> ermite.collapse(); | ||
| 499 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 493 | k-expand from:body -> ermite.expandFile(); | 500 | k-expand from:body -> ermite.expandFile(); | ||
| 494 | k-expandAll from:body -> ermite.expandAll(); | 501 | k-expandAll from:body -> ermite.expandAll(); | ||
| 495 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -323,6 +323,12 @@ | |||||
| 323 | </div> | 323 | </div> | ||
| 324 | <div> | 324 | <div> | ||
| 325 | <dt> | 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 | <kbd>y</kbd> | 332 | <kbd>y</kbd> | ||
| 327 | </dt> | 333 | </dt> | ||
| 328 | <dd>copy a permalink pinned to the commit</dd> | 334 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -516,6 +522,7 @@ | |||||
| 516 | k-split from:body -> ermite.layout('split'); | 522 | k-split from:body -> ermite.layout('split'); | ||
| 517 | k-unified from:body -> ermite.layout('unified'); | 523 | k-unified from:body -> ermite.layout('unified'); | ||
| 518 | k-collapse from:body -> ermite.collapse(); | 524 | k-collapse from:body -> ermite.collapse(); | ||
| 525 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 519 | k-expand from:body -> ermite.expandFile(); | 526 | k-expand from:body -> ermite.expandFile(); | ||
| 520 | k-expandAll from:body -> ermite.expandAll(); | 527 | k-expandAll from:body -> ermite.expandAll(); | ||
| 521 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -254,6 +254,12 @@ | |||||
| 254 | </div> | 254 | </div> | ||
| 255 | <div> | 255 | <div> | ||
| 256 | <dt> | 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 | <kbd>y</kbd> | 263 | <kbd>y</kbd> | ||
| 258 | </dt> | 264 | </dt> | ||
| 259 | <dd>copy a permalink pinned to the commit</dd> | 265 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -431,6 +437,7 @@ | |||||
| 431 | k-split from:body -> ermite.layout('split'); | 437 | k-split from:body -> ermite.layout('split'); | ||
| 432 | k-unified from:body -> ermite.layout('unified'); | 438 | k-unified from:body -> ermite.layout('unified'); | ||
| 433 | k-collapse from:body -> ermite.collapse(); | 439 | k-collapse from:body -> ermite.collapse(); | ||
| 440 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 434 | k-expand from:body -> ermite.expandFile(); | 441 | k-expand from:body -> ermite.expandFile(); | ||
| 435 | k-expandAll from:body -> ermite.expandAll(); | 442 | k-expandAll from:body -> ermite.expandAll(); | ||
| 436 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -475,6 +475,12 @@ | |||||
| 475 | </div> | 475 | </div> | ||
| 476 | <div> | 476 | <div> | ||
| 477 | <dt> | 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 | <kbd>y</kbd> | 484 | <kbd>y</kbd> | ||
| 479 | </dt> | 485 | </dt> | ||
| 480 | <dd>copy a permalink pinned to the commit</dd> | 486 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -652,6 +658,7 @@ | |||||
| 652 | k-split from:body -> ermite.layout('split'); | 658 | k-split from:body -> ermite.layout('split'); | ||
| 653 | k-unified from:body -> ermite.layout('unified'); | 659 | k-unified from:body -> ermite.layout('unified'); | ||
| 654 | k-collapse from:body -> ermite.collapse(); | 660 | k-collapse from:body -> ermite.collapse(); | ||
| 661 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 655 | k-expand from:body -> ermite.expandFile(); | 662 | k-expand from:body -> ermite.expandFile(); | ||
| 656 | k-expandAll from:body -> ermite.expandAll(); | 663 | k-expandAll from:body -> ermite.expandAll(); | ||
| 657 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -251,6 +251,12 @@ | |||||
| 251 | </div> | 251 | </div> | ||
| 252 | <div> | 252 | <div> | ||
| 253 | <dt> | 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 | <kbd>y</kbd> | 260 | <kbd>y</kbd> | ||
| 255 | </dt> | 261 | </dt> | ||
| 256 | <dd>copy a permalink pinned to the commit</dd> | 262 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -428,6 +434,7 @@ | |||||
| 428 | k-split from:body -> ermite.layout('split'); | 434 | k-split from:body -> ermite.layout('split'); | ||
| 429 | k-unified from:body -> ermite.layout('unified'); | 435 | k-unified from:body -> ermite.layout('unified'); | ||
| 430 | k-collapse from:body -> ermite.collapse(); | 436 | k-collapse from:body -> ermite.collapse(); | ||
| 437 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 431 | k-expand from:body -> ermite.expandFile(); | 438 | k-expand from:body -> ermite.expandFile(); | ||
| 432 | k-expandAll from:body -> ermite.expandAll(); | 439 | k-expandAll from:body -> ermite.expandAll(); | ||
| 433 | k-permalink from:body -> ermite.permalink(); | 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(); | |
| @@ -20,7 +20,7 @@ | |||||
| 20 | </script> | 20 | </script> | ||
| 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | 21 | <script src="/-/static/js/hx-sse.min.HASH.js" defer> | ||
| 22 | </script> | 22 | </script> | ||
| 23 | - | <script src="/-/static/js/store.HASH.js" defer> | 23 | + | <script src="/-/static/js/store.HASH.js"> |
| 24 | </script> | 24 | </script> | ||
| 25 | <script src="/-/static/js/ermite.HASH.js" defer> | 25 | <script src="/-/static/js/ermite.HASH.js" defer> | ||
| 26 | </script> | 26 | </script> | ||
| @@ -267,6 +267,12 @@ | |||||
| 267 | </div> | 267 | </div> | ||
| 268 | <div> | 268 | <div> | ||
| 269 | <dt> | 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 | <kbd>y</kbd> | 276 | <kbd>y</kbd> | ||
| 271 | </dt> | 277 | </dt> | ||
| 272 | <dd>copy a permalink pinned to the commit</dd> | 278 | <dd>copy a permalink pinned to the commit</dd> | ||
| @@ -444,6 +450,7 @@ | |||||
| 444 | k-split from:body -> ermite.layout('split'); | 450 | k-split from:body -> ermite.layout('split'); | ||
| 445 | k-unified from:body -> ermite.layout('unified'); | 451 | k-unified from:body -> ermite.layout('unified'); | ||
| 446 | k-collapse from:body -> ermite.collapse(); | 452 | k-collapse from:body -> ermite.collapse(); | ||
| 453 | + | k-wide from:body -> window.ermiteWide.toggle(); | |||
| 447 | k-expand from:body -> ermite.expandFile(); | 454 | k-expand from:body -> ermite.expandFile(); | ||
| 448 | k-expandAll from:body -> ermite.expandAll(); | 455 | k-expandAll from:body -> ermite.expandAll(); | ||
| 449 | k-permalink from:body -> ermite.permalink(); | 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 | |
| @@ -123,7 +123,7 @@ func TestWebEventsHome(t *testing.T) { | |||||
| 123 | assert.Equal(t, "home-list", ev.name) | 123 | assert.Equal(t, "home-list", ev.name) | ||
| 124 | assert.NotEmpty(t, ev.id) | 124 | assert.NotEmpty(t, ev.id) | ||
| 125 | assert.Contains(t, ev.data, `<li id="repo-pub"`) | 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 | assert.Contains(t, ev.data, `href="/pub"`) | 127 | assert.Contains(t, ev.data, `href="/pub"`) | ||
| 128 | assert.Contains(t, ev.data, `<ul class="repo-commits" aria-label="Recent commits of pub">`, "the live row lists the commits too") | 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 | assert.Equal(t, "home-list", ev.name) | 134 | assert.Equal(t, "home-list", ev.name) | ||
| 135 | assert.Contains(t, ev.data, `<li id="repo-fresh"`) | 135 | assert.Contains(t, ev.data, `<li id="repo-fresh"`) | ||
| 136 | assert.Contains(t, ev.data, "a <b>fresh</b> one", "text is escaped") | 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 | // A repository that turns private is removed from the anonymous list. | 139 | // A repository that turns private is removed from the anonymous list. | ||
| 140 | priv := domain.VisibilityPrivate | 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 | ||
| @@ -268,6 +268,8 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||||
| 268 | assert.Contains(t, line.body, `name="path" value="search.go"`) | 268 | assert.Contains(t, line.body, `name="path" value="search.go"`) | ||
| 269 | assert.Contains(t, line.body, `name="line" value="3"`) | 269 | assert.Contains(t, line.body, `name="line" value="3"`) | ||
| 270 | assert.Contains(t, line.body, `name="side" value="new"`) | 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 | 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") | 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 | assert.Equal(t, 0, strings.Count(b.get(base+"?on=new:3:search.go", nil).body, `<form`), "nor for a visitor") | 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 | |
| @@ -42,6 +42,7 @@ type Repo struct { | |||||
| 42 | Visibility Visibility | 42 | Visibility Visibility | ||
| 43 | CreatedAt time.Time | 43 | CreatedAt time.Time | ||
| 44 | PushedAt time.Time // zero until the first push | 44 | PushedAt time.Time // zero until the first push | ||
| 45 | + | ActiveAt time.Time // latest push, merge request or issue activity | |||
| 45 | // Settings are the merge settings; the stores always fill them in, with | 46 | // Settings are the merge settings; the stores always fill them in, with | ||
| 46 | // the defaults for a repository that never stored any. | 47 | // the defaults for a repository that never stored any. | ||
| 47 | Settings RepoSettings | 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 | |
| @@ -21,7 +21,7 @@ type RepoPage struct { | |||||
| 21 | 21 | ||||
| 22 | // RepoView is a repository with the Git state that belongs in its summary. | 22 | // RepoView is a repository with the Git state that belongs in its summary. | ||
| 23 | type RepoView struct { | 23 | type RepoView struct { | ||
| 24 | - | domain.Repo | 24 | + | domain.Repo // includes ActiveAt, the latest repository activity |
| 25 | // DefaultBranch is the branch HEAD points to, even when it does not exist | 25 | // DefaultBranch is the branch HEAD points to, even when it does not exist | ||
| 26 | // yet; empty when HEAD is detached or the directory is missing. | 26 | // yet; empty when HEAD is detached or the directory is missing. | ||
| 27 | DefaultBranch string | 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 | } | |
| @@ -289,59 +289,74 @@ func (_c *RepoMetaStore_List_Call) RunAndReturn(run func(ctx context.Context) ([ | |||||
| 289 | return _c | 289 | return _c | ||
| 290 | } | 290 | } | ||
| 291 | 291 | ||||
| 292 | - | // Update provides a mock function for the type RepoMetaStore | 292 | + | // Mutate provides a mock function for the type RepoMetaStore |
| 293 | - | func (_mock *RepoMetaStore) Update(ctx context.Context, repo domain.Repo) error { | 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, repo) | 294 | + | ret := _mock.Called(ctx, name, fn) |
| 295 | 295 | ||||
| 296 | if len(ret) == 0 { | 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 | 300 | + | var r0 domain.Repo |
| 301 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Repo) error); ok { | 301 | + | var r1 error |
| 302 | - | r0 = returnFunc(ctx, repo) | 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 | } else { | 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' | 318 | + | // RepoMetaStore_Mutate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mutate' |
| 310 | - | type RepoMetaStore_Update_Call struct { | 319 | + | type RepoMetaStore_Mutate_Call struct { |
| 311 | *mock.Call | 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 | // - ctx context.Context | 324 | // - ctx context.Context | ||
| 316 | - | // - repo domain.Repo | 325 | + | // - name domain.RepoName |
| 317 | - | func (_e *RepoMetaStore_Expecter) Update(ctx any, repo any) *RepoMetaStore_Update_Call { | 326 | + | // - fn func(*domain.Repo) (changed bool, err error) |
| 318 | - | return &RepoMetaStore_Update_Call{Call: _e.mock.On("Update", ctx, repo)} | 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 | _c.Call.Run(func(args mock.Arguments) { | 332 | _c.Call.Run(func(args mock.Arguments) { | ||
| 323 | var arg0 context.Context | 333 | var arg0 context.Context | ||
| 324 | if args[0] != nil { | 334 | if args[0] != nil { | ||
| 325 | arg0 = args[0].(context.Context) | 335 | arg0 = args[0].(context.Context) | ||
| 326 | } | 336 | } | ||
| 327 | - | var arg1 domain.Repo | 337 | + | var arg1 domain.RepoName |
| 328 | if args[1] != nil { | 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 | run( | 345 | run( | ||
| 332 | arg0, | 346 | arg0, | ||
| 333 | arg1, | 347 | arg1, | ||
| 348 | + | arg2, | |||
| 334 | ) | 349 | ) | ||
| 335 | }) | 350 | }) | ||
| 336 | return _c | 351 | return _c | ||
| 337 | } | 352 | } | ||
| 338 | 353 | ||||
| 339 | - | func (_c *RepoMetaStore_Update_Call) Return(err error) *RepoMetaStore_Update_Call { | 354 | + | func (_c *RepoMetaStore_Mutate_Call) Return(repo domain.Repo, err error) *RepoMetaStore_Mutate_Call { |
| 340 | - | _c.Call.Return(err) | 355 | + | _c.Call.Return(repo, err) |
| 341 | return _c | 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 | _c.Call.Return(run) | 360 | _c.Call.Return(run) | ||
| 346 | return _c | 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 | |
| @@ -32,10 +32,13 @@ type RepoMetaStore interface { | |||||
| 32 | Get(ctx context.Context, name domain.RepoName) (domain.Repo, error) | 32 | Get(ctx context.Context, name domain.RepoName) (domain.Repo, error) | ||
| 33 | // List returns every repository ordered by name. | 33 | // List returns every repository ordered by name. | ||
| 34 | List(ctx context.Context) ([]domain.Repo, error) | 34 | List(ctx context.Context) ([]domain.Repo, error) | ||
| 35 | - | // Update replaces the stored repository. It returns domain.ErrNotFound if | 35 | + | // Mutate applies fn to the stored repository and writes under the version read. |
| 36 | - | // there is none and domain.ErrConflict if another write changed the | 36 | + | // Concurrent writes retry fn up to a bounded number of attempts, then return |
| 37 | - | // stored repository since it was read. | 37 | + | // domain.ErrConflict. fn must be pure and safe to call several times, and |
| 38 | - | Update(ctx context.Context, repo domain.Repo) error | 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 | // Delete removes the repository and everything keyed to it: its merge | 42 | // Delete removes the repository and everything keyed to it: its merge | ||
| 40 | // requests, their comments and read positions, and its number sequence. | 43 | // requests, their comments and read positions, and its number sequence. | ||
| 41 | // A repository created again under the same name starts empty, with its | 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 | + | } | |
| @@ -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 +45 -0
| @@ -0,0 +1,45 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package usecase | |
| 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 | + | ) | |
| 14 | + | ||
| 15 | + | func TestActivityTimestampsOverStore(t *testing.T) { | |
| 16 | + | ctx := t.Context() | |
| 17 | + | s, err := kvstore.Open(ctx, t.TempDir()) | |
| 18 | + | require.NoError(t, err) | |
| 19 | + | t.Cleanup(func() { require.NoError(t, s.Close(ctx)) }) | |
| 20 | + | n, err := domain.ParseRepoName("proj") | |
| 21 | + | require.NoError(t, err) | |
| 22 | + | at := time.Date(2026, 10, 19, 12, 0, 0, 0, time.UTC) | |
| 23 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: at})) | |
| 24 | + | got, err := touchPushedAt(ctx, s, n, at.Add(time.Hour)) | |
| 25 | + | require.NoError(t, err) | |
| 26 | + | require.Equal(t, got.PushedAt, got.ActiveAt) | |
| 27 | + | _, err = touchActiveAt(ctx, s, n, at.Add(3*time.Hour)) | |
| 28 | + | require.NoError(t, err) | |
| 29 | + | _, err = touchPushedAt(ctx, s, n, at.Add(2*time.Hour)) | |
| 30 | + | require.NoError(t, err) | |
| 31 | + | _, err = touchPushedAt(ctx, s, n, at) | |
| 32 | + | require.NoError(t, err) | |
| 33 | + | _, err = touchActiveAt(ctx, s, n, at) | |
| 34 | + | require.NoError(t, err) | |
| 35 | + | got, err = s.Get(ctx, n) | |
| 36 | + | require.NoError(t, err) | |
| 37 | + | require.Equal(t, at.Add(2*time.Hour), got.PushedAt) | |
| 38 | + | require.Equal(t, at.Add(3*time.Hour), got.ActiveAt) | |
| 39 | + | require.Equal(t, domain.VisibilityPrivate, got.Visibility) | |
| 40 | + | require.NoError(t, s.Delete(ctx, n)) | |
| 41 | + | _, err = touchPushedAt(ctx, s, n, at) | |
| 42 | + | require.NoError(t, err) | |
| 43 | + | _, err = touchActiveAt(ctx, s, n, at) | |
| 44 | + | require.NoError(t, err) | |
| 45 | + | } | |
| @@ -0,0 +1,45 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package usecase | |||
| 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 | + | ) | |||
| 14 | + | ||||
| 15 | + | func TestActivityTimestampsOverStore(t *testing.T) { | |||
| 16 | + | ctx := t.Context() | |||
| 17 | + | s, err := kvstore.Open(ctx, t.TempDir()) | |||
| 18 | + | require.NoError(t, err) | |||
| 19 | + | t.Cleanup(func() { require.NoError(t, s.Close(ctx)) }) | |||
| 20 | + | n, err := domain.ParseRepoName("proj") | |||
| 21 | + | require.NoError(t, err) | |||
| 22 | + | at := time.Date(2026, 10, 19, 12, 0, 0, 0, time.UTC) | |||
| 23 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: at})) | |||
| 24 | + | got, err := touchPushedAt(ctx, s, n, at.Add(time.Hour)) | |||
| 25 | + | require.NoError(t, err) | |||
| 26 | + | require.Equal(t, got.PushedAt, got.ActiveAt) | |||
| 27 | + | _, err = touchActiveAt(ctx, s, n, at.Add(3*time.Hour)) | |||
| 28 | + | require.NoError(t, err) | |||
| 29 | + | _, err = touchPushedAt(ctx, s, n, at.Add(2*time.Hour)) | |||
| 30 | + | require.NoError(t, err) | |||
| 31 | + | _, err = touchPushedAt(ctx, s, n, at) | |||
| 32 | + | require.NoError(t, err) | |||
| 33 | + | _, err = touchActiveAt(ctx, s, n, at) | |||
| 34 | + | require.NoError(t, err) | |||
| 35 | + | got, err = s.Get(ctx, n) | |||
| 36 | + | require.NoError(t, err) | |||
| 37 | + | require.Equal(t, at.Add(2*time.Hour), got.PushedAt) | |||
| 38 | + | require.Equal(t, at.Add(3*time.Hour), got.ActiveAt) | |||
| 39 | + | require.Equal(t, domain.VisibilityPrivate, got.Visibility) | |||
| 40 | + | require.NoError(t, s.Delete(ctx, n)) | |||
| 41 | + | _, err = touchPushedAt(ctx, s, n, at) | |||
| 42 | + | require.NoError(t, err) | |||
| 43 | + | _, err = touchActiveAt(ctx, s, n, at) | |||
| 44 | + | require.NoError(t, err) | |||
| 45 | + | } | |||
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 | + | } | |
| @@ -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/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 | } | |
| @@ -85,6 +85,7 @@ func (m *IssueModeration) Approve(ctx context.Context, p domain.Principal, repo | |||||
| 85 | log := zerowrap.FromCtx(ctx) | 85 | log := zerowrap.FromCtx(ctx) | ||
| 86 | log.Info().Str("repo", repo.String()).Int64("number", issue.Number). | 86 | log.Info().Str("repo", repo.String()).Int64("number", issue.Number). | ||
| 87 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("pending issue approved") | 87 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("pending issue approved") | ||
| 88 | + | recordActivity(ctx, m.meta, repo, now) | |||
| 88 | m.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: r.Visibility, CreatedAt: r.CreatedAt}) | 89 | m.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: r.Visibility, CreatedAt: r.CreatedAt}) | ||
| 89 | return issue, nil | 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) | |
| @@ -89,6 +89,7 @@ func TestIssueModerationApprove(t *testing.T) { | |||||
| 89 | u := newModerationUnit(t, domain.IssuesPublic) | 89 | u := newModerationUnit(t, domain.IssuesPublic) | ||
| 90 | issue := domain.Issue{Repo: u.repo.Name, Number: 7, State: domain.IssueStateOpen} | 90 | issue := domain.Issue{Repo: u.repo.Name, Number: 7, State: domain.IssueStateOpen} | ||
| 91 | u.pending.EXPECT().Approve(mock.Anything, u.repo.Name, u.id, now).Return(issue, nil).Once() | 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 | u.bus.EXPECT().Publish(mock.Anything, domain.IssueOpened{Issue: issue, At: now, Visibility: u.repo.Visibility, CreatedAt: u.repo.CreatedAt}).Once() | 93 | u.bus.EXPECT().Publish(mock.Anything, domain.IssueOpened{Issue: issue, At: now, Visibility: u.repo.Visibility, CreatedAt: u.repo.CreatedAt}).Once() | ||
| 93 | got, err := u.mod.Approve(ctx, domain.Owner(), u.repo.Name, u.id) | 94 | got, err := u.mod.Approve(ctx, domain.Owner(), u.repo.Name, u.id) | ||
| 94 | require.NoError(t, err) | 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 { | |
| @@ -124,6 +124,7 @@ func (i *Issues) Create(ctx context.Context, p domain.Principal, name domain.Rep | |||||
| 124 | log := zerowrap.FromCtx(ctx) | 124 | log := zerowrap.FromCtx(ctx) | ||
| 125 | log.Info().Str("repo", name.String()).Int64("number", issue.Number). | 125 | log.Info().Str("repo", name.String()).Int64("number", issue.Number). | ||
| 126 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue opened") | 126 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue opened") | ||
| 127 | + | recordActivity(ctx, i.meta, repo.Name, now) | |||
| 127 | i.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 128 | i.bus.Publish(ctx, domain.IssueOpened{Issue: issue, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 128 | return issue, nil | 129 | return issue, nil | ||
| 129 | } | 130 | } | ||
| @@ -211,6 +212,7 @@ func (i *Issues) AddComment(ctx context.Context, p domain.Principal, name domain | |||||
| 211 | log := zerowrap.FromCtx(ctx) | 212 | log := zerowrap.FromCtx(ctx) | ||
| 212 | log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID). | 213 | log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID). | ||
| 213 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue comment added") | 214 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue comment added") | ||
| 215 | + | recordActivity(ctx, i.meta, name, c.CreatedAt) | |||
| 214 | i.bus.Publish(ctx, domain.IssueCommented{ | 216 | i.bus.Publish(ctx, domain.IssueCommented{ | ||
| 215 | Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | 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 | log := zerowrap.FromCtx(ctx) | 262 | log := zerowrap.FromCtx(ctx) | ||
| 261 | log.Info().Str("repo", name.String()).Int64("number", number). | 263 | log.Info().Str("repo", name.String()).Int64("number", number). | ||
| 262 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue " + verb + "d") | 264 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("issue " + verb + "d") | ||
| 265 | + | recordActivity(ctx, i.meta, repo.Name, issue.UpdatedAt) | |||
| 263 | if to == domain.IssueStateOpen { | 266 | if to == domain.IssueStateOpen { | ||
| 264 | i.bus.Publish(ctx, domain.IssueReopened{Issue: issue, At: issue.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 267 | i.bus.Publish(ctx, domain.IssueReopened{Issue: issue, At: issue.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 265 | } else { | 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 | + | } | |
| @@ -6,14 +6,17 @@ import ( | |||||
| 6 | "context" | 6 | "context" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | "testing" | 8 | "testing" | ||
| 9 | + | "time" | |||
| 9 | 10 | ||||
| 10 | "github.com/stretchr/testify/assert" | 11 | "github.com/stretchr/testify/assert" | ||
| 12 | + | "github.com/stretchr/testify/mock" | |||
| 11 | "github.com/stretchr/testify/require" | 13 | "github.com/stretchr/testify/require" | ||
| 12 | 14 | ||||
| 13 | "bnema.dev/ermite/internal/adapters/eventbus" | 15 | "bnema.dev/ermite/internal/adapters/eventbus" | ||
| 14 | "bnema.dev/ermite/internal/adapters/kvstore" | 16 | "bnema.dev/ermite/internal/adapters/kvstore" | ||
| 15 | "bnema.dev/ermite/internal/domain" | 17 | "bnema.dev/ermite/internal/domain" | ||
| 16 | "bnema.dev/ermite/internal/ports" | 18 | "bnema.dev/ermite/internal/ports" | ||
| 19 | + | "bnema.dev/ermite/internal/ports/mocks" | |||
| 17 | "bnema.dev/ermite/internal/usecase" | 20 | "bnema.dev/ermite/internal/usecase" | ||
| 18 | ) | 21 | ) | ||
| 19 | 22 | ||||
| @@ -46,6 +49,10 @@ func TestIssuesOverTheStore(t *testing.T) { | |||||
| 46 | require.NoError(t, err) | 49 | require.NoError(t, err) | ||
| 47 | assert.EqualValues(t, 2, issue.Number, "the number follows the merge request") | 50 | assert.EqualValues(t, 2, issue.Number, "the number follows the merge request") | ||
| 48 | assert.Equal(t, "bot", issue.Author) | 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 | kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1) | 57 | kind, err := issues.Kind(ctx, domain.Anonymous(), n, 1) | ||
| 51 | require.NoError(t, err) | 58 | require.NoError(t, err) | ||
| @@ -85,3 +92,63 @@ func TestIssuesOverTheStore(t *testing.T) { | |||||
| 85 | } | 92 | } | ||
| 86 | assert.Equal(t, []string{"issue.opened", "issue.commented", "issue.closed", "issue.commented", "issue.reopened"}, kinds) | 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) { | |
| @@ -39,6 +39,7 @@ func newIssueUnit(t *testing.T, vis domain.Visibility, mode domain.IssuesMode) * | |||||
| 39 | clock := mocks.NewClock(t) | 39 | clock := mocks.NewClock(t) | ||
| 40 | clock.EXPECT().Now().Return(now).Maybe() | 40 | clock.EXPECT().Now().Return(now).Maybe() | ||
| 41 | u.meta.EXPECT().Get(mock.Anything, u.repo.Name).Return(u.repo, nil).Maybe() | 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 | u.issues = usecase.NewIssues(usecase.IssuesConfig{Meta: u.meta, Store: u.store, MergeRequests: u.mrs, Bus: u.bus, Clock: clock}) | 43 | u.issues = usecase.NewIssues(usecase.IssuesConfig{Meta: u.meta, Store: u.store, MergeRequests: u.mrs, Bus: u.bus, Clock: clock}) | ||
| 43 | return u | 44 | return u | ||
| 44 | } | 45 | } | ||
| @@ -116,6 +117,7 @@ func TestIssueCreateAndTransitions(t *testing.T) { | |||||
| 116 | got, err := u.issues.Create(ctx, p, u.repo.Name, ports.CreateIssueParams{Title: " Crash ", Body: "why"}) | 117 | got, err := u.issues.Create(ctx, p, u.repo.Name, ports.CreateIssueParams{Title: " Crash ", Body: "why"}) | ||
| 117 | require.NoError(t, err) | 118 | require.NoError(t, err) | ||
| 118 | assert.EqualValues(t, 7, got.Number) | 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 | t.Run("invalid text stores nothing", func(t *testing.T) { | 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, | |
| @@ -254,6 +254,7 @@ func (m *MergeRequests) Create(ctx context.Context, p domain.Principal, name dom | |||||
| 254 | if err != nil { | 254 | if err != nil { | ||
| 255 | return domain.MergeRequest{}, err | 255 | return domain.MergeRequest{}, err | ||
| 256 | } | 256 | } | ||
| 257 | + | recordActivity(ctx, m.meta, repo.Name, now) | |||
| 257 | m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 258 | m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 258 | if m.pulls != nil { | 259 | if m.pulls != nil { | ||
| 259 | // The commits the opening is for are the ones of now. | 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 | if err != nil { | 378 | if err != nil { | ||
| 378 | return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err) | 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 | m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 382 | m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 381 | return m.withHead(ctx, mr) | 383 | return m.withHead(ctx, mr) | ||
| 382 | } | 384 | } | ||
| @@ -656,6 +658,7 @@ func (m *MergeRequests) AddComment(ctx context.Context, p domain.Principal, name | |||||
| 656 | log := zerowrap.FromCtx(ctx) | 658 | log := zerowrap.FromCtx(ctx) | ||
| 657 | log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID). | 659 | log.Info().Str("repo", name.String()).Int64("number", number).Int64("comment", c.ID). | ||
| 658 | 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") | 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 | m.bus.Publish(ctx, domain.CommentAdded{ | 662 | m.bus.Publish(ctx, domain.CommentAdded{ | ||
| 660 | Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | 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 | log := zerowrap.FromCtx(ctx) | 799 | log := zerowrap.FromCtx(ctx) | ||
| 797 | log.Info().Str("repo", name.String()).Int64("number", number). | 800 | log.Info().Str("repo", name.String()).Int64("number", number). | ||
| 798 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request closed") | 801 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request closed") | ||
| 802 | + | recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt) | |||
| 799 | m.bus.Publish(ctx, domain.MergeRequestClosed{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 803 | m.bus.Publish(ctx, domain.MergeRequestClosed{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 800 | if m.pulls != nil { | 804 | if m.pulls != nil { | ||
| 801 | m.pulls.OnPullRequest(ctx, name, ports.PullRequestEvent{Number: number, Action: domain.PullClosed, Head: mr.HeadID, Base: mr.BaseID}, p) | 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 | at := mr.UpdatedAt | 1004 | at := mr.UpdatedAt | ||
| 1005 | + | recordActivity(ctx, m.meta, repo.Name, at) | |||
| 1001 | m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 1006 | m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 1002 | m.bus.Publish(ctx, domain.PushEvent{ | 1007 | m.bus.Publish(ctx, domain.PushEvent{ | ||
| 1003 | Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates, | 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 | + | } | |
| @@ -1448,3 +1448,14 @@ func TestNoteThreadsRangesAndVerdicts(t *testing.T) { | |||||
| 1448 | _, err = r.mrs.DeleteComment(r.ctx, root, r.repo, mr.Number, changes.ID) | 1448 | _, err = r.mrs.DeleteComment(r.ctx, root, r.repo, mr.Number, changes.ID) | ||
| 1449 | assert.ErrorIs(t, err, domain.ErrPreconditionFailed) | 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 | |
| @@ -15,9 +15,6 @@ import ( | |||||
| 15 | "bnema.dev/ermite/internal/ports" | 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 | // Pushes are the use cases around a Git push. | 18 | // Pushes are the use cases around a Git push. | ||
| 22 | type Pushes struct { | 19 | type Pushes struct { | ||
| 23 | meta ports.RepoMetaStore | 20 | meta ports.RepoMetaStore | ||
| @@ -209,7 +206,7 @@ func (u *Pushes) checkWorkflows(ctx context.Context, name domain.RepoName, updat | |||||
| 209 | return nil | 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 | // repairs HEAD when it points to a missing branch, tells the reader that the | 210 | // repairs HEAD when it points to a missing branch, tells the reader that the | ||
| 214 | // repository changed, queues the workflows the push triggers, publishes a | 211 | // repository changed, queues the workflows the push triggers, publishes a | ||
| 215 | // PushEvent and schedules maintenance. Every | 212 | // PushEvent and schedules maintenance. Every | ||
| @@ -245,36 +242,31 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep | |||||
| 245 | return errors.Join(errs...) | 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 | // A repository deleted meanwhile is not an error. It returns the repository | 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 | 248 | + | // as stored after mutation, or the zero value when it is unknown. A push |
| 251 | - | // merge, which moves a branch too, both record through it. | 249 | + | // (Pushes.Record) and a merge, which moves a branch too, both record through it. |
| 252 | func touchPushedAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) { | 250 | func touchPushedAt(ctx context.Context, meta ports.RepoMetaStore, name domain.RepoName, now time.Time) (domain.Repo, error) { | ||
| 253 | - | for range pushRetries { | 251 | + | repo, err := meta.Mutate(ctx, name, func(repo *domain.Repo) (bool, error) { |
| 254 | - | repo, err := meta.Get(ctx, name) | 252 | + | changed := false |
| 255 | - | if errors.Is(err, domain.ErrNotFound) { | 253 | + | if now.After(repo.PushedAt) { |
| 256 | - | return domain.Repo{}, nil | 254 | + | repo.PushedAt = now |
| 257 | - | } | 255 | + | changed = true |
| 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 | |||
| 268 | } | 256 | } | ||
| 269 | - | if errors.Is(err, domain.ErrNotFound) { | 257 | + | if now.After(repo.ActiveAt) { |
| 270 | - | return domain.Repo{}, nil | 258 | + | repo.ActiveAt = now |
| 259 | + | changed = true | |||
| 271 | } | 260 | } | ||
| 272 | - | if err != nil { | 261 | + | return changed, nil |
| 273 | - | return repo, fmt.Errorf("update repository %q: %w", name, err) | 262 | + | }) |
| 274 | - | } | 263 | + | if errors.Is(err, domain.ErrNotFound) { |
| 275 | - | return repo, nil | 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 | // fixDefaultBranch points HEAD to a branch that exists when it points to a | 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() | |
| @@ -305,8 +305,7 @@ func TestPushesRecord(t *testing.T) { | |||||
| 305 | f := newPushFixture(t) | 305 | f := newPushFixture(t) | ||
| 306 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 306 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | ||
| 307 | repo := domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: now.Add(-time.Hour)} | 307 | repo := domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: now.Add(-time.Hour)} | ||
| 308 | - | f.meta.EXPECT().Get(ctx, n).Return(repo, nil) | 308 | + | expectRepoMutation(t, f.meta, repo, domain.Repo{Name: n, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, PushedAt: now, ActiveAt: now}).Once() |
| 309 | - | f.meta.EXPECT().Update(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: repo.CreatedAt, PushedAt: now}).Return(nil) | |||
| 310 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | 309 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | ||
| 311 | f.bus.EXPECT().Publish(ctx, domain.PushEvent{Repo: n, Updates: ups, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}).Return() | 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 | fresh := mocks.NewRepoFreshener(t) | 319 | fresh := mocks.NewRepoFreshener(t) | ||
| 321 | fresh.EXPECT().Refresh(ctx, n).Return().Once() | 320 | fresh.EXPECT().Refresh(ctx, n).Return().Once() | ||
| 322 | f.pushes.WithFreshener(fresh) | 321 | f.pushes.WithFreshener(fresh) | ||
| 323 | - | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n}, nil) | 322 | + | expectPushMutation(t, f.meta, domain.Repo{Name: n}, nil) |
| 324 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil) | |||
| 325 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | 323 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | ||
| 326 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 324 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | ||
| 327 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | 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 | t.Parallel() | 329 | t.Parallel() | ||
| 332 | f := newPushFixture(t) | 330 | f := newPushFixture(t) | ||
| 333 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 331 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | ||
| 334 | - | repo := domain.Repo{Name: n} | 332 | + | later := domain.Repo{Name: n, PushedAt: now.Add(time.Second), ActiveAt: now.Add(time.Second)} |
| 335 | - | f.meta.EXPECT().Get(ctx, n).Return(repo, nil).Once() | 333 | + | expectPushMutation(t, f.meta, later, nil) // a later push recorded meanwhile |
| 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 | |||
| 339 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | 334 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | ||
| 340 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 335 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | ||
| 341 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | 336 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | ||
| @@ -345,7 +340,7 @@ func TestPushesRecord(t *testing.T) { | |||||
| 345 | t.Parallel() | 340 | t.Parallel() | ||
| 346 | f := newPushFixture(t) | 341 | f := newPushFixture(t) | ||
| 347 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 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 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | 344 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | ||
| 350 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 345 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | ||
| 351 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | 346 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | ||
| @@ -355,7 +350,7 @@ func TestPushesRecord(t *testing.T) { | |||||
| 355 | t.Parallel() | 350 | t.Parallel() | ||
| 356 | f := newPushFixture(t) | 351 | f := newPushFixture(t) | ||
| 357 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 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 | f.git.EXPECT().DefaultBranch(ctx, n).Return("", false, domain.ErrNotFound) | 354 | f.git.EXPECT().DefaultBranch(ctx, n).Return("", false, domain.ErrNotFound) | ||
| 360 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 355 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | ||
| 361 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | 356 | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | ||
| @@ -365,7 +360,7 @@ func TestPushesRecord(t *testing.T) { | |||||
| 365 | t.Parallel() | 360 | t.Parallel() | ||
| 366 | f := newPushFixture(t) | 361 | f := newPushFixture(t) | ||
| 367 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 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 | f.git.EXPECT().DefaultBranch(ctx, n).Return("trunk", false, nil) | 364 | f.git.EXPECT().DefaultBranch(ctx, n).Return("trunk", false, nil) | ||
| 370 | f.git.EXPECT().SetDefaultBranch(ctx, n, "main").Return(domain.ErrNotFound) | 365 | f.git.EXPECT().SetDefaultBranch(ctx, n, "main").Return(domain.ErrNotFound) | ||
| 371 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 366 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | ||
| @@ -377,7 +372,7 @@ func TestPushesRecord(t *testing.T) { | |||||
| 377 | f := newPushFixture(t) | 372 | f := newPushFixture(t) | ||
| 378 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 373 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | ||
| 379 | boom := errors.New("boom") | 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 | f.git.EXPECT().DefaultBranch(ctx, n).Return("", false, boom) | 376 | f.git.EXPECT().DefaultBranch(ctx, n).Return("", false, boom) | ||
| 382 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 377 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | ||
| 383 | err := f.pushes.Record(ctx, p, n, ups) | 378 | err := f.pushes.Record(ctx, p, n, ups) | ||
| @@ -414,7 +409,7 @@ func TestPushesDefaultBranchRule(t *testing.T) { | |||||
| 414 | t.Parallel() | 409 | t.Parallel() | ||
| 415 | f := newPushFixture(t) | 410 | f := newPushFixture(t) | ||
| 416 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 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 | f.git.EXPECT().DefaultBranch(ctx, n).Return(tt.head, tt.exists, nil) | 413 | f.git.EXPECT().DefaultBranch(ctx, n).Return(tt.head, tt.exists, nil) | ||
| 419 | if tt.want != "" { | 414 | if tt.want != "" { | ||
| 420 | f.git.EXPECT().SetDefaultBranch(ctx, n, tt.want).Return(nil) | 415 | f.git.EXPECT().SetDefaultBranch(ctx, n, tt.want).Return(nil) | ||
| @@ -429,7 +424,7 @@ func TestPushesDefaultBranchRule(t *testing.T) { | |||||
| 429 | f := newPushFixture(t) | 424 | f := newPushFixture(t) | ||
| 430 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | 425 | f.sched.EXPECT().Schedule(ctx, n).Return().Once() | ||
| 431 | boom := errors.New("boom") | 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 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", false, nil) | 428 | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", false, nil) | ||
| 434 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom) | 429 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom) | ||
| 435 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | 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 | + | } | |
| @@ -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 | |
| @@ -155,11 +155,7 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||||
| 155 | // updateMeta applies the metadata fields, retrying when a write (a push | 155 | // updateMeta applies the metadata fields, retrying when a write (a push | ||
| 156 | // recording pushed_at) gets in between. | 156 | // recording pushed_at) gets in between. | ||
| 157 | func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params ports.UpdateRepoParams) (domain.Repo, error) { | 157 | func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params ports.UpdateRepoParams) (domain.Repo, error) { | ||
| 158 | - | for range pushRetries { | 158 | + | repo, err := r.meta.Mutate(ctx, name, func(repo *domain.Repo) (bool, error) { |
| 159 | - | repo, err := r.meta.Get(ctx, name) | |||
| 160 | - | if err != nil { | |||
| 161 | - | return domain.Repo{}, err | |||
| 162 | - | } | |||
| 163 | if params.Description != nil { | 159 | if params.Description != nil { | ||
| 164 | repo.Description = *params.Description | 160 | repo.Description = *params.Description | ||
| 165 | } | 161 | } | ||
| @@ -181,16 +177,12 @@ func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params por | |||||
| 181 | if params.Issues != nil { | 177 | if params.Issues != nil { | ||
| 182 | repo.Settings.Issues = *params.Issues | 178 | repo.Settings.Issues = *params.Issues | ||
| 183 | } | 179 | } | ||
| 184 | - | err = r.meta.Update(ctx, repo) | 180 | + | return true, nil |
| 185 | - | if errors.Is(err, domain.ErrConflict) { | 181 | + | }) |
| 186 | - | continue | 182 | + | if err != nil { |
| 187 | - | } | 183 | + | return domain.Repo{}, fmt.Errorf("update repository %q: %w", name, err) |
| 188 | - | if err != nil { | |||
| 189 | - | return domain.Repo{}, fmt.Errorf("update repository %q: %w", name, err) | |||
| 190 | - | } | |||
| 191 | - | return repo, nil | |||
| 192 | } | 184 | } | ||
| 193 | - | return domain.Repo{}, fmt.Errorf("update repository %q: %w", name, domain.ErrConflict) | 185 | + | return repo, nil |
| 194 | } | 186 | } | ||
| 195 | 187 | ||||
| 196 | // Delete moves the repository directory to the trash, removes its metadata | 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) | |
| @@ -30,9 +30,9 @@ func TestUpdate(t *testing.T) { | |||||
| 30 | t.Run("description and visibility are stored and RepoUpdated is published", func(t *testing.T) { | 30 | t.Run("description and visibility are stored and RepoUpdated is published", func(t *testing.T) { | ||
| 31 | t.Parallel() | 31 | t.Parallel() | ||
| 32 | f := newFixture(t) | 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 | want := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, Description: "new", Settings: domain.DefaultRepoSettings()} | 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 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | 36 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | ||
| 37 | Repo: n, At: now, Visibility: domain.VisibilityPublic, Previous: domain.VisibilityPrivate, | 37 | Repo: n, At: now, Visibility: domain.VisibilityPublic, Previous: domain.VisibilityPrivate, | ||
| 38 | }).Return().Once() | 38 | }).Return().Once() | ||
| @@ -47,11 +47,11 @@ func TestUpdate(t *testing.T) { | |||||
| 47 | t.Run("topics are normalized and stored, and RepoUpdated is published", func(t *testing.T) { | 47 | t.Run("topics are normalized and stored, and RepoUpdated is published", func(t *testing.T) { | ||
| 48 | t.Parallel() | 48 | t.Parallel() | ||
| 49 | f := newFixture(t) | 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 | want := priv | 51 | want := priv | ||
| 52 | want.Topics = []string{"cli", "go"} | 52 | want.Topics = []string{"cli", "go"} | ||
| 53 | want.Settings = domain.DefaultRepoSettings() | 53 | want.Settings = domain.DefaultRepoSettings() | ||
| 54 | - | f.meta.EXPECT().Update(ctx, want).Return(nil) | 54 | + | expectRepoMutation(t, f.meta, priv, want) |
| 55 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | 55 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | ||
| 56 | Repo: n, At: now, Visibility: domain.VisibilityPrivate, Previous: domain.VisibilityPrivate, | 56 | Repo: n, At: now, Visibility: domain.VisibilityPrivate, Previous: domain.VisibilityPrivate, | ||
| 57 | }).Return().Once() | 57 | }).Return().Once() | ||
| @@ -66,10 +66,10 @@ func TestUpdate(t *testing.T) { | |||||
| 66 | f := newFixture(t) | 66 | f := newFixture(t) | ||
| 67 | withTopics := priv | 67 | withTopics := priv | ||
| 68 | withTopics.Topics = []string{"go"} | 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 | want := priv | 70 | want := priv | ||
| 71 | want.Settings = domain.DefaultRepoSettings() | 71 | want.Settings = domain.DefaultRepoSettings() | ||
| 72 | - | f.meta.EXPECT().Update(ctx, want).Return(nil) | 72 | + | expectRepoMutation(t, f.meta, withTopics, want) |
| 73 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | 73 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | ||
| 74 | 74 | ||||
| 75 | got, err := f.repos.Update(ctx, admin, n, ports.UpdateRepoParams{Topics: &[]string{}}) | 75 | got, err := f.repos.Update(ctx, admin, n, ports.UpdateRepoParams{Topics: &[]string{}}) | ||
| @@ -77,15 +77,12 @@ func TestUpdate(t *testing.T) { | |||||
| 77 | assert.Nil(t, got.Topics) | 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) { | 80 | + | t.Run("exhausted mutation conflicts are returned", func(t *testing.T) { |
| 81 | - | t.Parallel() | |||
| 82 | f := newFixture(t) | 81 | f := newFixture(t) | ||
| 83 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil) | 82 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil) | ||
| 84 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(domain.ErrConflict).Once() | 83 | + | f.meta.EXPECT().Mutate(ctx, n, mock.Anything).Return(domain.Repo{}, domain.ErrConflict).Once() |
| 85 | - | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil).Once() | |||
| 86 | - | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | |||
| 87 | _, err := f.repos.Update(ctx, admin, n, ports.UpdateRepoParams{Description: ptr("x")}) | 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 | t.Run("the default branch is checked, then HEAD moves", func(t *testing.T) { | 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 | var order []string | 103 | var order []string | ||
| 107 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil) | 104 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil) | ||
| 108 | f.git.EXPECT().HasBranch(ctx, n, "dev").Return(true, nil) | 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 | order = append(order, "meta") | 107 | order = append(order, "meta") | ||
| 111 | - | return nil | 108 | + | repo := priv |
| 109 | + | _, err := fn(&repo) | |||
| 110 | + | return repo, err | |||
| 112 | }) | 111 | }) | ||
| 113 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").RunAndReturn(func(context.Context, domain.RepoName, string) error { | 112 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").RunAndReturn(func(context.Context, domain.RepoName, string) error { | ||
| 114 | order = append(order, "head") | 113 | order = append(order, "head") | ||
| @@ -127,7 +126,7 @@ func TestUpdate(t *testing.T) { | |||||
| 127 | pub := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, Description: "old"} | 126 | pub := domain.Repo{Name: n, Visibility: domain.VisibilityPublic, Description: "old"} | ||
| 128 | f.meta.EXPECT().Get(ctx, n).Return(pub, nil) | 127 | f.meta.EXPECT().Get(ctx, n).Return(pub, nil) | ||
| 129 | f.git.EXPECT().HasBranch(ctx, n, "dev").Return(true, nil) | 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 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom) | 130 | f.git.EXPECT().SetDefaultBranch(ctx, n, "dev").Return(boom) | ||
| 132 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | 131 | f.bus.EXPECT().Publish(ctx, domain.RepoUpdated{ | ||
| 133 | Repo: n, At: now, Visibility: domain.VisibilityPrivate, Previous: domain.VisibilityPublic, | 132 | Repo: n, At: now, Visibility: domain.VisibilityPrivate, Previous: domain.VisibilityPublic, | ||
| @@ -250,10 +249,10 @@ func TestUpdateIssuesMode(t *testing.T) { | |||||
| 250 | t.Run("public is stored and published", func(t *testing.T) { | 249 | t.Run("public is stored and published", func(t *testing.T) { | ||
| 251 | t.Parallel() | 250 | t.Parallel() | ||
| 252 | f := newFixture(t) | 251 | f := newFixture(t) | ||
| 253 | - | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Twice() | 252 | + | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() |
| 254 | - | f.meta.EXPECT().Update(ctx, mock.MatchedBy(func(r domain.Repo) bool { | 253 | + | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { |
| 255 | return r.Settings.Issues == domain.IssuesPublic | 254 | return r.Settings.Issues == domain.IssuesPublic | ||
| 256 | - | })).Return(nil) | 255 | + | })) |
| 257 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | 256 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | ||
| 258 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesPublic)}) | 257 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesPublic)}) | ||
| 259 | require.NoError(t, err) | 258 | require.NoError(t, err) | ||
| @@ -271,10 +270,10 @@ func TestUpdateIssuesMode(t *testing.T) { | |||||
| 271 | t.Run("owner is stored and published", func(t *testing.T) { | 270 | t.Run("owner is stored and published", func(t *testing.T) { | ||
| 272 | t.Parallel() | 271 | t.Parallel() | ||
| 273 | f := newFixture(t) | 272 | f := newFixture(t) | ||
| 274 | - | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Twice() | 273 | + | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() |
| 275 | - | f.meta.EXPECT().Update(ctx, mock.MatchedBy(func(r domain.Repo) bool { | 274 | + | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { |
| 276 | return r.Settings.Issues == domain.IssuesOwner && r.Settings.FailingChecksBlock | 275 | return r.Settings.Issues == domain.IssuesOwner && r.Settings.FailingChecksBlock | ||
| 277 | - | })).Return(nil) | 276 | + | })) |
| 278 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | 277 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | ||
| 279 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesOwner)}) | 278 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesOwner)}) | ||
| 280 | require.NoError(t, err) | 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". | |
| @@ -32,6 +32,8 @@ message Repo { | |||||
| 32 | RepoSettings settings = 6; | 32 | RepoSettings settings = 6; | ||
| 33 | // Normalized and sorted; empty on repositories stored before topics. | 33 | // Normalized and sorted; empty on repositories stored before topics. | ||
| 34 | repeated string topics = 7; | 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 | // SchemaVersion is stored under the key "meta/schema". | 39 | // SchemaVersion is stored under the key "meta/schema". | ||