refactor(web): one helper shapes the log lines of the page, raw feed and live feed
appendLogLine turns the runner's markers into structure and joins the pieces of a line it cut, for the job page and the live feed (the SSE feed now sends merged lines; a piece that opens a batch is marked and joined by the page). The dead placeholders of actions.go are gone. The live log script moves out of ermite.js into log.js with a testable follow(), covered by jstest/log.test.mjs (append, join, scroll at the end only, close on ci-end).
- commit
- cd99c65322d694fa509569a3eaab1de8f1d5a9d4
- author
- brice · · 2026-10-12 19:20 UTC
- parent
- 7513520
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
43 files · +502 -262
M internal/adapters/web/ASSETS.md +1 -0
| @@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime. | |||
| 9 | 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | |
| 10 | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | |
| 11 | 11 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | |
| 12 | + | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | |
| 12 | 13 | | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | |
| 13 | 14 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | |
| 14 | 15 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | |
| @@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime. | |||||
| 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | 9 | | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` | | ||
| 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | 10 | | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` | | ||
| 11 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | 11 | | `static/js/ermite.js` | key sequences to `k-*` events, the functions the keys and inputs call (`window.ermite`), the live fragments (`ermiteLive`), the diff table of contents observer | `mockups/d/js/ermite.js`, extended here | | | ||
| 12 | + | | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | | | |||
| 12 | | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | 13 | | `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | | | ||
| 13 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | 14 | | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | | | ||
| 14 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | 15 | | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | | | ||
M internal/adapters/web/actions.go +41 -54
| @@ -198,6 +198,9 @@ type LogLineView struct { | |||
| 198 | 198 | // Group marks "##[group]" and "##[endgroup]" lines and Error the | |
| 199 | 199 | // "##[error]" ones; the text is shown without the marker. | |
| 200 | 200 | Group, End, Error bool | |
| 201 | + | // Continued marks a piece of a line whose beginning is in an earlier batch | |
| 202 | + | // of the live feed: the page appends its text to the line before it. | |
| 203 | + | Continued bool | |
| 201 | 204 | } | |
| 202 | 205 | ||
| 203 | 206 | // JobPage is one job with its steps and its log. | |
| @@ -452,6 +455,30 @@ func (s *Server) job(ctx context.Context, r *http.Request) (templ.Component, int | |||
| 452 | 455 | return Job(l, p), http.StatusOK, nil | |
| 453 | 456 | } | |
| 454 | 457 | ||
| 458 | + | // appendLogLine adds the line n of a job log to the lines shown. The markers | |
| 459 | + | // of the runner ("##[group]", "##[endgroup]", "##[error]") become structure and | |
| 460 | + | // are not shown as text. A piece that continues a line too long to send whole | |
| 461 | + | // is joined to the line before it; one that opens the list (its line is in an | |
| 462 | + | // earlier batch) keeps Continued, and the page appends it to that line. | |
| 463 | + | func appendLogLine(out []LogLineView, n int64, l ports.JobLogLine) []LogLineView { | |
| 464 | + | text := l.Text | |
| 465 | + | v := LogLineView{N: n, Step: l.Step, Time: l.Time.UTC().Format("15:04:05"), Continued: l.Continued} | |
| 466 | + | switch { | |
| 467 | + | case strings.HasPrefix(text, "##[group]"): | |
| 468 | + | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |
| 469 | + | case strings.HasPrefix(text, "##[endgroup]"): | |
| 470 | + | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |
| 471 | + | case strings.HasPrefix(text, "##[error]"): | |
| 472 | + | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |
| 473 | + | } | |
| 474 | + | v.Text = visibleText(text) | |
| 475 | + | if l.Continued && len(out) > 0 { | |
| 476 | + | out[len(out)-1].Text += v.Text | |
| 477 | + | return out | |
| 478 | + | } | |
| 479 | + | return append(out, v) | |
| 480 | + | } | |
| 481 | + | ||
| 455 | 482 | // logClient is the key of the visitor for the limit of log streams per client. | |
| 456 | 483 | func (s *Server) logClient(r *http.Request) string { | |
| 457 | 484 | return clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies)) | |
| @@ -465,40 +492,14 @@ func (s *Server) readLog(ctx context.Context, client string, repo domain.RepoNam | |||
| 465 | 492 | } | |
| 466 | 493 | var out []LogLineView | |
| 467 | 494 | next := from | |
| 468 | - | ranges := j.StepRanges() | |
| 469 | - | stepOf := func(n int64) int32 { | |
| 470 | - | for i, r := range ranges { | |
| 471 | - | if n >= r.Start && n < r.End { | |
| 472 | - | return j.Steps[i].Number | |
| 473 | - | } | |
| 474 | - | } | |
| 475 | - | return 0 | |
| 476 | - | } | |
| 477 | 495 | stop := errors.New("enough") | |
| 478 | 496 | err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from, Client: client}, func(b ports.LogBatch) error { | |
| 479 | 497 | for i, l := range b.Lines { | |
| 480 | - | if len(out) >= max { | |
| 481 | - | return stop | |
| 482 | - | } | |
| 483 | - | n := b.Offset + int64(i) | |
| 484 | - | text := l.Text | |
| 485 | - | v := LogLineView{N: n, Step: stepOf(n), Time: l.Time.UTC().Format("15:04:05")} | |
| 486 | - | switch { | |
| 487 | - | case strings.HasPrefix(text, "##[group]"): | |
| 488 | - | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |
| 489 | - | case strings.HasPrefix(text, "##[endgroup]"): | |
| 490 | - | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |
| 491 | - | case strings.HasPrefix(text, "##[error]"): | |
| 492 | - | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |
| 498 | + | if len(out) >= max && !l.Continued { | |
| 499 | + | return stop // the rest of a cut line still belongs to this page | |
| 493 | 500 | } | |
| 494 | - | if l.Continued && len(out) > 0 { | |
| 495 | - | out[len(out)-1].Text += visibleText(text) | |
| 496 | - | next = n + 1 | |
| 497 | - | continue | |
| 498 | - | } | |
| 499 | - | v.Text = visibleText(text) | |
| 500 | - | out = append(out, v) | |
| 501 | - | next = n + 1 | |
| 501 | + | out = appendLogLine(out, b.Offset+int64(i), l) | |
| 502 | + | next = b.Offset + int64(i) + 1 | |
| 502 | 503 | } | |
| 503 | 504 | return nil | |
| 504 | 505 | }) | |
| @@ -533,8 +534,9 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) { | |||
| 533 | 534 | } | |
| 534 | 535 | attempt = int32(n) | |
| 535 | 536 | } | |
| 536 | - | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt) | |
| 537 | - | if err != nil { | |
| 537 | + | // The job is read first: a job that is not there, or not readable, is a 404 | |
| 538 | + | // before any header of the log is sent. | |
| 539 | + | if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt); err != nil { | |
| 538 | 540 | s.fail(ctx, w, r, err) | |
| 539 | 541 | return | |
| 540 | 542 | } | |
| @@ -573,7 +575,6 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) { | |||
| 573 | 575 | _, _ = fmt.Fprintln(w) | |
| 574 | 576 | } | |
| 575 | 577 | if err != nil && !open && errors.Is(err, domain.ErrNotFound) { | |
| 576 | - | _ = view | |
| 577 | 578 | w.WriteHeader(http.StatusNotFound) | |
| 578 | 579 | } | |
| 579 | 580 | } | |
| @@ -714,7 +715,7 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C | |||
| 714 | 715 | ||
| 715 | 716 | // checksSection is the checks of the rail of a merge request, with the rule of | |
| 716 | 717 | // the repository that applies to them. | |
| 717 | - | func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) ChecksSection { | |
| 718 | + | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |
| 718 | 719 | sec := ChecksSection{} | |
| 719 | 720 | for _, c := range checks { | |
| 720 | 721 | row := CheckRow{ | |
| @@ -736,7 +737,6 @@ func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) Ch | |||
| 736 | 737 | default: | |
| 737 | 738 | sec.Note = "checks do not block the merge in this repository." | |
| 738 | 739 | } | |
| 739 | - | _ = now | |
| 740 | 740 | return sec | |
| 741 | 741 | } | |
| 742 | 742 | ||
| @@ -770,8 +770,7 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) { | |||
| 770 | 770 | s.denyEvents(ctx, w, errNotFound) | |
| 771 | 771 | return | |
| 772 | 772 | } | |
| 773 | - | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0) | |
| 774 | - | if err != nil { | |
| 773 | + | if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0); err != nil { | |
| 775 | 774 | s.denyEvents(ctx, w, err) | |
| 776 | 775 | return | |
| 777 | 776 | } | |
| @@ -786,29 +785,17 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) { | |||
| 786 | 785 | if out.raw(": connected\n\n") != nil { | |
| 787 | 786 | return | |
| 788 | 787 | } | |
| 789 | - | ranges := view.StepRanges() | |
| 790 | - | _ = ranges | |
| 791 | 788 | n := from | |
| 792 | 789 | err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{ | |
| 793 | 790 | JobID: jobID, Offset: from, Follow: true, | |
| 794 | 791 | Client: s.logClient(r), | |
| 795 | 792 | }, func(b ports.LogBatch) error { | |
| 796 | - | var html strings.Builder | |
| 793 | + | var views []LogLineView | |
| 797 | 794 | for i, l := range b.Lines { | |
| 798 | - | v := LogLineView{N: b.Offset + int64(i), Time: l.Time.UTC().Format("15:04:05"), Step: l.Step} | |
| 799 | - | text := l.Text | |
| 800 | - | switch { | |
| 801 | - | case strings.HasPrefix(text, "##[group]"): | |
| 802 | - | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |
| 803 | - | case strings.HasPrefix(text, "##[endgroup]"): | |
| 804 | - | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |
| 805 | - | case strings.HasPrefix(text, "##[error]"): | |
| 806 | - | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |
| 807 | - | } | |
| 808 | - | v.Text = visibleText(text) | |
| 809 | - | if l.Continued { | |
| 810 | - | continue // joined to its line by the page reload; rare and harmless | |
| 811 | - | } | |
| 795 | + | views = appendLogLine(views, b.Offset+int64(i), l) | |
| 796 | + | } | |
| 797 | + | var html strings.Builder | |
| 798 | + | for _, v := range views { | |
| 812 | 799 | var b2 strings.Builder | |
| 813 | 800 | if err := LogLine(v).Render(ctx, &b2); err != nil { | |
| 814 | 801 | return err | |
| @@ -198,6 +198,9 @@ type LogLineView struct { | |||||
| 198 | // Group marks "##[group]" and "##[endgroup]" lines and Error the | 198 | // Group marks "##[group]" and "##[endgroup]" lines and Error the | ||
| 199 | // "##[error]" ones; the text is shown without the marker. | 199 | // "##[error]" ones; the text is shown without the marker. | ||
| 200 | Group, End, Error bool | 200 | Group, End, Error bool | ||
| 201 | + | // Continued marks a piece of a line whose beginning is in an earlier batch | |||
| 202 | + | // of the live feed: the page appends its text to the line before it. | |||
| 203 | + | Continued bool | |||
| 201 | } | 204 | } | ||
| 202 | 205 | ||||
| 203 | // JobPage is one job with its steps and its log. | 206 | // JobPage is one job with its steps and its log. | ||
| @@ -452,6 +455,30 @@ func (s *Server) job(ctx context.Context, r *http.Request) (templ.Component, int | |||||
| 452 | return Job(l, p), http.StatusOK, nil | 455 | return Job(l, p), http.StatusOK, nil | ||
| 453 | } | 456 | } | ||
| 454 | 457 | ||||
| 458 | + | // appendLogLine adds the line n of a job log to the lines shown. The markers | |||
| 459 | + | // of the runner ("##[group]", "##[endgroup]", "##[error]") become structure and | |||
| 460 | + | // are not shown as text. A piece that continues a line too long to send whole | |||
| 461 | + | // is joined to the line before it; one that opens the list (its line is in an | |||
| 462 | + | // earlier batch) keeps Continued, and the page appends it to that line. | |||
| 463 | + | func appendLogLine(out []LogLineView, n int64, l ports.JobLogLine) []LogLineView { | |||
| 464 | + | text := l.Text | |||
| 465 | + | v := LogLineView{N: n, Step: l.Step, Time: l.Time.UTC().Format("15:04:05"), Continued: l.Continued} | |||
| 466 | + | switch { | |||
| 467 | + | case strings.HasPrefix(text, "##[group]"): | |||
| 468 | + | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |||
| 469 | + | case strings.HasPrefix(text, "##[endgroup]"): | |||
| 470 | + | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |||
| 471 | + | case strings.HasPrefix(text, "##[error]"): | |||
| 472 | + | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |||
| 473 | + | } | |||
| 474 | + | v.Text = visibleText(text) | |||
| 475 | + | if l.Continued && len(out) > 0 { | |||
| 476 | + | out[len(out)-1].Text += v.Text | |||
| 477 | + | return out | |||
| 478 | + | } | |||
| 479 | + | return append(out, v) | |||
| 480 | + | } | |||
| 481 | + | ||||
| 455 | // logClient is the key of the visitor for the limit of log streams per client. | 482 | // logClient is the key of the visitor for the limit of log streams per client. | ||
| 456 | func (s *Server) logClient(r *http.Request) string { | 483 | func (s *Server) logClient(r *http.Request) string { | ||
| 457 | return clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies)) | 484 | return clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies)) | ||
| @@ -465,40 +492,14 @@ func (s *Server) readLog(ctx context.Context, client string, repo domain.RepoNam | |||||
| 465 | } | 492 | } | ||
| 466 | var out []LogLineView | 493 | var out []LogLineView | ||
| 467 | next := from | 494 | next := from | ||
| 468 | - | ranges := j.StepRanges() | |||
| 469 | - | stepOf := func(n int64) int32 { | |||
| 470 | - | for i, r := range ranges { | |||
| 471 | - | if n >= r.Start && n < r.End { | |||
| 472 | - | return j.Steps[i].Number | |||
| 473 | - | } | |||
| 474 | - | } | |||
| 475 | - | return 0 | |||
| 476 | - | } | |||
| 477 | stop := errors.New("enough") | 495 | stop := errors.New("enough") | ||
| 478 | err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from, Client: client}, func(b ports.LogBatch) error { | 496 | err := s.cfg.Runs.GetJobLog(ctx, principal(ctx), repo, ports.GetJobLogParams{JobID: j.ID, Attempt: j.Attempt, Offset: from, Client: client}, func(b ports.LogBatch) error { | ||
| 479 | for i, l := range b.Lines { | 497 | for i, l := range b.Lines { | ||
| 480 | - | if len(out) >= max { | 498 | + | if len(out) >= max && !l.Continued { |
| 481 | - | return stop | 499 | + | return stop // the rest of a cut line still belongs to this page |
| 482 | - | } | |||
| 483 | - | n := b.Offset + int64(i) | |||
| 484 | - | text := l.Text | |||
| 485 | - | v := LogLineView{N: n, Step: stepOf(n), Time: l.Time.UTC().Format("15:04:05")} | |||
| 486 | - | switch { | |||
| 487 | - | case strings.HasPrefix(text, "##[group]"): | |||
| 488 | - | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |||
| 489 | - | case strings.HasPrefix(text, "##[endgroup]"): | |||
| 490 | - | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |||
| 491 | - | case strings.HasPrefix(text, "##[error]"): | |||
| 492 | - | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |||
| 493 | } | 500 | } | ||
| 494 | - | if l.Continued && len(out) > 0 { | 501 | + | out = appendLogLine(out, b.Offset+int64(i), l) |
| 495 | - | out[len(out)-1].Text += visibleText(text) | 502 | + | next = b.Offset + int64(i) + 1 |
| 496 | - | next = n + 1 | |||
| 497 | - | continue | |||
| 498 | - | } | |||
| 499 | - | v.Text = visibleText(text) | |||
| 500 | - | out = append(out, v) | |||
| 501 | - | next = n + 1 | |||
| 502 | } | 503 | } | ||
| 503 | return nil | 504 | return nil | ||
| 504 | }) | 505 | }) | ||
| @@ -533,8 +534,9 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) { | |||||
| 533 | } | 534 | } | ||
| 534 | attempt = int32(n) | 535 | attempt = int32(n) | ||
| 535 | } | 536 | } | ||
| 536 | - | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt) | 537 | + | // The job is read first: a job that is not there, or not readable, is a 404 |
| 537 | - | if err != nil { | 538 | + | // before any header of the log is sent. |
| 539 | + | if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, attempt); err != nil { | |||
| 538 | s.fail(ctx, w, r, err) | 540 | s.fail(ctx, w, r, err) | ||
| 539 | return | 541 | return | ||
| 540 | } | 542 | } | ||
| @@ -573,7 +575,6 @@ func (s *Server) rawLog(w http.ResponseWriter, r *http.Request) { | |||||
| 573 | _, _ = fmt.Fprintln(w) | 575 | _, _ = fmt.Fprintln(w) | ||
| 574 | } | 576 | } | ||
| 575 | if err != nil && !open && errors.Is(err, domain.ErrNotFound) { | 577 | if err != nil && !open && errors.Is(err, domain.ErrNotFound) { | ||
| 576 | - | _ = view | |||
| 577 | w.WriteHeader(http.StatusNotFound) | 578 | w.WriteHeader(http.StatusNotFound) | ||
| 578 | } | 579 | } | ||
| 579 | } | 580 | } | ||
| @@ -714,7 +715,7 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C | |||||
| 714 | 715 | ||||
| 715 | // checksSection is the checks of the rail of a merge request, with the rule of | 716 | // checksSection is the checks of the rail of a merge request, with the rule of | ||
| 716 | // the repository that applies to them. | 717 | // the repository that applies to them. | ||
| 717 | - | func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) ChecksSection { | 718 | + | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { |
| 718 | sec := ChecksSection{} | 719 | sec := ChecksSection{} | ||
| 719 | for _, c := range checks { | 720 | for _, c := range checks { | ||
| 720 | row := CheckRow{ | 721 | row := CheckRow{ | ||
| @@ -736,7 +737,6 @@ func checksSection(repo domain.Repo, checks []ports.CheckView, now time.Time) Ch | |||||
| 736 | default: | 737 | default: | ||
| 737 | sec.Note = "checks do not block the merge in this repository." | 738 | sec.Note = "checks do not block the merge in this repository." | ||
| 738 | } | 739 | } | ||
| 739 | - | _ = now | |||
| 740 | return sec | 740 | return sec | ||
| 741 | } | 741 | } | ||
| 742 | 742 | ||||
| @@ -770,8 +770,7 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) { | |||||
| 770 | s.denyEvents(ctx, w, errNotFound) | 770 | s.denyEvents(ctx, w, errNotFound) | ||
| 771 | return | 771 | return | ||
| 772 | } | 772 | } | ||
| 773 | - | view, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0) | 773 | + | if _, err := s.cfg.Runs.GetJob(ctx, principal(ctx), d.repo.Name, jobID, 0); err != nil { |
| 774 | - | if err != nil { | |||
| 775 | s.denyEvents(ctx, w, err) | 774 | s.denyEvents(ctx, w, err) | ||
| 776 | return | 775 | return | ||
| 777 | } | 776 | } | ||
| @@ -786,29 +785,17 @@ func (s *Server) logStream(w http.ResponseWriter, r *http.Request) { | |||||
| 786 | if out.raw(": connected\n\n") != nil { | 785 | if out.raw(": connected\n\n") != nil { | ||
| 787 | return | 786 | return | ||
| 788 | } | 787 | } | ||
| 789 | - | ranges := view.StepRanges() | |||
| 790 | - | _ = ranges | |||
| 791 | n := from | 788 | n := from | ||
| 792 | err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{ | 789 | err = s.cfg.Runs.GetJobLog(ctx, principal(ctx), d.repo.Name, ports.GetJobLogParams{ | ||
| 793 | JobID: jobID, Offset: from, Follow: true, | 790 | JobID: jobID, Offset: from, Follow: true, | ||
| 794 | Client: s.logClient(r), | 791 | Client: s.logClient(r), | ||
| 795 | }, func(b ports.LogBatch) error { | 792 | }, func(b ports.LogBatch) error { | ||
| 796 | - | var html strings.Builder | 793 | + | var views []LogLineView |
| 797 | for i, l := range b.Lines { | 794 | for i, l := range b.Lines { | ||
| 798 | - | v := LogLineView{N: b.Offset + int64(i), Time: l.Time.UTC().Format("15:04:05"), Step: l.Step} | 795 | + | views = appendLogLine(views, b.Offset+int64(i), l) |
| 799 | - | text := l.Text | 796 | + | } |
| 800 | - | switch { | 797 | + | var html strings.Builder |
| 801 | - | case strings.HasPrefix(text, "##[group]"): | 798 | + | for _, v := range views { |
| 802 | - | v.Group, text = true, strings.TrimPrefix(text, "##[group]") | |||
| 803 | - | case strings.HasPrefix(text, "##[endgroup]"): | |||
| 804 | - | v.End, text = true, strings.TrimPrefix(text, "##[endgroup]") | |||
| 805 | - | case strings.HasPrefix(text, "##[error]"): | |||
| 806 | - | v.Error, text = true, strings.TrimPrefix(text, "##[error]") | |||
| 807 | - | } | |||
| 808 | - | v.Text = visibleText(text) | |||
| 809 | - | if l.Continued { | |||
| 810 | - | continue // joined to its line by the page reload; rare and harmless | |||
| 811 | - | } | |||
| 812 | var b2 strings.Builder | 799 | var b2 strings.Builder | ||
| 813 | if err := LogLine(v).Render(ctx, &b2); err != nil { | 800 | if err := LogLine(v).Render(ctx, &b2); err != nil { | ||
| 814 | return err | 801 | return err | ||
M internal/adapters/web/actions.templ +1 -1
| @@ -208,7 +208,7 @@ templ Job(l Layout, p JobPage) { | |||
| 208 | 208 | // LogLine is a line of the log. Group and error markers of the runner show as | |
| 209 | 209 | // headings and errors, never as raw text. | |
| 210 | 210 | templ LogLine(ln LogLineView) { | |
| 211 | - | <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li> | |
| 211 | + | <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) } data-cont?={ ln.Continued }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li> | |
| 212 | 212 | } | |
| 213 | 213 | ||
| 214 | 214 | templ jobRail(p JobPage) { | |
| @@ -208,7 +208,7 @@ templ Job(l Layout, p JobPage) { | |||||
| 208 | // LogLine is a line of the log. Group and error markers of the runner show as | 208 | // LogLine is a line of the log. Group and error markers of the runner show as | ||
| 209 | // headings and errors, never as raw text. | 209 | // headings and errors, never as raw text. | ||
| 210 | templ LogLine(ln LogLineView) { | 210 | templ LogLine(ln LogLineView) { | ||
| 211 | - | <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li> | 211 | + | <li id={ "L" + itoa64(ln.N) } class={ logClass(ln) } data-step={ itoa(int(ln.Step)) } data-cont?={ ln.Continued }><time>{ ln.Time }</time><span class="t">{ ln.Text }</span></li> |
| 212 | } | 212 | } | ||
| 213 | 213 | ||||
| 214 | templ jobRail(p JobPage) { | 214 | templ jobRail(p JobPage) { | ||
M internal/adapters/web/actions_templ.go +49 -39 generated file: collapsed
M internal/adapters/web/actions_test.go +42 -0
| @@ -68,3 +68,45 @@ func TestLogPagesPassTheClientKey(t *testing.T) { | |||
| 68 | 68 | assert.Equal(t, want, c) | |
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | + | ||
| 72 | + | // M4: one helper shapes the lines of the page, the raw feed and the live feed. | |
| 73 | + | func TestAppendLogLine(t *testing.T) { | |
| 74 | + | t.Parallel() | |
| 75 | + | at := time.Date(2026, 10, 12, 10, 11, 12, 0, time.UTC) | |
| 76 | + | line := func(text string, cont bool) ports.JobLogLine { | |
| 77 | + | return ports.JobLogLine{Step: 2, CILogLine: domain.CILogLine{Time: at, Text: text, Continued: cont}} | |
| 78 | + | } | |
| 79 | + | var out []LogLineView | |
| 80 | + | out = appendLogLine(out, 10, line("##[group]Run make", false)) | |
| 81 | + | out = appendLogLine(out, 11, line("plain <b>", false)) | |
| 82 | + | out = appendLogLine(out, 12, line("##[error]Process completed with exit code 1.", false)) | |
| 83 | + | out = appendLogLine(out, 13, line("##[endgroup]", false)) | |
| 84 | + | require.Len(t, out, 4) | |
| 85 | + | assert.True(t, out[0].Group) | |
| 86 | + | assert.Equal(t, "Run make", out[0].Text, "the marker is structure, not text") | |
| 87 | + | assert.EqualValues(t, 10, out[0].N) | |
| 88 | + | assert.Equal(t, "10:11:12", out[0].Time) | |
| 89 | + | assert.EqualValues(t, 2, out[0].Step) | |
| 90 | + | assert.False(t, out[1].Group || out[1].Error || out[1].End) | |
| 91 | + | assert.True(t, out[2].Error) | |
| 92 | + | assert.Equal(t, "Process completed with exit code 1.", out[2].Text) | |
| 93 | + | assert.True(t, out[3].End) | |
| 94 | + | ||
| 95 | + | // A cut line comes back as one line, with the number of its first piece. | |
| 96 | + | var cut []LogLineView | |
| 97 | + | cut = appendLogLine(cut, 20, line("first half, ", false)) | |
| 98 | + | cut = appendLogLine(cut, 21, line("second half", true)) | |
| 99 | + | require.Len(t, cut, 1) | |
| 100 | + | assert.Equal(t, "first half, second half", cut[0].Text) | |
| 101 | + | assert.EqualValues(t, 20, cut[0].N) | |
| 102 | + | ||
| 103 | + | // A piece that opens a batch has no line before it here: it stays, marked, | |
| 104 | + | // for the page to join to the line it already holds. | |
| 105 | + | open := appendLogLine(nil, 30, line("rest", true)) | |
| 106 | + | require.Len(t, open, 1) | |
| 107 | + | assert.True(t, open[0].Continued) | |
| 108 | + | ||
| 109 | + | // Invisible characters of the runner's text are shown, never applied. | |
| 110 | + | hidden := appendLogLine(nil, 40, line("a\u202eb", false)) | |
| 111 | + | assert.NotContains(t, hidden[0].Text, "\u202e") | |
| 112 | + | } | |
| @@ -68,3 +68,45 @@ func TestLogPagesPassTheClientKey(t *testing.T) { | |||||
| 68 | assert.Equal(t, want, c) | 68 | assert.Equal(t, want, c) | ||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | + | ||||
| 72 | + | // M4: one helper shapes the lines of the page, the raw feed and the live feed. | |||
| 73 | + | func TestAppendLogLine(t *testing.T) { | |||
| 74 | + | t.Parallel() | |||
| 75 | + | at := time.Date(2026, 10, 12, 10, 11, 12, 0, time.UTC) | |||
| 76 | + | line := func(text string, cont bool) ports.JobLogLine { | |||
| 77 | + | return ports.JobLogLine{Step: 2, CILogLine: domain.CILogLine{Time: at, Text: text, Continued: cont}} | |||
| 78 | + | } | |||
| 79 | + | var out []LogLineView | |||
| 80 | + | out = appendLogLine(out, 10, line("##[group]Run make", false)) | |||
| 81 | + | out = appendLogLine(out, 11, line("plain <b>", false)) | |||
| 82 | + | out = appendLogLine(out, 12, line("##[error]Process completed with exit code 1.", false)) | |||
| 83 | + | out = appendLogLine(out, 13, line("##[endgroup]", false)) | |||
| 84 | + | require.Len(t, out, 4) | |||
| 85 | + | assert.True(t, out[0].Group) | |||
| 86 | + | assert.Equal(t, "Run make", out[0].Text, "the marker is structure, not text") | |||
| 87 | + | assert.EqualValues(t, 10, out[0].N) | |||
| 88 | + | assert.Equal(t, "10:11:12", out[0].Time) | |||
| 89 | + | assert.EqualValues(t, 2, out[0].Step) | |||
| 90 | + | assert.False(t, out[1].Group || out[1].Error || out[1].End) | |||
| 91 | + | assert.True(t, out[2].Error) | |||
| 92 | + | assert.Equal(t, "Process completed with exit code 1.", out[2].Text) | |||
| 93 | + | assert.True(t, out[3].End) | |||
| 94 | + | ||||
| 95 | + | // A cut line comes back as one line, with the number of its first piece. | |||
| 96 | + | var cut []LogLineView | |||
| 97 | + | cut = appendLogLine(cut, 20, line("first half, ", false)) | |||
| 98 | + | cut = appendLogLine(cut, 21, line("second half", true)) | |||
| 99 | + | require.Len(t, cut, 1) | |||
| 100 | + | assert.Equal(t, "first half, second half", cut[0].Text) | |||
| 101 | + | assert.EqualValues(t, 20, cut[0].N) | |||
| 102 | + | ||||
| 103 | + | // A piece that opens a batch has no line before it here: it stays, marked, | |||
| 104 | + | // for the page to join to the line it already holds. | |||
| 105 | + | open := appendLogLine(nil, 30, line("rest", true)) | |||
| 106 | + | require.Len(t, open, 1) | |||
| 107 | + | assert.True(t, open[0].Continued) | |||
| 108 | + | ||||
| 109 | + | // Invisible characters of the runner's text are shown, never applied. | |||
| 110 | + | hidden := appendLogLine(nil, 40, line("a\u202eb", false)) | |||
| 111 | + | assert.NotContains(t, hidden[0].Text, "\u202e") | |||
| 112 | + | } | |||
M internal/adapters/web/assets.go +2 -1
| @@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/" | |||
| 23 | 23 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | |
| 24 | 24 | // the content, so they can be cached for ever. | |
| 25 | 25 | type Assets struct { | |
| 26 | - | CSS, HTMX, Live, SSE, App, Files, Icon string | |
| 26 | + | CSS, HTMX, Live, SSE, App, Files, Log, Icon string | |
| 27 | 27 | // Font is the UI font, preloaded by every page. | |
| 28 | 28 | Font string | |
| 29 | 29 | // Rules is the speculation rules file named by the Speculation-Rules header. | |
| @@ -56,6 +56,7 @@ var scripts = []struct { | |||
| 56 | 56 | {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }}, | |
| 57 | 57 | {"ermite.js", func(a *Assets, u string) { a.App = u }}, | |
| 58 | 58 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | |
| 59 | + | {"log.js", func(a *Assets, u string) { a.Log = u }}, | |
| 59 | 60 | } | |
| 60 | 61 | ||
| 61 | 62 | // sheetAssets are the files the stylesheet references as url(<dir>/<file>): | |
| @@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/" | |||||
| 23 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | 23 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | ||
| 24 | // the content, so they can be cached for ever. | 24 | // the content, so they can be cached for ever. | ||
| 25 | type Assets struct { | 25 | type Assets struct { | ||
| 26 | - | CSS, HTMX, Live, SSE, App, Files, Icon string | 26 | + | CSS, HTMX, Live, SSE, App, Files, Log, Icon string |
| 27 | // Font is the UI font, preloaded by every page. | 27 | // Font is the UI font, preloaded by every page. | ||
| 28 | Font string | 28 | Font string | ||
| 29 | // Rules is the speculation rules file named by the Speculation-Rules header. | 29 | // Rules is the speculation rules file named by the Speculation-Rules header. | ||
| @@ -56,6 +56,7 @@ var scripts = []struct { | |||||
| 56 | {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }}, | 56 | {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }}, | ||
| 57 | {"ermite.js", func(a *Assets, u string) { a.App = u }}, | 57 | {"ermite.js", func(a *Assets, u string) { a.App = u }}, | ||
| 58 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | 58 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | ||
| 59 | + | {"log.js", func(a *Assets, u string) { a.Log = u }}, | |||
| 59 | } | 60 | } | ||
| 60 | 61 | ||||
| 61 | // sheetAssets are the files the stylesheet references as url(<dir>/<file>): | 62 | // sheetAssets are the files the stylesheet references as url(<dir>/<file>): | ||
A internal/adapters/web/jstest/log.test.mjs +103 -0
| @@ -0,0 +1,103 @@ | |||
| 1 | + | // Tests of the live job log of static/js/log.js (window.ermiteLog) with a stub | |
| 2 | + | // EventSource on the tiny hand-rolled DOM. Run with | |
| 3 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |
| 4 | + | import assert from 'node:assert/strict'; | |
| 5 | + | import { test } from 'node:test'; | |
| 6 | + | import { el, installDom } from './dom.mjs'; | |
| 7 | + | ||
| 8 | + | // A line is "text" or "+text" for a piece that continues the line before it. | |
| 9 | + | function line(text) { | |
| 10 | + | const cont = text.startsWith('+'); | |
| 11 | + | const li = el('li', { cls: 'lg', attrs: cont ? { 'data-cont': '' } : {} }, el('span', { cls: 't', text: cont ? text.slice(1) : text })); | |
| 12 | + | return li; | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | installDom({ parseHTML: (data) => data.split('|').filter(Boolean).map(line) }); | |
| 16 | + | globalThis.document.addEventListener = () => {}; // log.js starts itself on DOMContentLoaded | |
| 17 | + | ||
| 18 | + | class StubEventSource { | |
| 19 | + | static CLOSED = 2; | |
| 20 | + | static last = null; | |
| 21 | + | constructor(url) { | |
| 22 | + | this.url = url; | |
| 23 | + | this.readyState = 1; | |
| 24 | + | this.handlers = new Map(); | |
| 25 | + | StubEventSource.last = this; | |
| 26 | + | } | |
| 27 | + | addEventListener(name, fn) { this.handlers.set(name, fn); } | |
| 28 | + | close() { this.readyState = StubEventSource.CLOSED; } | |
| 29 | + | emit(name, data) { this.handlers.get(name)?.({ data }); } | |
| 30 | + | } | |
| 31 | + | globalThis.EventSource = StubEventSource; | |
| 32 | + | ||
| 33 | + | let scrolls = []; | |
| 34 | + | globalThis.scrollTo = (x, y) => scrolls.push(y); | |
| 35 | + | Object.assign(globalThis, { innerHeight: 800, scrollY: 0 }); | |
| 36 | + | ||
| 37 | + | await import('../static/js/log.js'); | |
| 38 | + | const { follow } = window.ermiteLog; | |
| 39 | + | ||
| 40 | + | // page makes a job page body: the box with its list and the lines already there. | |
| 41 | + | function page(attrs, ...lines) { | |
| 42 | + | const list = el('ol', { cls: 'log' }, ...lines.map(line)); | |
| 43 | + | const box = el('div', { id: 'job-log', attrs }, list); | |
| 44 | + | const body = document.body; | |
| 45 | + | body.replaceChildren(box); | |
| 46 | + | Object.defineProperty(globalThis.document.body, 'scrollHeight', { value: 1000, configurable: true }); | |
| 47 | + | return { box, list }; | |
| 48 | + | } | |
| 49 | + | const running = { 'data-follow': '', 'data-stream': '/r/job/stream', 'data-next': '3' }; | |
| 50 | + | const texts = (list) => list.children.map((li) => li.querySelector('.t').textContent); | |
| 51 | + | ||
| 52 | + | test('it does nothing for a job that is over, or a page without a feed', () => { | |
| 53 | + | assert.equal(follow(null), null); | |
| 54 | + | assert.equal(follow(page({ 'data-stream': '/s' }).box), null, 'no data-follow: the job ended'); | |
| 55 | + | assert.equal(follow(page({ 'data-follow': '' }).box), null, 'no stream URL'); | |
| 56 | + | }); | |
| 57 | + | ||
| 58 | + | test('it opens the feed after the last line the page holds', () => { | |
| 59 | + | const { box } = page(running, 'a', 'b', 'c'); | |
| 60 | + | const es = follow(box); | |
| 61 | + | assert.equal(es.url, '/r/job/stream?from=3'); | |
| 62 | + | }); | |
| 63 | + | ||
| 64 | + | test('it appends the lines of a batch', () => { | |
| 65 | + | const { box, list } = page(running, 'a'); | |
| 66 | + | const es = follow(box); | |
| 67 | + | es.emit('ci-log', 'b|c'); | |
| 68 | + | es.emit('ci-log', 'd'); | |
| 69 | + | assert.deepEqual(texts(list), ['a', 'b', 'c', 'd']); | |
| 70 | + | }); | |
| 71 | + | ||
| 72 | + | test('a piece that continues a line of an earlier batch joins that line', () => { | |
| 73 | + | const { box, list } = page(running, 'start of a long'); | |
| 74 | + | const es = follow(box); | |
| 75 | + | es.emit('ci-log', '+ line|next'); | |
| 76 | + | assert.deepEqual(texts(list), ['start of a long line', 'next']); | |
| 77 | + | }); | |
| 78 | + | ||
| 79 | + | test('it scrolls with the log only while the reader is at its end', () => { | |
| 80 | + | const { box } = page(running, 'a'); | |
| 81 | + | const es = follow(box); | |
| 82 | + | scrolls = []; | |
| 83 | + | Object.assign(globalThis, { scrollY: 190 }); // 800 + 190 >= 1000 - 80: at the end | |
| 84 | + | es.emit('ci-log', 'b'); | |
| 85 | + | assert.deepEqual(scrolls, [1000]); | |
| 86 | + | scrolls = []; | |
| 87 | + | Object.assign(globalThis, { scrollY: 0 }); // reading above | |
| 88 | + | es.emit('ci-log', 'c'); | |
| 89 | + | assert.deepEqual(scrolls, []); | |
| 90 | + | }); | |
| 91 | + | ||
| 92 | + | test('ci-end closes the feed, and a closed connection stays closed', () => { | |
| 93 | + | const { box } = page(running, 'a'); | |
| 94 | + | const es = follow(box); | |
| 95 | + | assert.equal(es.readyState, 1); | |
| 96 | + | es.emit('ci-end', 'end'); | |
| 97 | + | assert.equal(es.readyState, StubEventSource.CLOSED); | |
| 98 | + | ||
| 99 | + | const again = follow(page(running, 'a').box); | |
| 100 | + | again.readyState = StubEventSource.CLOSED; | |
| 101 | + | again.onerror(); | |
| 102 | + | assert.equal(again.readyState, StubEventSource.CLOSED); | |
| 103 | + | }); | |
| @@ -0,0 +1,103 @@ | |||||
| 1 | + | // Tests of the live job log of static/js/log.js (window.ermiteLog) with a stub | |||
| 2 | + | // EventSource on the tiny hand-rolled DOM. Run with | |||
| 3 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |||
| 4 | + | import assert from 'node:assert/strict'; | |||
| 5 | + | import { test } from 'node:test'; | |||
| 6 | + | import { el, installDom } from './dom.mjs'; | |||
| 7 | + | ||||
| 8 | + | // A line is "text" or "+text" for a piece that continues the line before it. | |||
| 9 | + | function line(text) { | |||
| 10 | + | const cont = text.startsWith('+'); | |||
| 11 | + | const li = el('li', { cls: 'lg', attrs: cont ? { 'data-cont': '' } : {} }, el('span', { cls: 't', text: cont ? text.slice(1) : text })); | |||
| 12 | + | return li; | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | installDom({ parseHTML: (data) => data.split('|').filter(Boolean).map(line) }); | |||
| 16 | + | globalThis.document.addEventListener = () => {}; // log.js starts itself on DOMContentLoaded | |||
| 17 | + | ||||
| 18 | + | class StubEventSource { | |||
| 19 | + | static CLOSED = 2; | |||
| 20 | + | static last = null; | |||
| 21 | + | constructor(url) { | |||
| 22 | + | this.url = url; | |||
| 23 | + | this.readyState = 1; | |||
| 24 | + | this.handlers = new Map(); | |||
| 25 | + | StubEventSource.last = this; | |||
| 26 | + | } | |||
| 27 | + | addEventListener(name, fn) { this.handlers.set(name, fn); } | |||
| 28 | + | close() { this.readyState = StubEventSource.CLOSED; } | |||
| 29 | + | emit(name, data) { this.handlers.get(name)?.({ data }); } | |||
| 30 | + | } | |||
| 31 | + | globalThis.EventSource = StubEventSource; | |||
| 32 | + | ||||
| 33 | + | let scrolls = []; | |||
| 34 | + | globalThis.scrollTo = (x, y) => scrolls.push(y); | |||
| 35 | + | Object.assign(globalThis, { innerHeight: 800, scrollY: 0 }); | |||
| 36 | + | ||||
| 37 | + | await import('../static/js/log.js'); | |||
| 38 | + | const { follow } = window.ermiteLog; | |||
| 39 | + | ||||
| 40 | + | // page makes a job page body: the box with its list and the lines already there. | |||
| 41 | + | function page(attrs, ...lines) { | |||
| 42 | + | const list = el('ol', { cls: 'log' }, ...lines.map(line)); | |||
| 43 | + | const box = el('div', { id: 'job-log', attrs }, list); | |||
| 44 | + | const body = document.body; | |||
| 45 | + | body.replaceChildren(box); | |||
| 46 | + | Object.defineProperty(globalThis.document.body, 'scrollHeight', { value: 1000, configurable: true }); | |||
| 47 | + | return { box, list }; | |||
| 48 | + | } | |||
| 49 | + | const running = { 'data-follow': '', 'data-stream': '/r/job/stream', 'data-next': '3' }; | |||
| 50 | + | const texts = (list) => list.children.map((li) => li.querySelector('.t').textContent); | |||
| 51 | + | ||||
| 52 | + | test('it does nothing for a job that is over, or a page without a feed', () => { | |||
| 53 | + | assert.equal(follow(null), null); | |||
| 54 | + | assert.equal(follow(page({ 'data-stream': '/s' }).box), null, 'no data-follow: the job ended'); | |||
| 55 | + | assert.equal(follow(page({ 'data-follow': '' }).box), null, 'no stream URL'); | |||
| 56 | + | }); | |||
| 57 | + | ||||
| 58 | + | test('it opens the feed after the last line the page holds', () => { | |||
| 59 | + | const { box } = page(running, 'a', 'b', 'c'); | |||
| 60 | + | const es = follow(box); | |||
| 61 | + | assert.equal(es.url, '/r/job/stream?from=3'); | |||
| 62 | + | }); | |||
| 63 | + | ||||
| 64 | + | test('it appends the lines of a batch', () => { | |||
| 65 | + | const { box, list } = page(running, 'a'); | |||
| 66 | + | const es = follow(box); | |||
| 67 | + | es.emit('ci-log', 'b|c'); | |||
| 68 | + | es.emit('ci-log', 'd'); | |||
| 69 | + | assert.deepEqual(texts(list), ['a', 'b', 'c', 'd']); | |||
| 70 | + | }); | |||
| 71 | + | ||||
| 72 | + | test('a piece that continues a line of an earlier batch joins that line', () => { | |||
| 73 | + | const { box, list } = page(running, 'start of a long'); | |||
| 74 | + | const es = follow(box); | |||
| 75 | + | es.emit('ci-log', '+ line|next'); | |||
| 76 | + | assert.deepEqual(texts(list), ['start of a long line', 'next']); | |||
| 77 | + | }); | |||
| 78 | + | ||||
| 79 | + | test('it scrolls with the log only while the reader is at its end', () => { | |||
| 80 | + | const { box } = page(running, 'a'); | |||
| 81 | + | const es = follow(box); | |||
| 82 | + | scrolls = []; | |||
| 83 | + | Object.assign(globalThis, { scrollY: 190 }); // 800 + 190 >= 1000 - 80: at the end | |||
| 84 | + | es.emit('ci-log', 'b'); | |||
| 85 | + | assert.deepEqual(scrolls, [1000]); | |||
| 86 | + | scrolls = []; | |||
| 87 | + | Object.assign(globalThis, { scrollY: 0 }); // reading above | |||
| 88 | + | es.emit('ci-log', 'c'); | |||
| 89 | + | assert.deepEqual(scrolls, []); | |||
| 90 | + | }); | |||
| 91 | + | ||||
| 92 | + | test('ci-end closes the feed, and a closed connection stays closed', () => { | |||
| 93 | + | const { box } = page(running, 'a'); | |||
| 94 | + | const es = follow(box); | |||
| 95 | + | assert.equal(es.readyState, 1); | |||
| 96 | + | es.emit('ci-end', 'end'); | |||
| 97 | + | assert.equal(es.readyState, StubEventSource.CLOSED); | |||
| 98 | + | ||||
| 99 | + | const again = follow(page(running, 'a').box); | |||
| 100 | + | again.readyState = StubEventSource.CLOSED; | |||
| 101 | + | again.onerror(); | |||
| 102 | + | assert.equal(again.readyState, StubEventSource.CLOSED); | |||
| 103 | + | }); | |||
M internal/adapters/web/mr.go +2 -2
| @@ -459,9 +459,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 459 | 459 | } | |
| 460 | 460 | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | |
| 461 | 461 | if open && head != "" { | |
| 462 | - | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now) | |
| 462 | + | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head)) | |
| 463 | 463 | } else if head != "" { | |
| 464 | - | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now) | |
| 464 | + | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head)) | |
| 465 | 465 | p.Checks.Note = "" | |
| 466 | 466 | } | |
| 467 | 467 | p.Note = diffNote(res.Diff, len(p.Files), p.NFiles) | |
| @@ -459,9 +459,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 459 | } | 459 | } | ||
| 460 | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | 460 | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | ||
| 461 | if open && head != "" { | 461 | if open && head != "" { | ||
| 462 | - | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now) | 462 | + | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head)) |
| 463 | } else if head != "" { | 463 | } else if head != "" { | ||
| 464 | - | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head), now) | 464 | + | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head)) |
| 465 | p.Checks.Note = "" | 465 | p.Checks.Note = "" | ||
| 466 | } | 466 | } | ||
| 467 | p.Note = diffNote(res.Diff, len(p.Files), p.NFiles) | 467 | p.Note = diffNote(res.Diff, len(p.Files), p.NFiles) | ||
M internal/adapters/web/shell.templ +1 -0
| @@ -22,6 +22,7 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 22 | 22 | <script src={ l.Assets.SSE } defer></script> | |
| 23 | 23 | <script src={ l.Assets.App } defer></script> | |
| 24 | 24 | <script src={ l.Assets.Files } defer></script> | |
| 25 | + | <script src={ l.Assets.Log } defer></script> | |
| 25 | 26 | </head> | |
| 26 | 27 | <body | |
| 27 | 28 | data-ref={ l.RefPath } | |
| @@ -22,6 +22,7 @@ templ Shell(l Layout, rail templ.Component) { | |||||
| 22 | <script src={ l.Assets.SSE } defer></script> | 22 | <script src={ l.Assets.SSE } defer></script> | ||
| 23 | <script src={ l.Assets.App } defer></script> | 23 | <script src={ l.Assets.App } defer></script> | ||
| 24 | <script src={ l.Assets.Files } defer></script> | 24 | <script src={ l.Assets.Files } defer></script> | ||
| 25 | + | <script src={ l.Assets.Log } defer></script> | |||
| 25 | </head> | 26 | </head> | ||
| 26 | <body | 27 | <body | ||
| 27 | data-ref={ l.RefPath } | 28 | data-ref={ l.RefPath } | ||
M internal/adapters/web/shell_templ.go +155 -142 generated file: collapsed
M internal/adapters/web/static/js/ermite.js +0 -23
| @@ -363,26 +363,3 @@ window.ermiteLive = (() => { | |||
| 363 | 363 | }, | |
| 364 | 364 | }; | |
| 365 | 365 | })(); | |
| 366 | - | ||
| 367 | - | // ermiteLog follows the log of a running job on its page: the lines arrive as | |
| 368 | - | // "ci-log" events (internal/adapters/web/actions.go, logStream) and are | |
| 369 | - | // appended to the list. The browser reconnects by itself with Last-Event-ID, | |
| 370 | - | // the number of the next line; "ci-end" closes the feed when the job is over. | |
| 371 | - | // The page scrolls with the log only while the reader is at its end. | |
| 372 | - | (() => { | |
| 373 | - | const box = document.getElementById('job-log'); | |
| 374 | - | if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return; | |
| 375 | - | const list = box.querySelector('ol.log'); | |
| 376 | - | if (!list) return; | |
| 377 | - | const url = box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0'); | |
| 378 | - | const es = new EventSource(url); | |
| 379 | - | es.addEventListener('ci-log', (e) => { | |
| 380 | - | const t = document.createElement('template'); | |
| 381 | - | t.innerHTML = e.data; | |
| 382 | - | const atEnd = window.innerHeight + window.scrollY >= document.body.scrollHeight - 80; | |
| 383 | - | list.append(...t.content.children); | |
| 384 | - | if (atEnd) window.scrollTo(0, document.body.scrollHeight); | |
| 385 | - | }); | |
| 386 | - | es.addEventListener('ci-end', () => es.close()); | |
| 387 | - | es.onerror = () => { if (es.readyState === EventSource.CLOSED) es.close(); }; | |
| 388 | - | })(); | |
| @@ -363,26 +363,3 @@ window.ermiteLive = (() => { | |||||
| 363 | }, | 363 | }, | ||
| 364 | }; | 364 | }; | ||
| 365 | })(); | 365 | })(); | ||
| 366 | - | ||||
| 367 | - | // ermiteLog follows the log of a running job on its page: the lines arrive as | |||
| 368 | - | // "ci-log" events (internal/adapters/web/actions.go, logStream) and are | |||
| 369 | - | // appended to the list. The browser reconnects by itself with Last-Event-ID, | |||
| 370 | - | // the number of the next line; "ci-end" closes the feed when the job is over. | |||
| 371 | - | // The page scrolls with the log only while the reader is at its end. | |||
| 372 | - | (() => { | |||
| 373 | - | const box = document.getElementById('job-log'); | |||
| 374 | - | if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return; | |||
| 375 | - | const list = box.querySelector('ol.log'); | |||
| 376 | - | if (!list) return; | |||
| 377 | - | const url = box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0'); | |||
| 378 | - | const es = new EventSource(url); | |||
| 379 | - | es.addEventListener('ci-log', (e) => { | |||
| 380 | - | const t = document.createElement('template'); | |||
| 381 | - | t.innerHTML = e.data; | |||
| 382 | - | const atEnd = window.innerHeight + window.scrollY >= document.body.scrollHeight - 80; | |||
| 383 | - | list.append(...t.content.children); | |||
| 384 | - | if (atEnd) window.scrollTo(0, document.body.scrollHeight); | |||
| 385 | - | }); | |||
| 386 | - | es.addEventListener('ci-end', () => es.close()); | |||
| 387 | - | es.onerror = () => { if (es.readyState === EventSource.CLOSED) es.close(); }; | |||
| 388 | - | })(); | |||
A internal/adapters/web/static/js/log.js +43 -0
| @@ -0,0 +1,43 @@ | |||
| 1 | + | // log.js: the live log of a running job (page /<repo>/actions/runs/<id>/job/<id>). | |
| 2 | + | // The lines arrive as "ci-log" events (internal/adapters/web/actions.go, | |
| 3 | + | // logStream), each one the <li> of the lines of a batch, and are appended to the | |
| 4 | + | // list. The browser reconnects by itself with Last-Event-ID, the number of the | |
| 5 | + | // next line; "ci-end" closes the feed when the job is over. The page follows the | |
| 6 | + | // log (scrolls to its end) only while the reader is at the end already. A piece | |
| 7 | + | // that continues a line begun in an earlier batch (data-cont) is joined to the | |
| 8 | + | // last line of the list. | |
| 9 | + | window.ermiteLog = (() => { | |
| 10 | + | const nearEnd = () => window.innerHeight + window.scrollY >= document.body.scrollHeight - 80; | |
| 11 | + | ||
| 12 | + | // follow starts the feed of the box (#job-log) and returns the EventSource, | |
| 13 | + | // or null when the box is not a running job's. | |
| 14 | + | function follow(box) { | |
| 15 | + | if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return null; | |
| 16 | + | const list = box.querySelector('ol.log'); | |
| 17 | + | if (!list) return null; | |
| 18 | + | const es = new window.EventSource(box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0')); | |
| 19 | + | es.addEventListener('ci-log', (e) => { | |
| 20 | + | const t = document.createElement('template'); | |
| 21 | + | t.innerHTML = e.data; | |
| 22 | + | const atEnd = nearEnd(); | |
| 23 | + | for (const line of [...t.content.children]) { | |
| 24 | + | const last = list.children[list.children.length - 1]; | |
| 25 | + | if (line.hasAttribute('data-cont') && last) { | |
| 26 | + | const text = line.querySelector('.t'); | |
| 27 | + | const into = last.querySelector('.t'); | |
| 28 | + | if (text && into) into.textContent += text.textContent; | |
| 29 | + | } else { | |
| 30 | + | list.append(line); | |
| 31 | + | } | |
| 32 | + | } | |
| 33 | + | if (atEnd) window.scrollTo(0, document.body.scrollHeight); | |
| 34 | + | }); | |
| 35 | + | es.addEventListener('ci-end', () => es.close()); | |
| 36 | + | es.onerror = () => { if (es.readyState === window.EventSource.CLOSED) es.close(); }; | |
| 37 | + | return es; | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | return { follow }; | |
| 41 | + | })(); | |
| 42 | + | ||
| 43 | + | document.addEventListener('DOMContentLoaded', () => window.ermiteLog.follow(document.getElementById('job-log'))); | |
| @@ -0,0 +1,43 @@ | |||||
| 1 | + | // log.js: the live log of a running job (page /<repo>/actions/runs/<id>/job/<id>). | |||
| 2 | + | // The lines arrive as "ci-log" events (internal/adapters/web/actions.go, | |||
| 3 | + | // logStream), each one the <li> of the lines of a batch, and are appended to the | |||
| 4 | + | // list. The browser reconnects by itself with Last-Event-ID, the number of the | |||
| 5 | + | // next line; "ci-end" closes the feed when the job is over. The page follows the | |||
| 6 | + | // log (scrolls to its end) only while the reader is at the end already. A piece | |||
| 7 | + | // that continues a line begun in an earlier batch (data-cont) is joined to the | |||
| 8 | + | // last line of the list. | |||
| 9 | + | window.ermiteLog = (() => { | |||
| 10 | + | const nearEnd = () => window.innerHeight + window.scrollY >= document.body.scrollHeight - 80; | |||
| 11 | + | ||||
| 12 | + | // follow starts the feed of the box (#job-log) and returns the EventSource, | |||
| 13 | + | // or null when the box is not a running job's. | |||
| 14 | + | function follow(box) { | |||
| 15 | + | if (!box || !box.hasAttribute('data-follow') || !box.dataset.stream || !window.EventSource) return null; | |||
| 16 | + | const list = box.querySelector('ol.log'); | |||
| 17 | + | if (!list) return null; | |||
| 18 | + | const es = new window.EventSource(box.dataset.stream + '?from=' + encodeURIComponent(box.dataset.next || '0')); | |||
| 19 | + | es.addEventListener('ci-log', (e) => { | |||
| 20 | + | const t = document.createElement('template'); | |||
| 21 | + | t.innerHTML = e.data; | |||
| 22 | + | const atEnd = nearEnd(); | |||
| 23 | + | for (const line of [...t.content.children]) { | |||
| 24 | + | const last = list.children[list.children.length - 1]; | |||
| 25 | + | if (line.hasAttribute('data-cont') && last) { | |||
| 26 | + | const text = line.querySelector('.t'); | |||
| 27 | + | const into = last.querySelector('.t'); | |||
| 28 | + | if (text && into) into.textContent += text.textContent; | |||
| 29 | + | } else { | |||
| 30 | + | list.append(line); | |||
| 31 | + | } | |||
| 32 | + | } | |||
| 33 | + | if (atEnd) window.scrollTo(0, document.body.scrollHeight); | |||
| 34 | + | }); | |||
| 35 | + | es.addEventListener('ci-end', () => es.close()); | |||
| 36 | + | es.onerror = () => { if (es.readyState === window.EventSource.CLOSED) es.close(); }; | |||
| 37 | + | return es; | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | return { follow }; | |||
| 41 | + | })(); | |||
| 42 | + | ||||
| 43 | + | document.addEventListener('DOMContentLoaded', () => window.ermiteLog.follow(document.getElementById('job-log'))); | |||
M internal/app/testdata/web/blob-binary.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-code.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-empty.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-image.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-markdown.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-svg.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-text.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-too-large.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/blob-trojan.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-big.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-binary.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-merge.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-modify-split.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-modify.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-rename.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-root.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-trojan.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 25 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commits-page2.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commits-path.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commits-root.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/long/paths/SHA40" data-blob="/long/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commits-small.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/empty.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/home.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/not-found-page.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/not-found.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | 25 | <body data-ref="" data-sha="" data-sel="" data-nav=""> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/refs.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/summary.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/demo/paths/SHA40" data-blob="/demo/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/tree-nested.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/tree-root.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/tree-slash-ref.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/feature/x"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/feature/x"> | 25 | <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/feature/x"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/tree-subdir.html +2 -0
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | </script> | |
| 20 | 20 | <script src="/-/static/js/files.HASH.js" defer> | |
| 21 | 21 | </script> | |
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |
| 23 | + | </script> | |
| 22 | 24 | </head> | |
| 23 | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | |
| 24 | 26 | <a class="skip" href="#main">skip to content</a> | |
| @@ -19,6 +19,8 @@ | |||||
| 19 | </script> | 19 | </script> | ||
| 20 | <script src="/-/static/js/files.HASH.js" defer> | 20 | <script src="/-/static/js/files.HASH.js" defer> | ||
| 21 | </script> | 21 | </script> | ||
| 22 | + | <script src="/-/static/js/log.HASH.js" defer> | |||
| 23 | + | </script> | |||
| 22 | </head> | 24 | </head> | ||
| 23 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | 25 | <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="" data-paths="/files/paths/SHA40" data-blob="/files/blob/main"> | ||
| 24 | <a class="skip" href="#main">skip to content</a> | 26 | <a class="skip" href="#main">skip to content</a> | ||