feat(web): merge request layout, merge bar and lazy-loaded files (#51)
- commit
- 3c3845e5d8be66bcbd3b3c37e910ff00f2dda13c
- author
- brice · · 2026-10-17 22:27 UTC
- committer
- bnema · · 2026-10-17 22:27 UTC
- parent
- e08f000
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown
72 files · +3495 -1450
M internal/adapters/gitcli/diff.go +10 -3
| @@ -107,16 +107,22 @@ func readDiff(r *bufio.Reader, limits domain.DiffLimits) (domain.Diff, domain.Di | |||
| 107 | 107 | stats.Deletions += entries[i].del | |
| 108 | 108 | } | |
| 109 | 109 | var diff domain.Diff | |
| 110 | - | if len(entries) > limits.MaxFiles { | |
| 110 | + | if limits.MaxFiles > 0 && len(entries) > limits.MaxFiles { | |
| 111 | 111 | entries = entries[:limits.MaxFiles] | |
| 112 | 112 | diff.Truncated = true | |
| 113 | 113 | } | |
| 114 | + | // Only the files of the window [from, to) get their hunks; the others are | |
| 115 | + | // listed as they are, neither truncated nor counted in the budgets. | |
| 116 | + | from, to := max(limits.From, 0), len(entries) | |
| 117 | + | if limits.To > 0 { | |
| 118 | + | to = min(limits.To, len(entries)) | |
| 119 | + | } | |
| 114 | 120 | ||
| 115 | 121 | // The patch holds one section per file, two for a type change, in the | |
| 116 | 122 | // order of the entries. Entries without lines to show (binary, pure | |
| 117 | 123 | // rename, mode change) have a section without hunks. | |
| 118 | 124 | lastRead := -1 | |
| 119 | - | for i := range entries { | |
| 125 | + | for i := from; i < to; i++ { | |
| 120 | 126 | if wantsHunks(&entries[i], limits, limits.MaxTotalLines, limits.MaxTotalBytes) { | |
| 121 | 127 | lastRead = i | |
| 122 | 128 | } | |
| @@ -136,8 +142,9 @@ func readDiff(r *bufio.Reader, limits domain.DiffLimits) (domain.Diff, domain.Di | |||
| 136 | 142 | if e.typeChange { | |
| 137 | 143 | sections = 2 | |
| 138 | 144 | } | |
| 145 | + | inWindow := i >= from && i < to | |
| 139 | 146 | switch { | |
| 140 | - | case !changedLines(e): | |
| 147 | + | case !inWindow, !changedLines(e): | |
| 141 | 148 | // Nothing to show. | |
| 142 | 149 | case !wantsHunks(e, limits, budget, pr.bytesLeft): | |
| 143 | 150 | e.file.Truncated = true | |
M internal/adapters/gitcli/diff_test.go +176 -0
| @@ -484,3 +484,179 @@ func TestGetCommitStopsGitEarly(t *testing.T) { | |||
| 484 | 484 | assert.True(t, d.Diff.Files[1].Truncated) | |
| 485 | 485 | assert.Equal(t, 100005, d.Stats.Additions) | |
| 486 | 486 | } | |
| 487 | + | ||
| 488 | + | // windowFixture commits a diff that mixes what the patch reader must step over | |
| 489 | + | // in a window: text, binary, a rename, a type change (two sections), a mode | |
| 490 | + | // change (no hunks) and more text. It returns the id of the second commit. | |
| 491 | + | func windowFixture(t *testing.T, data, name string) (*readertest.Fixture, string) { | |
| 492 | + | t.Helper() | |
| 493 | + | f := readertest.NewFixture(t, data, name) | |
| 494 | + | f.Lines("a-text", "a", 20).WriteBytes("b-bin", []byte{0, 1, 2}).Lines("c-old", "c", 30). | |
| 495 | + | Lines("d-type", "d", 5).Lines("e-mode", "e", 3).Lines("f-text", "f", 20).Lines("g-text", "g", 20) | |
| 496 | + | f.Commit("v1") | |
| 497 | + | f.Lines("a-text", "A", 21).WriteBytes("b-bin", []byte{0, 3}).Mv("c-old", "c-new"). | |
| 498 | + | Remove("d-type").Symlink("a-text", "d-type").Chmod("e-mode", 0o755).Lines("f-text", "F", 20).Lines("g-text", "G", 2) | |
| 499 | + | f.Write("c-new", readLines("c", 30)+"extra\n") // a rename with one line added | |
| 500 | + | id := f.Commit("v2") | |
| 501 | + | return f, id | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | func readLines(prefix string, n int) string { | |
| 505 | + | var b strings.Builder | |
| 506 | + | for i := range n { | |
| 507 | + | fmt.Fprintf(&b, "%s %d\n", prefix, i) | |
| 508 | + | } | |
| 509 | + | return b.String() | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | func TestDiffLimitsWindow(t *testing.T) { | |
| 513 | + | ctx := context.Background() | |
| 514 | + | g, data := newReader(t, nil) | |
| 515 | + | ||
| 516 | + | t.Run("a window reads the hunks of its files only, whatever the files around it", func(t *testing.T) { | |
| 517 | + | f, id := windowFixture(t, data, "win-mixed") | |
| 518 | + | full, err := g.GetCommit(ctx, f.Name, readertest.Rev(t, id), domain.DefaultDiffLimits()) | |
| 519 | + | require.NoError(t, err) | |
| 520 | + | assertMatchesGitShow(t, f, id, full) | |
| 521 | + | n := len(full.Diff.Files) | |
| 522 | + | require.GreaterOrEqual(t, n, 7) | |
| 523 | + | status := map[domain.FileStatus]bool{} | |
| 524 | + | for _, file := range full.Diff.Files { | |
| 525 | + | status[file.Status] = true | |
| 526 | + | } | |
| 527 | + | assert.True(t, status[domain.FileTypeChanged], "the fixture has a type change") | |
| 528 | + | assert.True(t, status[domain.FileRenamed], "the fixture has a rename") | |
| 529 | + | assert.False(t, full.Diff.Truncated) | |
| 530 | + | ||
| 531 | + | tests := []struct { | |
| 532 | + | name string | |
| 533 | + | from, to int | |
| 534 | + | }{ | |
| 535 | + | {"first file", 0, 1}, | |
| 536 | + | {"middle", 2, 5}, | |
| 537 | + | {"type change alone", 3, 4}, | |
| 538 | + | {"binary alone", 1, 2}, | |
| 539 | + | {"mode change alone", 4, 5}, | |
| 540 | + | {"rename alone", 2, 3}, | |
| 541 | + | {"through the end, to 0", 3, 0}, | |
| 542 | + | {"to past the end", 5, 100}, | |
| 543 | + | {"last file", n - 1, n}, | |
| 544 | + | {"from past the end", n, n + 5}, | |
| 545 | + | {"far past the end", 1000, 0}, | |
| 546 | + | {"empty window", 3, 3}, | |
| 547 | + | {"whole diff", 0, 0}, | |
| 548 | + | } | |
| 549 | + | for _, tc := range tests { | |
| 550 | + | t.Run(tc.name, func(t *testing.T) { | |
| 551 | + | limits := domain.DefaultDiffLimits() | |
| 552 | + | limits.From, limits.To = tc.from, tc.to | |
| 553 | + | d, err := g.GetCommit(ctx, f.Name, readertest.Rev(t, id), limits) | |
| 554 | + | require.NoError(t, err) | |
| 555 | + | require.Len(t, d.Diff.Files, n) | |
| 556 | + | assert.Equal(t, full.Stats, d.Stats) | |
| 557 | + | assert.False(t, d.Diff.Truncated, "files outside the window do not truncate the diff") | |
| 558 | + | to := tc.to | |
| 559 | + | if to == 0 { | |
| 560 | + | to = n | |
| 561 | + | } | |
| 562 | + | for i, got := range d.Diff.Files { | |
| 563 | + | want := full.Diff.Files[i] | |
| 564 | + | if i < tc.from || i >= to { | |
| 565 | + | want.Hunks = nil | |
| 566 | + | } | |
| 567 | + | assert.Equal(t, want, got, "file %d (%s)", i, want.NewPath) | |
| 568 | + | } | |
| 569 | + | }) | |
| 570 | + | } | |
| 571 | + | }) | |
| 572 | + | ||
| 573 | + | t.Run("files outside the window use none of the total budget", func(t *testing.T) { | |
| 574 | + | f := readertest.NewFixture(t, data, "win-budget") | |
| 575 | + | commitFiles(f, "five", map[string]int{"a": 10, "b": 10, "c": 10, "d": 10, "e": 10}) | |
| 576 | + | limits := domain.DiffLimits{MaxFileLines: 100, MaxTotalLines: 15, MaxTotalBytes: 1 << 20, From: 3, To: 5} | |
| 577 | + | d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits) | |
| 578 | + | require.NoError(t, err) | |
| 579 | + | require.Len(t, d.Diff.Files, 5) | |
| 580 | + | for i, file := range d.Diff.Files { | |
| 581 | + | switch i { | |
| 582 | + | case 3: | |
| 583 | + | assert.False(t, file.Truncated) | |
| 584 | + | assert.Len(t, file.Hunks, 1, "10 lines fit in 15") | |
| 585 | + | case 4: | |
| 586 | + | assert.True(t, file.Truncated, "the second file of the window does not fit in the 5 lines left") | |
| 587 | + | default: | |
| 588 | + | assert.False(t, file.Truncated) | |
| 589 | + | assert.Empty(t, file.Hunks) | |
| 590 | + | assert.Equal(t, 10, file.Additions, "counts stay accurate") | |
| 591 | + | } | |
| 592 | + | } | |
| 593 | + | assert.True(t, d.Diff.Truncated, "a file of the window is truncated") | |
| 594 | + | ||
| 595 | + | limits.From, limits.To = 0, 0 | |
| 596 | + | limits.MaxTotalBytes = 25 // "a 0\n".."a 9\n" is 30 bytes of text per file | |
| 597 | + | d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits) | |
| 598 | + | require.NoError(t, err) | |
| 599 | + | assert.True(t, d.Diff.Files[0].Truncated) | |
| 600 | + | }) | |
| 601 | + | ||
| 602 | + | t.Run("a file beyond the window is not truncated by its own size", func(t *testing.T) { | |
| 603 | + | f := readertest.NewFixture(t, data, "win-huge") | |
| 604 | + | f.Lines("a", "x", 5).Lines("z-huge", "y", 5000).Commit("c") | |
| 605 | + | limits := domain.DefaultDiffLimits() | |
| 606 | + | limits.From, limits.To = 0, 1 | |
| 607 | + | d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits) | |
| 608 | + | require.NoError(t, err) | |
| 609 | + | require.Len(t, d.Diff.Files, 2) | |
| 610 | + | assert.Len(t, d.Diff.Files[0].Hunks, 1) | |
| 611 | + | assert.False(t, d.Diff.Files[1].Truncated) | |
| 612 | + | assert.Empty(t, d.Diff.Files[1].Hunks) | |
| 613 | + | assert.False(t, d.Diff.Truncated) | |
| 614 | + | limits.From, limits.To = 1, 2 | |
| 615 | + | d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits) | |
| 616 | + | require.NoError(t, err) | |
| 617 | + | assert.True(t, d.Diff.Files[1].Truncated, "in the window, over the per-file limit") | |
| 618 | + | assert.True(t, d.Diff.Truncated) | |
| 619 | + | }) | |
| 620 | + | ||
| 621 | + | t.Run("MaxFiles 0 lists every file", func(t *testing.T) { | |
| 622 | + | f := readertest.NewFixture(t, data, "win-many") | |
| 623 | + | const total = 350 | |
| 624 | + | for i := range total { | |
| 625 | + | f.Write(fmt.Sprintf("many/f%03d", i), fmt.Sprintf("x%d\n", i)) | |
| 626 | + | } | |
| 627 | + | f.Commit("many") | |
| 628 | + | limits := domain.DefaultDiffLimits() | |
| 629 | + | limits.MaxFiles = 0 | |
| 630 | + | d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits) | |
| 631 | + | require.NoError(t, err) | |
| 632 | + | require.Len(t, d.Diff.Files, total) | |
| 633 | + | assert.False(t, d.Diff.Truncated) | |
| 634 | + | assert.Equal(t, domain.DiffStats{FilesChanged: total, Additions: total}, d.Stats) | |
| 635 | + | for i, file := range d.Diff.Files { | |
| 636 | + | assert.Equal(t, fmt.Sprintf("many/f%03d", i), file.NewPath) | |
| 637 | + | require.Len(t, file.Hunks, 1, file.NewPath) | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // A window past the 300th file reads its own hunks. | |
| 641 | + | limits.From, limits.To = 320, 330 | |
| 642 | + | d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits) | |
| 643 | + | require.NoError(t, err) | |
| 644 | + | require.Len(t, d.Diff.Files, total) | |
| 645 | + | assert.False(t, d.Diff.Truncated) | |
| 646 | + | for i, file := range d.Diff.Files { | |
| 647 | + | if i >= 320 && i < 330 { | |
| 648 | + | require.Len(t, file.Hunks, 1) | |
| 649 | + | assert.Equal(t, fmt.Sprintf("x%d", i), file.Hunks[0].Lines[0].Text) | |
| 650 | + | } else { | |
| 651 | + | assert.Empty(t, file.Hunks) | |
| 652 | + | } | |
| 653 | + | assert.False(t, file.Truncated) | |
| 654 | + | } | |
| 655 | + | ||
| 656 | + | // The default limits still cut at 300. | |
| 657 | + | d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, domain.DefaultDiffLimits()) | |
| 658 | + | require.NoError(t, err) | |
| 659 | + | assert.Len(t, d.Diff.Files, 300) | |
| 660 | + | assert.True(t, d.Diff.Truncated) | |
| 661 | + | }) | |
| 662 | + | } | |
M internal/adapters/rpc/mergerequest.go +1 -1
| @@ -166,7 +166,7 @@ func (s *mergeRequestService) GetMergeRequest(ctx context.Context, req *connect. | |||
| 166 | 166 | if err != nil { | |
| 167 | 167 | return nil, connectError(ctx, err) | |
| 168 | 168 | } | |
| 169 | - | d, err := s.mrs.Get(ctx, caller(ctx), name, req.Msg.GetNumber(), req.Msg.GetSinceLastRead()) | |
| 169 | + | d, err := s.mrs.Get(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.GetMergeRequestOptions{SinceLastRead: req.Msg.GetSinceLastRead()}) | |
| 170 | 170 | if err != nil { | |
| 171 | 171 | return nil, connectError(ctx, err) | |
| 172 | 172 | } | |
M internal/adapters/rpc/mergerequest_test.go +1 -1
| @@ -212,7 +212,7 @@ func TestMergeRequestServiceGet(t *testing.T) { | |||
| 212 | 212 | t.Run(tt.name, func(t *testing.T) { | |
| 213 | 213 | t.Parallel() | |
| 214 | 214 | mrs := mocks.NewMergeRequestService(t) | |
| 215 | - | mrs.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(4), true). | |
| 215 | + | mrs.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(4), ports.GetMergeRequestOptions{SinceLastRead: true}). | |
| 216 | 216 | Return(ports.MergeRequestDetail{MergeRequest: domain.MergeRequest{Number: 4}, CommitsTruncated: true}, tt.usecase).Once() | |
| 217 | 217 | resp, err := (&mergeRequestService{mrs}).GetMergeRequest(ownerCtx(), connect.NewRequest(&pb.GetMergeRequestRequest{Repo: "demo", Number: 4, SinceLastRead: true})) | |
| 218 | 218 | if tt.wantCode != 0 { | |
M internal/adapters/web/actions.templ +14 -0
| @@ -423,6 +423,20 @@ templ CommitChecks(c ChecksSummary) { | |||
| 423 | 423 | } | |
| 424 | 424 | } | |
| 425 | 425 | ||
| 426 | + | // MRChecksSum is the state of the checks of a merge request in the bar above | |
| 427 | + | // the diff, a link to the checks of the rail. The event stream replaces it | |
| 428 | + | // with the checks section (same event, fragments.go). | |
| 429 | + | templ MRChecksSum(c ChecksSummary) { | |
| 430 | + | <a class="ck-sum mr-ck" id="mr-ck" href="#mr-checks" title={ c.Title }> | |
| 431 | + | if c.Total > 0 { | |
| 432 | + | @CheckIcon(c.Kind, c.State) | |
| 433 | + | <span>{ c.Label }</span> | |
| 434 | + | } else { | |
| 435 | + | <span class="mute">no checks</span> | |
| 436 | + | } | |
| 437 | + | </a> | |
| 438 | + | } | |
| 439 | + | ||
| 426 | 440 | // MRChecks is the checks section of the rail of a merge request. | |
| 427 | 441 | templ MRChecks(c ChecksSection) { | |
| 428 | 442 | <section aria-labelledby="r-checks" id="mr-checks"> | |
M internal/adapters/web/actions_templ.go +120 -43 generated file: collapsed
M internal/adapters/web/assets.go +2 -1
| @@ -24,7 +24,7 @@ const StaticPrefix = "/-/static/" | |||
| 24 | 24 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | |
| 25 | 25 | // the content, so they can be cached for ever. | |
| 26 | 26 | type Assets struct { | |
| 27 | - | CSS, HTMX, Live, SSE, App, Files, Passkey, Log, Icon string | |
| 27 | + | CSS, HTMX, Live, SSE, App, Files, Passkey, Log, MR, Icon string | |
| 28 | 28 | // Manifest is the web app manifest (install on a phone's home screen), | |
| 29 | 29 | // AppleIcon the home screen icon iOS reads from a link instead. | |
| 30 | 30 | Manifest, AppleIcon string | |
| @@ -62,6 +62,7 @@ var scripts = []struct { | |||
| 62 | 62 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | |
| 63 | 63 | {"passkey.js", func(a *Assets, u string) { a.Passkey = u }}, | |
| 64 | 64 | {"log.js", func(a *Assets, u string) { a.Log = u }}, | |
| 65 | + | {"mr.js", func(a *Assets, u string) { a.MR = u }}, | |
| 65 | 66 | } | |
| 66 | 67 | ||
| 67 | 68 | // sheetAssets are the files the stylesheet references as url(<dir>/<file>): | |
M internal/adapters/web/diff.go +61 -34
| @@ -80,10 +80,6 @@ type DiffHunk struct { | |||
| 80 | 80 | Split []SplitRow | |
| 81 | 81 | } | |
| 82 | 82 | ||
| 83 | - | // Bar is the change bar of the file list, "+++--·····", in three runs so each | |
| 84 | - | // can carry its colour. | |
| 85 | - | type Bar struct{ Add, Del, Rest string } | |
| 86 | - | ||
| 87 | 83 | // DiffFile is one changed file. | |
| 88 | 84 | type DiffFile struct { | |
| 89 | 85 | ID string | |
| @@ -93,12 +89,48 @@ type DiffFile struct { | |||
| 93 | 89 | Key string | |
| 94 | 90 | Path string // display path, "old → new" for a rename | |
| 95 | 91 | Add, Del int | |
| 96 | - | Bar Bar | |
| 92 | + | // Bin is true for a binary file: no lines to count. | |
| 93 | + | Bin bool | |
| 97 | 94 | // Note is why there is no text to show, or what else changed: a binary | |
| 98 | 95 | // file, a mode change, a truncation, a similarity. | |
| 99 | 96 | Note string | |
| 100 | 97 | Open bool | |
| 101 | 98 | Hunks []DiffHunk | |
| 99 | + | // Digits is the width of the largest line number of the file, at least 2 | |
| 100 | + | // and at most 7: the gutter is as wide as the file needs (class lnw-N). | |
| 101 | + | Digits int | |
| 102 | + | // Name and Dir are the last element of the path and the directory before | |
| 103 | + | // it ("" at the root), for the compact file list. | |
| 104 | + | Name, Dir string | |
| 105 | + | // Notes is the number of comments on lines of the file, Open those not | |
| 106 | + | // resolved; the file list shows them. | |
| 107 | + | Notes, OpenNotes int | |
| 108 | + | // Lazy is true when the hunks of the file were not read: the page shows a | |
| 109 | + | // placeholder that loads them (Src, the window of files it belongs to). | |
| 110 | + | Lazy bool | |
| 111 | + | Src string | |
| 112 | + | // From is the index of the first file of the window of a lazy file: the | |
| 113 | + | // page without scripts links to ?from=From. | |
| 114 | + | From int | |
| 115 | + | // Height is the size class of a lazy file (1 to 4, by its changed lines), | |
| 116 | + | // so that the placeholder takes about the room of the diff. | |
| 117 | + | Height int | |
| 118 | + | // Comments are the ids of the comments on the file ("c12"): a link to a | |
| 119 | + | // comment of a lazy file loads that file first. | |
| 120 | + | Comments string | |
| 121 | + | // Loose are the comments of a file loaded with its window that are on a | |
| 122 | + | // line its diff does not show: listed at the end of the file. | |
| 123 | + | Loose []CommentView | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // gutterDigits is the number of digits of the largest line number of the | |
| 127 | + | // hunks, clamped to the widths the style sheet has a class for. | |
| 128 | + | func gutterDigits(hunks []domain.Hunk) int { | |
| 129 | + | most := 0 | |
| 130 | + | for _, h := range hunks { | |
| 131 | + | most = max(most, h.OldStart+h.OldLines, h.NewStart+h.NewLines) | |
| 132 | + | } | |
| 133 | + | return min(max(len(itoa(most)), 2), 7) | |
| 102 | 134 | } | |
| 103 | 135 | ||
| 104 | 136 | func lineNumber(n int) string { | |
| @@ -176,26 +208,6 @@ func pairLines(ls []DiffLine) []SplitRow { | |||
| 176 | 208 | return out | |
| 177 | 209 | } | |
| 178 | 210 | ||
| 179 | - | // changeBar draws the change bar of a file, scaled to the largest file. | |
| 180 | - | func changeBar(add, del, most int) Bar { | |
| 181 | - | const cells = 10 | |
| 182 | - | if most == 0 { | |
| 183 | - | return Bar{Rest: strings.Repeat("·", cells)} | |
| 184 | - | } | |
| 185 | - | scale := func(n int) int { | |
| 186 | - | c := (n*cells + most - 1) / most | |
| 187 | - | if n > 0 && c == 0 { | |
| 188 | - | c = 1 | |
| 189 | - | } | |
| 190 | - | return c | |
| 191 | - | } | |
| 192 | - | a, d := scale(add), scale(del) | |
| 193 | - | if a+d > cells { | |
| 194 | - | d = cells - a | |
| 195 | - | } | |
| 196 | - | return Bar{strings.Repeat("+", a), strings.Repeat("-", d), strings.Repeat("·", cells-a-d)} | |
| 197 | - | } | |
| 198 | - | ||
| 199 | 211 | // statusLetter is the one-letter status of a file. | |
| 200 | 212 | func statusLetter(s domain.FileStatus) string { | |
| 201 | 213 | switch s { | |
| @@ -213,6 +225,27 @@ func statusLetter(s domain.FileStatus) string { | |||
| 213 | 225 | return "M" | |
| 214 | 226 | } | |
| 215 | 227 | ||
| 228 | + | // fileFilterMin is the number of files above which the file list has a | |
| 229 | + | // filter. | |
| 230 | + | const fileFilterMin = 10 | |
| 231 | + | ||
| 232 | + | // statusName says what a status letter means, for its tooltip. | |
| 233 | + | func statusName(letter string) string { | |
| 234 | + | switch letter { | |
| 235 | + | case "A": | |
| 236 | + | return "added" | |
| 237 | + | case "D": | |
| 238 | + | return "deleted" | |
| 239 | + | case "R": | |
| 240 | + | return "renamed" | |
| 241 | + | case "C": | |
| 242 | + | return "copied" | |
| 243 | + | case "T": | |
| 244 | + | return "type changed" | |
| 245 | + | } | |
| 246 | + | return "modified" | |
| 247 | + | } | |
| 248 | + | ||
| 216 | 249 | // hunkHeader is the "@@ -a,b +c,d @@ context" line of a hunk. | |
| 217 | 250 | func hunkHeader(h domain.Hunk) string { | |
| 218 | 251 | s := fmt.Sprintf("@@ -%d,%d +%d,%d @@", h.OldStart, h.OldLines, h.NewStart, h.NewLines) | |
| @@ -239,16 +272,11 @@ func diffLine(l domain.Line) DiffLine { | |||
| 239 | 272 | ||
| 240 | 273 | // diffFiles converts the files of a diff for the page. | |
| 241 | 274 | func diffFiles(files []domain.FileDiff) []DiffFile { | |
| 242 | - | most := 0 | |
| 243 | - | for _, f := range files { | |
| 244 | - | most = max(most, f.Additions+f.Deletions) | |
| 245 | - | } | |
| 246 | 275 | out := make([]DiffFile, 0, len(files)) | |
| 247 | 276 | for i, f := range files { | |
| 248 | 277 | df := DiffFile{ | |
| 249 | 278 | ID: "f" + itoa(i+1), Status: statusLetter(f.Status), | |
| 250 | - | Add: f.Additions, Del: f.Deletions, | |
| 251 | - | Bar: changeBar(f.Additions, f.Deletions, most), | |
| 279 | + | Add: f.Additions, Del: f.Deletions, Bin: f.Binary, | |
| 252 | 280 | // A file that is shown in full opens; a file with nothing to read | |
| 253 | 281 | // (binary, truncated) stays closed under its note. | |
| 254 | 282 | Open: len(f.Hunks) > 0 && !f.Truncated, | |
| @@ -264,9 +292,8 @@ func diffFiles(files []domain.FileDiff) []DiffFile { | |||
| 264 | 292 | df.Path = visibleText(f.NewPath) | |
| 265 | 293 | } | |
| 266 | 294 | df.Note = fileNote(f) | |
| 267 | - | if f.Binary { | |
| 268 | - | df.Bar = Bar{Rest: "bin"} | |
| 269 | - | } | |
| 295 | + | df.Digits = gutterDigits(f.Hunks) | |
| 296 | + | df.Name, df.Dir = splitPath(df.Key) | |
| 270 | 297 | for _, h := range f.Hunks { | |
| 271 | 298 | dh := DiffHunk{Header: hunkHeader(h)} | |
| 272 | 299 | for _, l := range h.Lines { | |
M internal/adapters/web/diff.templ +57 -7
| @@ -141,17 +141,39 @@ templ commitRail(p CommitPage) { | |||
| 141 | 141 | // FileList is the table of contents of a diff: status letter, path, +n -m | |
| 142 | 142 | // and an ASCII bar. The row of the file in view is marked. | |
| 143 | 143 | templ FileList(files []DiffFile) { | |
| 144 | + | if len(files) > fileFilterMin { | |
| 145 | + | <div class="ffilter"> | |
| 146 | + | <label for="files-filter" class="sr">filter the changed files</label> | |
| 147 | + | <input | |
| 148 | + | id="files-filter" | |
| 149 | + | type="search" | |
| 150 | + | placeholder="Filter files…" | |
| 151 | + | autocomplete="off" | |
| 152 | + | data-pick=".files > li" | |
| 153 | + | hx-on:input="ermite.filter(this)" | |
| 154 | + | /> | |
| 155 | + | </div> | |
| 156 | + | } | |
| 144 | 157 | <ol class="files" aria-label="Changed files"> | |
| 145 | 158 | for _, f := range files { | |
| 146 | 159 | <li> | |
| 147 | 160 | <a | |
| 148 | 161 | href={ templ.SafeURL("#" + f.ID) } | |
| 149 | - | hx-on:click="document.getElementById(this.hash.slice(1)).open = true" | |
| 162 | + | title={ f.Path } | |
| 163 | + | hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))" | |
| 150 | 164 | > | |
| 151 | - | <span class="st" title={ f.Status }>{ f.Status }</span> | |
| 152 | - | <span class="p" title={ f.Path }>{ f.Path }</span> | |
| 153 | - | <span class="n"><span class="plus">+{ itoa(f.Add) }</span> <span class="minus">-{ itoa(f.Del) }</span></span> | |
| 154 | - | <span class="b" aria-hidden="true"><span class="plus">{ f.Bar.Add }</span><span class="minus">{ f.Bar.Del }</span>{ f.Bar.Rest }</span> | |
| 165 | + | <span class="st" data-st={ f.Status } title={ statusName(f.Status) }>{ f.Status }</span> | |
| 166 | + | <span class="nm"><span class="p">{ f.Name }</span>{ " " }<span class="d"><bdi>{ f.Dir }</bdi></span></span> | |
| 167 | + | if f.OpenNotes > 0 { | |
| 168 | + | <span class="cn" title={ plural(f.OpenNotes, "open comment", "open comments") }>{ itoa(f.OpenNotes) }</span> | |
| 169 | + | } else if f.Notes > 0 { | |
| 170 | + | <span class="cn done" title={ plural(f.Notes, "resolved comment", "resolved comments") }>{ itoa(f.Notes) }</span> | |
| 171 | + | } | |
| 172 | + | if f.Bin { | |
| 173 | + | <span class="n mute">bin</span> | |
| 174 | + | } else { | |
| 175 | + | <span class="n"><span class="plus">+{ itoa(f.Add) }</span> <span class="minus">-{ itoa(f.Del) }</span></span> | |
| 176 | + | } | |
| 155 | 177 | </a> | |
| 156 | 178 | </li> | |
| 157 | 179 | } | |
| @@ -164,7 +186,7 @@ templ FileList(files []DiffFile) { | |||
| 164 | 186 | // colour alone. | |
| 165 | 187 | templ DiffFileView(f DiffFile, p DiffView) { | |
| 166 | 188 | <details | |
| 167 | - | class="dfile" | |
| 189 | + | class={ "dfile", "lnw-" + itoa(f.Digits) } | |
| 168 | 190 | id={ f.ID } | |
| 169 | 191 | data-file | |
| 170 | 192 | if f.Open { | |
| @@ -177,7 +199,14 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 177 | 199 | { " " }<span class="note">{ f.Note }</span> | |
| 178 | 200 | } | |
| 179 | 201 | </summary> | |
| 180 | - | if len(f.Hunks) > 0 { | |
| 202 | + | if f.Lazy { | |
| 203 | + | // A placeholder: mr.js fetches the window of the file when it comes | |
| 204 | + | // near the screen and swaps the files of the window in. Without | |
| 205 | + | // scripts the link opens the page of that window. | |
| 206 | + | <div class={ "dlazy", "dlazy-" + itoa(f.Height) } data-src={ f.Src } data-notes={ f.Comments }> | |
| 207 | + | <a href={ templ.SafeURL("?from=" + itoa(f.From) + "#" + f.ID) }>show this file</a> | |
| 208 | + | </div> | |
| 209 | + | } else if len(f.Hunks) > 0 { | |
| 181 | 210 | <div class="dscroll"> | |
| 182 | 211 | if p.ShowUnified { | |
| 183 | 212 | <table class="dt uni" aria-label={ f.Path + ", unified" }> | |
| @@ -233,9 +262,30 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 233 | 262 | } | |
| 234 | 263 | </p> | |
| 235 | 264 | } | |
| 265 | + | if len(f.Loose) > 0 { | |
| 266 | + | <div class="dloose"> | |
| 267 | + | for _, c := range f.Loose { | |
| 268 | + | @commentView(c, "") | |
| 269 | + | } | |
| 270 | + | </div> | |
| 271 | + | } | |
| 236 | 272 | </details> | |
| 237 | 273 | } | |
| 238 | 274 | ||
| 275 | + | // MRFiles is a window of files of a merge request, for the placeholders of | |
| 276 | + | // the page: the files only, which replace the placeholders of the same ids. | |
| 277 | + | templ MRFiles(p MRFilesPage) { | |
| 278 | + | for _, f := range p.Files { | |
| 279 | + | @DiffFileView(f, p.DiffView) | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | // MRFilesMoved answers a window of a diff that is not the one of the page any | |
| 284 | + | // more: the placeholder shows it instead of a file of another diff. | |
| 285 | + | templ MRFilesMoved() { | |
| 286 | + | <p class="notice" data-moved>this merge request changed since the page was loaded — <a href="">reload</a></p> | |
| 287 | + | } | |
| 288 | + | ||
| 239 | 289 | // splitSide is one half of a row of the split view; a side without a line is | |
| 240 | 290 | // an empty cell. | |
| 241 | 291 | templ splitSide(ln *DiffLine, old bool) { | |
M internal/adapters/web/diff_templ.go +463 -215 generated file: collapsed
M internal/adapters/web/diff_test.go +0 -9
| @@ -85,15 +85,6 @@ func TestPairLines(t *testing.T) { | |||
| 85 | 85 | assert.Equal(t, DiffLine{Kind: "add", Mark: "+", New: "2", Segs: []Seg{{Text: "a1"}}, newN: 2}, ls[4]) | |
| 86 | 86 | } | |
| 87 | 87 | ||
| 88 | - | func TestChangeBar(t *testing.T) { | |
| 89 | - | t.Parallel() | |
| 90 | - | assert.Equal(t, Bar{Rest: "··········"}, changeBar(0, 0, 0)) | |
| 91 | - | assert.Equal(t, Bar{Add: "++++++++++"}, changeBar(100, 0, 100)) | |
| 92 | - | assert.Equal(t, Bar{Add: "+++++", Del: "-----", Rest: ""}, changeBar(50, 50, 100)) | |
| 93 | - | assert.Equal(t, Bar{Add: "+", Rest: "·········"}, changeBar(1, 0, 1000), "a change is never rounded away") | |
| 94 | - | assert.Equal(t, 10, len(changeBar(7, 7, 8).Add+changeBar(7, 7, 8).Del+changeBar(7, 7, 8).Rest), "never wider than the bar") | |
| 95 | - | } | |
| 96 | - | ||
| 97 | 88 | func TestFileNotes(t *testing.T) { | |
| 98 | 89 | t.Parallel() | |
| 99 | 90 | tests := []struct { | |
M internal/adapters/web/fragments.go +7 -2
| @@ -496,8 +496,13 @@ func (l *liveStream) mrChecks(ctx context.Context) ([]fragment, error) { | |||
| 496 | 496 | case err != nil: | |
| 497 | 497 | return nil, err | |
| 498 | 498 | } | |
| 499 | - | f, err := renderFragment(ctx, "ci-run", MRChecks(checksSection(repo, l.s.checksFor(ctx, name, l.head)))) | |
| 500 | - | return []fragment{f}, err | |
| 499 | + | checks := l.s.checksFor(ctx, name, l.head) | |
| 500 | + | f, err := renderFragment(ctx, "ci-run", MRChecks(checksSection(repo, checks))) | |
| 501 | + | if err != nil { | |
| 502 | + | return nil, err | |
| 503 | + | } | |
| 504 | + | sum, err := renderFragment(ctx, "ci-run", MRChecksSum(checksSummary(name, l.head, checks))) | |
| 505 | + | return []fragment{f, sum}, err | |
| 501 | 506 | } | |
| 502 | 507 | ||
| 503 | 508 | // recentActions is the "Recent actions" block of the summary rail, read again | |
A internal/adapters/web/jstest/mr.test.mjs +38 -0
| @@ -0,0 +1,38 @@ | |||
| 1 | + | // Tests of the pure core of the merge request page (static/js/mr.js). Run with | |
| 2 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |
| 3 | + | import assert from 'node:assert/strict'; | |
| 4 | + | import { test } from 'node:test'; | |
| 5 | + | ||
| 6 | + | globalThis.window = globalThis; | |
| 7 | + | globalThis.document = { readyState: 'complete', querySelector: () => null, querySelectorAll: () => [] }; | |
| 8 | + | await import('../static/js/mr.js'); | |
| 9 | + | const C = window.ermiteMR.core; | |
| 10 | + | ||
| 11 | + | test('windowOf: the start of the window of a file', () => { | |
| 12 | + | assert.equal(C.windowOf('f1', 25), 0); | |
| 13 | + | assert.equal(C.windowOf('f25', 25), 0); | |
| 14 | + | assert.equal(C.windowOf('f26', 25), 25); | |
| 15 | + | assert.equal(C.windowOf('f84', 25), 75); | |
| 16 | + | assert.equal(C.windowOf('c3', 25), -1, 'not a file'); | |
| 17 | + | assert.equal(C.windowOf('f0', 25), -1); | |
| 18 | + | }); | |
| 19 | + | ||
| 20 | + | test('menuKey: modes by number, D toggles the deletion, nothing else', () => { | |
| 21 | + | assert.deepEqual(C.menuKey('1', 2), { mode: 0 }); | |
| 22 | + | assert.deepEqual(C.menuKey('2', 2), { mode: 1 }); | |
| 23 | + | assert.equal(C.menuKey('3', 2), null, 'no third mode'); | |
| 24 | + | assert.equal(C.menuKey('0', 2), null); | |
| 25 | + | assert.deepEqual(C.menuKey('D', 2), { toggle: true }); | |
| 26 | + | assert.equal(C.menuKey('d', 2), null, 'd is the split layout'); | |
| 27 | + | assert.equal(C.menuKey('Enter', 2), null, 'the browser submits: the focus is on the button'); | |
| 28 | + | assert.equal(C.menuKey('M', 2), null); | |
| 29 | + | }); | |
| 30 | + | ||
| 31 | + | test('step: stays in the list', () => { | |
| 32 | + | assert.equal(C.step(0, 1, 3), 1); | |
| 33 | + | assert.equal(C.step(2, 1, 3), 2); | |
| 34 | + | assert.equal(C.step(0, -1, 3), 0); | |
| 35 | + | assert.equal(C.step(-1, 1, 3), 0); | |
| 36 | + | assert.equal(C.step(-1, -1, 3), 2); | |
| 37 | + | assert.equal(C.step(0, 1, 0), -1); | |
| 38 | + | }); | |
M internal/adapters/web/keys.templ +4 -1
| @@ -29,6 +29,9 @@ templ Keys() { | |||
| 29 | 29 | k-split from:body -> ermite.layout('split'); | |
| 30 | 30 | k-unified from:body -> ermite.layout('unified'); | |
| 31 | 31 | k-collapse from:body -> ermite.collapse(); | |
| 32 | - | k-permalink from:body -> ermite.permalink()" | |
| 32 | + | k-permalink from:body -> ermite.permalink(); | |
| 33 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 34 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 35 | + | k-gf from:body -> window.ermiteMR?.focusFiles()" | |
| 33 | 36 | ></div> | |
| 34 | 37 | } | |
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.go +70 -13
| @@ -28,6 +28,13 @@ import ( | |||
| 28 | 28 | // GET /<repo>/merge-requests/<n> the request with its diff | |
| 29 | 29 | // GET /<repo>/merge-requests/<n>/since the diff since the last read | |
| 30 | 30 | // GET /<repo>/compare/<branch> a branch against the default branch | |
| 31 | + | // GET /<repo>/merge-requests/<n>/files a window of files of the diff | |
| 32 | + | // | |
| 33 | + | // The page lists every changed file and reads the hunks of the first window | |
| 34 | + | // of files only (mrWindow); the files after it are placeholders that the | |
| 35 | + | // browser fills in, a window per request, as they come near the screen | |
| 36 | + | // (/files?from=&base=&head=). Without scripts each placeholder links to the | |
| 37 | + | // page of its window (?from=), which shows the hunks of that window. | |
| 31 | 38 | // | |
| 32 | 39 | // Reading marks nothing as read. MarkRead stores state, and a GET must stay | |
| 33 | 40 | // safe: a prefetcher, a link preview or a reload of the owner's tab would move | |
| @@ -43,6 +50,9 @@ import ( | |||
| 43 | 50 | // forms and the CLI lines exist only in the owner's copy. | |
| 44 | 51 | ||
| 45 | 52 | const ( | |
| 53 | + | // mrWindow is how many files of a merge request one read gets the hunks | |
| 54 | + | // of: the page shows the first window and loads the others. | |
| 55 | + | mrWindow = 25 | |
| 46 | 56 | // mrPerPage is how many decided merge requests a page of the list shows, | |
| 47 | 57 | // at least; pages of the use case are read whole. | |
| 48 | 58 | mrPerPage = 50 | |
| @@ -114,6 +124,8 @@ type ResolveForm struct { | |||
| 114 | 124 | // Value is the state the form sets. | |
| 115 | 125 | Value bool | |
| 116 | 126 | Label string | |
| 127 | + | // From is the window of the page the comment is on (CommentForm.From). | |
| 128 | + | From int | |
| 117 | 129 | } | |
| 118 | 130 | ||
| 119 | 131 | // CommentForm is the POST that adds a comment. | |
| @@ -125,6 +137,9 @@ type CommentForm struct { | |||
| 125 | 137 | Path, Line, Side string | |
| 126 | 138 | Cancel string | |
| 127 | 139 | Focus bool | |
| 140 | + | // From is the window of the page the form is on (see mrWindow), sent | |
| 141 | + | // back so that the answer returns to it; 0 for the first. | |
| 142 | + | From int | |
| 128 | 143 | } | |
| 129 | 144 | ||
| 130 | 145 | // MRCommit is a commit of the request. | |
| @@ -198,8 +213,11 @@ type MRPage struct { | |||
| 198 | 213 | Invisible bool | |
| 199 | 214 | ||
| 200 | 215 | // General are the comments that are not on a line, Loose those on a line | |
| 201 | - | // that the diff shown does not have. | |
| 216 | + | // that the diff shown does not have. With LooseInFiles (a window of files | |
| 217 | + | // loaded later) the loose comments of a listed file go under the file | |
| 218 | + | // instead (DiffFile.Loose). | |
| 202 | 219 | General, Loose []CommentView | |
| 220 | + | LooseInFiles bool | |
| 203 | 221 | NComments int | |
| 204 | 222 | NUnresolved int | |
| 205 | 223 | GeneralForm *CommentForm | |
| @@ -208,12 +226,16 @@ type MRPage struct { | |||
| 208 | 226 | CommitsNote string | |
| 209 | 227 | Modes []string | |
| 210 | 228 | ChecksBlock bool | |
| 211 | - | // Checks are the CI checks of the head of the request. | |
| 212 | - | Checks ChecksSection | |
| 229 | + | // Checks are the CI checks of the head of the request, ChecksSum their | |
| 230 | + | // state in the bar above the diff. | |
| 231 | + | Checks ChecksSection | |
| 232 | + | ChecksSum ChecksSummary | |
| 213 | 233 | ||
| 214 | 234 | Box *MergeBox | |
| 215 | 235 | // Live is true when the page announces changes. | |
| 216 | 236 | Live bool | |
| 237 | + | // From is the first file of the window the page read the hunks of (?from=). | |
| 238 | + | From int | |
| 217 | 239 | } | |
| 218 | 240 | ||
| 219 | 241 | // CompareCommit is a commit of a branch that the target lacks. | |
| @@ -419,10 +441,19 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 419 | 441 | if err != nil { | |
| 420 | 442 | return nil, 0, err | |
| 421 | 443 | } | |
| 422 | - | res, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, since) | |
| 444 | + | from, err := parseFrom(r) | |
| 423 | 445 | if err != nil { | |
| 424 | 446 | return nil, 0, err | |
| 425 | 447 | } | |
| 448 | + | res, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | |
| 449 | + | SinceLastRead: since, Files: &ports.FileWindow{From: from, To: from + mrWindow}, | |
| 450 | + | }) | |
| 451 | + | if err != nil { | |
| 452 | + | return nil, 0, err | |
| 453 | + | } | |
| 454 | + | if from > 0 && from >= len(res.Diff.Files) { | |
| 455 | + | return nil, 0, errNotFound | |
| 456 | + | } | |
| 426 | 457 | m := res.MergeRequest | |
| 427 | 458 | now := s.cfg.Now() | |
| 428 | 459 | signed := SignedIn(ctx) | |
| @@ -470,13 +501,20 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 470 | 501 | } | |
| 471 | 502 | } | |
| 472 | 503 | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | |
| 473 | - | if open && head != "" { | |
| 474 | - | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head)) | |
| 475 | - | } else if head != "" { | |
| 476 | - | p.Checks = checksSection(d.repo.Repo, s.checksFor(ctx, d.repo.Name, head)) | |
| 477 | - | p.Checks.Note = "" | |
| 504 | + | if head != "" { | |
| 505 | + | checks := s.checksFor(ctx, d.repo.Name, head) | |
| 506 | + | p.Checks = checksSection(d.repo.Repo, checks) | |
| 507 | + | p.ChecksSum = checksSummary(d.repo.Name, head, checks) | |
| 508 | + | if !open { | |
| 509 | + | p.Checks.Note = "" | |
| 510 | + | } | |
| 478 | 511 | } | |
| 479 | 512 | p.Note = diffNote(res.Diff, len(p.Files), p.NFiles) | |
| 513 | + | countNotes(p.Files, res.Comments) | |
| 514 | + | if !res.DiffBaseID.IsZero() { | |
| 515 | + | lazyFiles(p.Files, from, filesSrc(d.name(), n, since, res.DiffBaseID.String(), head)) | |
| 516 | + | } | |
| 517 | + | p.From = from | |
| 480 | 518 | ||
| 481 | 519 | // The commits of the request, newest first. | |
| 482 | 520 | for _, c := range res.Commits { | |
| @@ -538,7 +576,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 538 | 576 | s.placeComments(&p, d.name(), n, res.Comments, head, !since, canWrite, on) | |
| 539 | 577 | lines, invisible := diffLines(p.Files) | |
| 540 | 578 | p.Invisible = invisible | |
| 541 | - | p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split") | |
| 579 | + | p.DiffView = newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from)) | |
| 542 | 580 | if signed && open { | |
| 543 | 581 | p.Box = s.mergeBox(d, n, m.Source, head, lastRead) | |
| 544 | 582 | if m.IsDraft() { | |
| @@ -570,6 +608,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 570 | 608 | p.Live = true | |
| 571 | 609 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source)+"&head="+url.QueryEscape(head)) | |
| 572 | 610 | l.HasRail = true | |
| 611 | + | l.MR = true | |
| 573 | 612 | return MergeRequest(l, p), http.StatusOK, nil | |
| 574 | 613 | } | |
| 575 | 614 | ||
| @@ -666,23 +705,30 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||
| 666 | 705 | } | |
| 667 | 706 | index := map[lineKey]*DiffLine{} | |
| 668 | 707 | fileIDs := map[string]string{} | |
| 708 | + | fileAt := map[string]int{} | |
| 669 | 709 | for fi := range p.Files { | |
| 670 | 710 | f := &p.Files[fi] | |
| 671 | 711 | fileIDs[f.Key] = f.ID | |
| 712 | + | fileAt[f.Key] = fi | |
| 672 | 713 | linkable := canWrite && f.Key != "" && !strings.ContainsFunc(f.Key, invisible) | |
| 714 | + | // The form of a line shows on the page of the window of its file. | |
| 715 | + | on := self + "?on=" | |
| 716 | + | if w := windowStart(fi); w > 0 { | |
| 717 | + | on = self + "?from=" + itoa(w) + "&on=" | |
| 718 | + | } | |
| 673 | 719 | for hi := range f.Hunks { | |
| 674 | 720 | for li := range f.Hunks[hi].Lines { | |
| 675 | 721 | ln := &f.Hunks[hi].Lines[li] | |
| 676 | 722 | if ln.oldN > 0 && ln.Kind != "add" { | |
| 677 | 723 | index[lineKey{f.Key, domain.SideOld, ln.oldN}] = ln | |
| 678 | 724 | if linkable { | |
| 679 | - | ln.OldHref = self + "?on=old:" + itoa(ln.oldN) + ":" + url.QueryEscape(f.Key) + "#" + f.ID | |
| 725 | + | ln.OldHref = on + "old:" + itoa(ln.oldN) + ":" + url.QueryEscape(f.Key) + "#" + f.ID | |
| 680 | 726 | } | |
| 681 | 727 | } | |
| 682 | 728 | if ln.newN > 0 && ln.Kind != "del" { | |
| 683 | 729 | index[lineKey{f.Key, domain.SideNew, ln.newN}] = ln | |
| 684 | 730 | if linkable { | |
| 685 | - | ln.NewHref = self + "?on=new:" + itoa(ln.newN) + ":" + url.QueryEscape(f.Key) + "#" + f.ID | |
| 731 | + | ln.NewHref = on + "new:" + itoa(ln.newN) + ":" + url.QueryEscape(f.Key) + "#" + f.ID | |
| 686 | 732 | } | |
| 687 | 733 | } | |
| 688 | 734 | } | |
| @@ -711,6 +757,11 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||
| 711 | 757 | p.NUnresolved++ | |
| 712 | 758 | } | |
| 713 | 759 | general := c.Path == "" | |
| 760 | + | fi, listed := fileAt[c.Path] | |
| 761 | + | if !general && listed && p.Files[fi].Lazy { | |
| 762 | + | // Its file comes with its window, and the comment with it. | |
| 763 | + | continue | |
| 764 | + | } | |
| 714 | 765 | var ln *DiffLine | |
| 715 | 766 | if !general { | |
| 716 | 767 | ln = find(c.Path, c.Side, c.Line) | |
| @@ -724,6 +775,9 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||
| 724 | 775 | } | |
| 725 | 776 | if canWrite { | |
| 726 | 777 | v.Resolve = &ResolveForm{Action: resolveAction(cid), Value: !c.Resolved, Label: "Resolve"} | |
| 778 | + | if !general && listed { | |
| 779 | + | v.Resolve.From = windowStart(fi) | |
| 780 | + | } | |
| 727 | 781 | if c.Resolved { | |
| 728 | 782 | v.Resolve.Label = "Unresolve" | |
| 729 | 783 | } | |
| @@ -733,6 +787,8 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||
| 733 | 787 | ln.Notes = append(ln.Notes, v) | |
| 734 | 788 | case general: | |
| 735 | 789 | p.General = append(p.General, v) | |
| 790 | + | case p.LooseInFiles && listed: | |
| 791 | + | p.Files[fi].Loose = append(p.Files[fi].Loose, v) | |
| 736 | 792 | default: | |
| 737 | 793 | p.Loose = append(p.Loose, v) | |
| 738 | 794 | } | |
| @@ -747,7 +803,8 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai | |||
| 747 | 803 | ln.Form = &CommentForm{ | |
| 748 | 804 | ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side, | |
| 749 | 805 | Title: "comment on " + visibleText(on.path) + ":" + itoa(on.line) + " (" + on.side + ")", | |
| 750 | - | Cancel: self + "#" + fileIDs[on.path], Focus: true, | |
| 806 | + | Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true, | |
| 807 | + | From: windowStart(fileAt[on.path]), | |
| 751 | 808 | } | |
| 752 | 809 | } | |
| 753 | 810 | } | |
M internal/adapters/web/mr.templ +113 -75
| @@ -162,13 +162,7 @@ templ MergeRequest(l Layout, p MRPage) { | |||
| 162 | 162 | if p.SinceNote != "" { | |
| 163 | 163 | <p class="notice">{ p.SinceNote }</p> | |
| 164 | 164 | } | |
| 165 | - | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| 166 | - | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 167 | - | <span class="grow"></span> | |
| 168 | - | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 169 | - | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |
| 170 | - | @collapseButton() | |
| 171 | - | </div> | |
| 165 | + | @mrBar(p) | |
| 172 | 166 | if p.Invisible { | |
| 173 | 167 | <p class="notice"> | |
| 174 | 168 | this diff holds invisible characters (direction controls, zero-width characters). They are shown as | |
| @@ -180,6 +174,7 @@ templ MergeRequest(l Layout, p MRPage) { | |||
| 180 | 174 | } | |
| 181 | 175 | <div | |
| 182 | 176 | id="diffs" | |
| 177 | + | data-mr | |
| 183 | 178 | if p.Both { | |
| 184 | 179 | data-diff={ p.View } | |
| 185 | 180 | } else { | |
| @@ -256,6 +251,9 @@ templ commentView(c CommentView, suffix string) { | |||
| 256 | 251 | if c.Resolve != nil { | |
| 257 | 252 | <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }> | |
| 258 | 253 | <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/> | |
| 254 | + | if c.Resolve.From > 0 { | |
| 255 | + | <input type="hidden" name="from" value={ itoa(c.Resolve.From) }/> | |
| 256 | + | } | |
| 259 | 257 | <button type="submit" class="btn">{ c.Resolve.Label }</button> | |
| 260 | 258 | </form> | |
| 261 | 259 | } | |
| @@ -289,6 +287,9 @@ templ commentForm(f CommentForm, suffix string) { | |||
| 289 | 287 | <input type="hidden" name="line" value={ f.Line }/> | |
| 290 | 288 | <input type="hidden" name="side" value={ f.Side }/> | |
| 291 | 289 | } | |
| 290 | + | if f.From > 0 { | |
| 291 | + | <input type="hidden" name="from" value={ itoa(f.From) }/> | |
| 292 | + | } | |
| 292 | 293 | <div class="f-btns"> | |
| 293 | 294 | <button type="submit" class="btn primary">Comment</button> | |
| 294 | 295 | if f.Cancel != "" { | |
| @@ -299,25 +300,11 @@ templ commentForm(f CommentForm, suffix string) { | |||
| 299 | 300 | } | |
| 300 | 301 | ||
| 301 | 302 | templ mrRail(p MRPage) { | |
| 302 | - | if p.Box != nil { | |
| 303 | - | @mergeBox(p) | |
| 304 | - | } | |
| 305 | - | @MRChecks(p.Checks) | |
| 306 | - | <section aria-labelledby="r-settings"> | |
| 307 | - | <h2 id="r-settings">Settings</h2> | |
| 308 | - | <dl class="kv"> | |
| 309 | - | <dt>modes</dt> | |
| 310 | - | <dd>{ strings.Join(p.Modes, ", ") }</dd> | |
| 311 | - | <dt>checks</dt> | |
| 312 | - | <dd> | |
| 313 | - | if p.ChecksBlock { | |
| 314 | - | failing ones block the merge | |
| 315 | - | } else { | |
| 316 | - | do not block the merge | |
| 317 | - | } | |
| 318 | - | </dd> | |
| 319 | - | </dl> | |
| 303 | + | <section aria-labelledby="r-files" class="r-files"> | |
| 304 | + | <h2 id="r-files">Files <span class="count">{ itoa(len(p.Files)) }</span><kbd aria-hidden="true">g f</kbd></h2> | |
| 305 | + | @FileList(p.Files) | |
| 320 | 306 | </section> | |
| 307 | + | @MRChecks(p.Checks) | |
| 321 | 308 | <section aria-labelledby="r-commits"> | |
| 322 | 309 | <h2 id="r-commits">Commits <span class="count">{ itoa(len(p.Commits)) }</span></h2> | |
| 323 | 310 | <ul class="pcommits"> | |
| @@ -344,74 +331,125 @@ templ mrRail(p MRPage) { | |||
| 344 | 331 | <p class="note">{ p.CommitsNote }</p> | |
| 345 | 332 | } | |
| 346 | 333 | </section> | |
| 347 | - | <section aria-labelledby="r-files"> | |
| 348 | - | <h2 id="r-files">Files <span class="count">{ itoa(len(p.Files)) }</span></h2> | |
| 349 | - | @FileList(p.Files) | |
| 350 | - | </section> | |
| 351 | - | } | |
| 352 | - | ||
| 353 | - | // mergeBox is the first block of the rail of an open request, for the owner: | |
| 354 | - | // the merge form (it carries the head that was read), the terminal lines, and | |
| 355 | - | // the other things one does with the request. | |
| 356 | - | templ mergeBox(p MRPage) { | |
| 357 | - | <section class="decide" aria-labelledby="r-merge" data-state="open"> | |
| 358 | - | <h2 id="r-merge">Merge</h2> | |
| 334 | + | <section aria-labelledby="r-settings"> | |
| 335 | + | <h2 id="r-settings">Settings</h2> | |
| 359 | 336 | <dl class="kv"> | |
| 360 | - | <dt>head</dt> | |
| 337 | + | <dt>modes</dt> | |
| 338 | + | <dd>{ strings.Join(p.Modes, ", ") }</dd> | |
| 339 | + | <dt>checks</dt> | |
| 361 | 340 | <dd> | |
| 362 | - | if p.Box.HeadGone { | |
| 363 | - | gone: the source branch was deleted | |
| 341 | + | if p.ChecksBlock { | |
| 342 | + | failing ones block the merge | |
| 364 | 343 | } else { | |
| 365 | - | { p.HeadShort } | |
| 344 | + | do not block the merge | |
| 366 | 345 | } | |
| 367 | 346 | </dd> | |
| 368 | - | <dt>into</dt> | |
| 369 | - | <dd>{ p.Target }</dd> | |
| 370 | 347 | </dl> | |
| 371 | - | if p.Box.ReadyAction != "" { | |
| 372 | - | <p class="d-draft">this is a draft: it cannot be merged until it is ready for review.</p> | |
| 373 | - | <form class="d-btns" method="post" action={ templ.SafeURL(p.Box.ReadyAction) }> | |
| 374 | - | <button type="submit" class="btn primary" title="remove the draft prefix from the title">Ready for review</button> | |
| 375 | - | </form> | |
| 376 | - | } else if !p.Box.HeadGone { | |
| 377 | - | <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }> | |
| 378 | - | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |
| 379 | - | <label class="sr" for="merge-mode">merge mode</label> | |
| 380 | - | <select id="merge-mode" name="mode"> | |
| 381 | - | for _, m := range p.Box.Modes { | |
| 382 | - | <option value={ m.Value }>{ m.Label }</option> | |
| 383 | - | } | |
| 384 | - | </select> | |
| 385 | - | if p.Box.CanDeleteBranch { | |
| 386 | - | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge</label> | |
| 387 | - | } | |
| 388 | - | <div class="d-btns"> | |
| 389 | - | <button type="submit" class="btn primary">Merge</button> | |
| 390 | - | </div> | |
| 391 | - | </form> | |
| 348 | + | </section> | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | // mrBar is the bar above the diff, which follows the review: the totals, the | |
| 352 | + | // layout of the diff, the state of the checks and, for the owner of an open | |
| 353 | + | // request, the merge menu and the other actions. Each menu is a <details>: it | |
| 354 | + | // works without scripts; with them, M and the keys of the menu drive it. | |
| 355 | + | templ mrBar(p MRPage) { | |
| 356 | + | <div class="diff-tools mr-bar" role="toolbar" aria-label="Diff view and merge"> | |
| 357 | + | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 358 | + | @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref) | |
| 359 | + | @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref) | |
| 360 | + | @collapseButton() | |
| 361 | + | <span class="grow"></span> | |
| 362 | + | @MRChecksSum(p.ChecksSum) | |
| 363 | + | if p.Box != nil { | |
| 364 | + | @mergeMenu(p) | |
| 365 | + | @moreMenu(p) | |
| 392 | 366 | } | |
| 393 | - | <div class="d-btns"> | |
| 367 | + | </div> | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | // mergeMenu is the merge of an open request, for the owner: the form (it | |
| 371 | + | // carries the head that was read), or "Ready for review" for a draft. The | |
| 372 | + | // mode and the branch deletion have keys (1-3, D) while it is open. | |
| 373 | + | templ mergeMenu(p MRPage) { | |
| 374 | + | <details class="menu mmenu" id="merge-menu" data-state="open"> | |
| 375 | + | <summary class="btn primary" aria-keyshortcuts="Shift+M"> | |
| 376 | + | if p.Box.ReadyAction != "" { | |
| 377 | + | Draft | |
| 378 | + | } else { | |
| 379 | + | Merge | |
| 380 | + | } | |
| 381 | + | <kbd aria-hidden="true">M</kbd> | |
| 382 | + | </summary> | |
| 383 | + | <div class="menu-pop" role="group" aria-label="Merge"> | |
| 384 | + | <dl class="kv"> | |
| 385 | + | <dt>head</dt> | |
| 386 | + | <dd> | |
| 387 | + | if p.Box.HeadGone { | |
| 388 | + | gone: the source branch was deleted | |
| 389 | + | } else { | |
| 390 | + | { p.HeadShort } | |
| 391 | + | } | |
| 392 | + | </dd> | |
| 393 | + | <dt>into</dt> | |
| 394 | + | <dd>{ p.Target }</dd> | |
| 395 | + | </dl> | |
| 396 | + | if p.Box.ReadyAction != "" { | |
| 397 | + | <p class="d-draft">this is a draft: it cannot be merged until it is ready for review.</p> | |
| 398 | + | <form class="d-btns" method="post" action={ templ.SafeURL(p.Box.ReadyAction) }> | |
| 399 | + | <button type="submit" class="btn primary" data-default title="remove the draft prefix from the title">Ready for review</button> | |
| 400 | + | </form> | |
| 401 | + | } else if !p.Box.HeadGone { | |
| 402 | + | <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }> | |
| 403 | + | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |
| 404 | + | <fieldset class="d-modes"> | |
| 405 | + | <legend class="sr">merge mode</legend> | |
| 406 | + | for i, m := range p.Box.Modes { | |
| 407 | + | <label> | |
| 408 | + | <input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/> | |
| 409 | + | { m.Label } | |
| 410 | + | if i < 9 { | |
| 411 | + | <kbd aria-hidden="true">{ itoa(i + 1) }</kbd> | |
| 412 | + | } | |
| 413 | + | </label> | |
| 414 | + | } | |
| 415 | + | </fieldset> | |
| 416 | + | if p.Box.CanDeleteBranch { | |
| 417 | + | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge <kbd aria-hidden="true">D</kbd></label> | |
| 418 | + | } | |
| 419 | + | <div class="d-btns"> | |
| 420 | + | <button type="submit" class="btn primary" data-default>Merge into { p.Target }</button> | |
| 421 | + | </div> | |
| 422 | + | </form> | |
| 423 | + | } | |
| 424 | + | </div> | |
| 425 | + | </details> | |
| 426 | + | } | |
| 427 | + | ||
| 428 | + | // moreMenu is what else the owner does with an open request: mark the head | |
| 429 | + | // read, close it, merge it from a terminal. | |
| 430 | + | templ moreMenu(p MRPage) { | |
| 431 | + | <details class="menu omenu"> | |
| 432 | + | <summary class="btn icon" aria-label="More actions" title="more actions">⋯</summary> | |
| 433 | + | <div class="menu-pop" role="group" aria-label="More actions"> | |
| 394 | 434 | if p.Box.CanMarkRead { | |
| 395 | 435 | <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }> | |
| 396 | 436 | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |
| 397 | - | <button type="submit" title="the next visit can show only what changed after this head" class="btn">Mark read</button> | |
| 437 | + | <button type="submit" title="the next visit can show only what changed after this head" class="btn" aria-keyshortcuts="Shift+R" data-read>Mark read <kbd aria-hidden="true">R</kbd></button> | |
| 398 | 438 | </form> | |
| 399 | 439 | } | |
| 400 | 440 | <form method="post" action={ templ.SafeURL(p.Box.CloseAction) }> | |
| 401 | 441 | <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button> | |
| 402 | 442 | </form> | |
| 443 | + | if !p.Box.HeadGone && p.Box.ReadyAction == "" { | |
| 444 | + | <details class="d-or"> | |
| 445 | + | <summary class="mute">or from a terminal</summary> | |
| 446 | + | @CloneBlock(p.Box.CLI) | |
| 447 | + | </details> | |
| 448 | + | } | |
| 403 | 449 | </div> | |
| 404 | - | if !p.Box.HeadGone && p.Box.ReadyAction == "" { | |
| 405 | - | <details class="d-or"> | |
| 406 | - | <summary class="mute">or from a terminal</summary> | |
| 407 | - | @CloneBlock(p.Box.CLI) | |
| 408 | - | </details> | |
| 409 | - | } | |
| 410 | - | </section> | |
| 450 | + | </details> | |
| 411 | 451 | } | |
| 412 | 452 | ||
| 413 | - | // Compare is a branch against the default branch: the commits it is ahead by, | |
| 414 | - | // its diff when the API can give one, and the form that opens a merge request. | |
| 415 | 453 | templ Compare(l Layout, p ComparePage) { | |
| 416 | 454 | @Shell(l, compareRail(p)) { | |
| 417 | 455 | <header class="show mr-h"> | |
A internal/adapters/web/mr_files.go +219 -0
| @@ -0,0 +1,219 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "net/http" | |
| 9 | + | "net/url" | |
| 10 | + | "strconv" | |
| 11 | + | "strings" | |
| 12 | + | ||
| 13 | + | "github.com/a-h/templ" | |
| 14 | + | ||
| 15 | + | "bnema.dev/ermite/internal/domain" | |
| 16 | + | "bnema.dev/ermite/internal/ports" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // The diff of a merge request is read a window of files at a time. The page | |
| 20 | + | // reads the files of one window (the first, or the one ?from= names) with | |
| 21 | + | // their hunks and lists the others as placeholders; a placeholder holds the | |
| 22 | + | // address of its window, which the browser fetches when it comes near the | |
| 23 | + | // screen and swaps in (mr.js). The window carries the base and the head the | |
| 24 | + | // page shows: when they moved, the use case refuses (ErrConflict) and the | |
| 25 | + | // placeholder offers to reload instead of mixing two diffs in one page. | |
| 26 | + | ||
| 27 | + | // MRFilesPage is a window of files of a merge request, for /files: the files | |
| 28 | + | // of the window, rendered like in the page, and nothing else. | |
| 29 | + | type MRFilesPage struct { | |
| 30 | + | Files []DiffFile | |
| 31 | + | DiffView | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // windowStart is the first file of the window that holds file i. | |
| 35 | + | func windowStart(i int) int { return i - i%mrWindow } | |
| 36 | + | ||
| 37 | + | // withFrom adds the window of a page to its address: the page of the files | |
| 38 | + | // [from, from+mrWindow) when from is not the first window. | |
| 39 | + | func withFrom(href string, from int) string { | |
| 40 | + | if from <= 0 { | |
| 41 | + | return href | |
| 42 | + | } | |
| 43 | + | sep := "?" | |
| 44 | + | if strings.Contains(href, "?") { | |
| 45 | + | sep = "&" | |
| 46 | + | } | |
| 47 | + | return href + sep + "from=" + itoa(from) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // formFrom reads the window a form was posted from (a hidden field "from"): | |
| 51 | + | // 0 when absent or not a window start, so that a redirect never carries | |
| 52 | + | // anything else. | |
| 53 | + | func formFrom(r *http.Request) int { | |
| 54 | + | s := r.PostFormValue("from") | |
| 55 | + | n, err := strconv.Atoi(s) | |
| 56 | + | if err != nil || n <= 0 || n%mrWindow != 0 || strconv.Itoa(n) != s { | |
| 57 | + | return 0 | |
| 58 | + | } | |
| 59 | + | return n | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // parseFrom reads ?from=: the start of a window, 0 when absent. Anything else | |
| 63 | + | // (not a number, negative, not a window start) is not a page. | |
| 64 | + | func parseFrom(r *http.Request) (int, error) { | |
| 65 | + | s := r.URL.Query().Get("from") | |
| 66 | + | if s == "" { | |
| 67 | + | return 0, nil | |
| 68 | + | } | |
| 69 | + | n, err := strconv.Atoi(s) | |
| 70 | + | if err != nil || n < 0 || n%mrWindow != 0 || strconv.Itoa(n) != s { | |
| 71 | + | return 0, errNotFound | |
| 72 | + | } | |
| 73 | + | return n, nil | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // lazyFiles marks the files outside [from, from+mrWindow) lazy: the page shows | |
| 77 | + | // a placeholder that loads their window. src is the URL of /files with the | |
| 78 | + | // base and the head of the diff; self the page, for the links of a page | |
| 79 | + | // without scripts. | |
| 80 | + | func lazyFiles(files []DiffFile, from int, src string) { | |
| 81 | + | for i := range files { | |
| 82 | + | f := &files[i] | |
| 83 | + | if i >= from && i < from+mrWindow { | |
| 84 | + | continue | |
| 85 | + | } | |
| 86 | + | f.Lazy = true | |
| 87 | + | f.From = windowStart(i) | |
| 88 | + | f.Src = src + "&from=" + itoa(f.From) | |
| 89 | + | f.Height = lazyHeight(f.Add + f.Del) | |
| 90 | + | f.Open = !f.Bin && f.Add+f.Del > 0 | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // lazyHeight is the size class of the placeholder of a file with n changed | |
| 95 | + | // lines: about the room its diff takes, so that the page does not jump much | |
| 96 | + | // when it comes in. | |
| 97 | + | func lazyHeight(n int) int { | |
| 98 | + | switch { | |
| 99 | + | case n <= 10: | |
| 100 | + | return 1 | |
| 101 | + | case n <= 50: | |
| 102 | + | return 2 | |
| 103 | + | case n <= 200: | |
| 104 | + | return 3 | |
| 105 | + | default: | |
| 106 | + | return 4 | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // filesSrc is the address of the windows of a diff. | |
| 111 | + | func filesSrc(repo string, n int64, since bool, base, head string) string { | |
| 112 | + | q := url.Values{} | |
| 113 | + | if since { | |
| 114 | + | q.Set("since", "1") | |
| 115 | + | } | |
| 116 | + | q.Set("base", base) | |
| 117 | + | q.Set("head", head) | |
| 118 | + | return mrHref(repo, n, "files") + "?" + q.Encode() | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // countNotes sets, on each file, the number of comments on its lines and the | |
| 122 | + | // ids of those comments. | |
| 123 | + | func countNotes(files []DiffFile, comments []domain.Comment) { | |
| 124 | + | at := make(map[string]int, len(files)) | |
| 125 | + | for i, f := range files { | |
| 126 | + | at[f.Key] = i | |
| 127 | + | } | |
| 128 | + | ids := make([][]string, len(files)) | |
| 129 | + | for _, c := range comments { | |
| 130 | + | i, ok := at[c.Path] | |
| 131 | + | if c.Path == "" || !ok { | |
| 132 | + | continue | |
| 133 | + | } | |
| 134 | + | files[i].Notes++ | |
| 135 | + | if !c.Resolved { | |
| 136 | + | files[i].OpenNotes++ | |
| 137 | + | } | |
| 138 | + | ids[i] = append(ids[i], "c"+strconv.FormatInt(c.ID, 10)) | |
| 139 | + | } | |
| 140 | + | for i := range files { | |
| 141 | + | files[i].Comments = strings.Join(ids[i], " ") | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // splitPath is the last element of a path and the directory before it. | |
| 146 | + | func splitPath(p string) (name, dir string) { | |
| 147 | + | i := strings.LastIndexByte(p, '/') | |
| 148 | + | if i < 0 { | |
| 149 | + | return visibleText(p), "" | |
| 150 | + | } | |
| 151 | + | return visibleText(p[i+1:]), visibleText(p[:i]) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | // mrFiles is GET /<repo>/merge-requests/<n>/files?from=&base=&head=[&since=1]: | |
| 155 | + | // the files of a window with their hunks and their comments, for a | |
| 156 | + | // placeholder of the page. A base or a head that is not the one of the diff | |
| 157 | + | // any more answers 409: the page says to reload. | |
| 158 | + | // | |
| 159 | + | //hexcheck:ignore suspicious-business-logic-in-adapter builds a window of the merge request page from the use case results; no rule is decided here | |
| 160 | + | func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 161 | + | n, err := mrNumber(r) | |
| 162 | + | if err != nil { | |
| 163 | + | return nil, 0, err | |
| 164 | + | } | |
| 165 | + | q := r.URL.Query() | |
| 166 | + | since := q.Get("since") == "1" | |
| 167 | + | if since && !SignedIn(ctx) { | |
| 168 | + | return nil, 0, errNotFound | |
| 169 | + | } | |
| 170 | + | from, err := parseFrom(r) | |
| 171 | + | if err != nil { | |
| 172 | + | return nil, 0, err | |
| 173 | + | } | |
| 174 | + | base, errB := domain.ParseObjectID(q.Get("base")) | |
| 175 | + | head, errH := domain.ParseObjectID(q.Get("head")) | |
| 176 | + | if errB != nil || errH != nil { | |
| 177 | + | return nil, 0, errNotFound | |
| 178 | + | } | |
| 179 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 180 | + | if err != nil { | |
| 181 | + | return nil, 0, err | |
| 182 | + | } | |
| 183 | + | res, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | |
| 184 | + | SinceLastRead: since, DiffOnly: true, Base: base, Head: head, | |
| 185 | + | Files: &ports.FileWindow{From: from, To: from + mrWindow}, | |
| 186 | + | }) | |
| 187 | + | if errors.Is(err, domain.ErrConflict) { | |
| 188 | + | return MRFilesMoved(), http.StatusConflict, nil | |
| 189 | + | } | |
| 190 | + | if err != nil { | |
| 191 | + | return nil, 0, err | |
| 192 | + | } | |
| 193 | + | if from >= len(res.Diff.Files) { | |
| 194 | + | return nil, 0, errNotFound | |
| 195 | + | } | |
| 196 | + | m := res.MergeRequest | |
| 197 | + | signed := SignedIn(ctx) | |
| 198 | + | canWrite := signed && m.State == domain.MRStateOpen && !since | |
| 199 | + | p := MRPage{Files: diffFiles(res.Diff.Files), LooseInFiles: true} | |
| 200 | + | s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil) | |
| 201 | + | window := p.Files[from:min(from+mrWindow, len(p.Files))] | |
| 202 | + | // The layouts follow the page: it sends ?view= (the layout shown) and | |
| 203 | + | // ?layouts=both when it holds both layouts and switches them itself, so | |
| 204 | + | // that every window of the page renders the same way. | |
| 205 | + | self := mrHref(d.name(), n) | |
| 206 | + | if since { | |
| 207 | + | self = mrHref(d.name(), n, "since") | |
| 208 | + | } | |
| 209 | + | lines, _ := diffLines(window) | |
| 210 | + | v := newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from)) | |
| 211 | + | switch r.URL.Query().Get("layouts") { | |
| 212 | + | case "both": | |
| 213 | + | v.Both = true | |
| 214 | + | case "one": | |
| 215 | + | v.Both = false | |
| 216 | + | } | |
| 217 | + | v.ShowUnified, v.ShowSplit = v.Both || v.View == "unified", v.Both || v.View == "split" | |
| 218 | + | return MRFiles(MRFilesPage{Files: window, DiffView: v}), http.StatusOK, nil | |
| 219 | + | } | |
M internal/adapters/web/mr_post.go +2 -0
| @@ -181,6 +181,7 @@ func (s *Server) mrComment(ctx context.Context, r *http.Request) (string, string | |||
| 181 | 181 | if err != nil { | |
| 182 | 182 | return "", back, err | |
| 183 | 183 | } | |
| 184 | + | back = withFrom(back, formFrom(r)) | |
| 184 | 185 | c, err := s.cfg.MergeRequests.AddComment(ctx, principal(ctx), name, n, params) | |
| 185 | 186 | if err != nil { | |
| 186 | 187 | return "", back, err | |
| @@ -209,6 +210,7 @@ func (s *Server) mrResolve(ctx context.Context, r *http.Request) (string, string | |||
| 209 | 210 | // An id this server never issued is 0: the use case answers not found, | |
| 210 | 211 | // after the repository and the scope were checked, as for any unknown | |
| 211 | 212 | // comment. | |
| 213 | + | back = withFrom(back, formFrom(r)) | |
| 212 | 214 | c, err := s.cfg.MergeRequests.ResolveComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")), resolved) | |
| 213 | 215 | if err != nil { | |
| 214 | 216 | return "", back, err | |
M internal/adapters/web/mr_templ.go +767 -574
| @@ -931,142 +931,95 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 931 | 931 | return templ_7745c5c3_Err | |
| 932 | 932 | } | |
| 933 | 933 | } | |
| 934 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " <div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 934 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " ") | |
| 935 | 935 | if templ_7745c5c3_Err != nil { | |
| 936 | 936 | return templ_7745c5c3_Err | |
| 937 | 937 | } | |
| 938 | - | var templ_7745c5c3_Var48 string | |
| 939 | - | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 940 | - | if templ_7745c5c3_Err != nil { | |
| 941 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 56} | |
| 942 | - | } | |
| 943 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 944 | - | if templ_7745c5c3_Err != nil { | |
| 945 | - | return templ_7745c5c3_Err | |
| 946 | - | } | |
| 947 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " · <span class=\"plus\">+") | |
| 948 | - | if templ_7745c5c3_Err != nil { | |
| 949 | - | return templ_7745c5c3_Err | |
| 950 | - | } | |
| 951 | - | var templ_7745c5c3_Var49 string | |
| 952 | - | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 953 | - | if templ_7745c5c3_Err != nil { | |
| 954 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 96} | |
| 955 | - | } | |
| 956 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 957 | - | if templ_7745c5c3_Err != nil { | |
| 958 | - | return templ_7745c5c3_Err | |
| 959 | - | } | |
| 960 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span> <span class=\"minus\">-") | |
| 961 | - | if templ_7745c5c3_Err != nil { | |
| 962 | - | return templ_7745c5c3_Err | |
| 963 | - | } | |
| 964 | - | var templ_7745c5c3_Var50 string | |
| 965 | - | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 966 | - | if templ_7745c5c3_Err != nil { | |
| 967 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 141} | |
| 968 | - | } | |
| 969 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | |
| 970 | - | if templ_7745c5c3_Err != nil { | |
| 971 | - | return templ_7745c5c3_Err | |
| 972 | - | } | |
| 973 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</span></span> <span class=\"grow\"></span>") | |
| 938 | + | templ_7745c5c3_Err = mrBar(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 974 | 939 | if templ_7745c5c3_Err != nil { | |
| 975 | 940 | return templ_7745c5c3_Err | |
| 976 | 941 | } | |
| 977 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 978 | - | if templ_7745c5c3_Err != nil { | |
| 979 | - | return templ_7745c5c3_Err | |
| 980 | - | } | |
| 981 | - | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 982 | - | if templ_7745c5c3_Err != nil { | |
| 983 | - | return templ_7745c5c3_Err | |
| 984 | - | } | |
| 985 | - | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |
| 986 | - | if templ_7745c5c3_Err != nil { | |
| 987 | - | return templ_7745c5c3_Err | |
| 988 | - | } | |
| 989 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</div>") | |
| 942 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " ") | |
| 990 | 943 | if templ_7745c5c3_Err != nil { | |
| 991 | 944 | return templ_7745c5c3_Err | |
| 992 | 945 | } | |
| 993 | 946 | if p.Invisible { | |
| 994 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 947 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 995 | 948 | if templ_7745c5c3_Err != nil { | |
| 996 | 949 | return templ_7745c5c3_Err | |
| 997 | 950 | } | |
| 998 | 951 | } | |
| 999 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " ") | |
| 952 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, " ") | |
| 1000 | 953 | if templ_7745c5c3_Err != nil { | |
| 1001 | 954 | return templ_7745c5c3_Err | |
| 1002 | 955 | } | |
| 1003 | 956 | if p.Note != "" { | |
| 1004 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<p class=\"notice\">") | |
| 957 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<p class=\"notice\">") | |
| 1005 | 958 | if templ_7745c5c3_Err != nil { | |
| 1006 | 959 | return templ_7745c5c3_Err | |
| 1007 | 960 | } | |
| 1008 | - | var templ_7745c5c3_Var51 string | |
| 1009 | - | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 961 | + | var templ_7745c5c3_Var48 string | |
| 962 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 1010 | 963 | if templ_7745c5c3_Err != nil { | |
| 1011 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 179, Col: 29} | |
| 964 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 173, Col: 29} | |
| 1012 | 965 | } | |
| 1013 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 966 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 1014 | 967 | if templ_7745c5c3_Err != nil { | |
| 1015 | 968 | return templ_7745c5c3_Err | |
| 1016 | 969 | } | |
| 1017 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</p>") | |
| 970 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</p>") | |
| 1018 | 971 | if templ_7745c5c3_Err != nil { | |
| 1019 | 972 | return templ_7745c5c3_Err | |
| 1020 | 973 | } | |
| 1021 | 974 | } | |
| 1022 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " <div id=\"diffs\"") | |
| 975 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " <div id=\"diffs\" data-mr") | |
| 1023 | 976 | if templ_7745c5c3_Err != nil { | |
| 1024 | 977 | return templ_7745c5c3_Err | |
| 1025 | 978 | } | |
| 1026 | 979 | if p.Both { | |
| 1027 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, " data-diff=\"") | |
| 980 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " data-diff=\"") | |
| 1028 | 981 | if templ_7745c5c3_Err != nil { | |
| 1029 | 982 | return templ_7745c5c3_Err | |
| 1030 | 983 | } | |
| 1031 | - | var templ_7745c5c3_Var52 string | |
| 1032 | - | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 984 | + | var templ_7745c5c3_Var49 string | |
| 985 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1033 | 986 | if templ_7745c5c3_Err != nil { | |
| 1034 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 184, Col: 22} | |
| 987 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 179, Col: 22} | |
| 1035 | 988 | } | |
| 1036 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52) | |
| 989 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49) | |
| 1037 | 990 | if templ_7745c5c3_Err != nil { | |
| 1038 | 991 | return templ_7745c5c3_Err | |
| 1039 | 992 | } | |
| 1040 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\"") | |
| 993 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\"") | |
| 1041 | 994 | if templ_7745c5c3_Err != nil { | |
| 1042 | 995 | return templ_7745c5c3_Err | |
| 1043 | 996 | } | |
| 1044 | 997 | } else { | |
| 1045 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, " data-view=\"") | |
| 998 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " data-view=\"") | |
| 1046 | 999 | if templ_7745c5c3_Err != nil { | |
| 1047 | 1000 | return templ_7745c5c3_Err | |
| 1048 | 1001 | } | |
| 1049 | - | var templ_7745c5c3_Var53 string | |
| 1050 | - | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1002 | + | var templ_7745c5c3_Var50 string | |
| 1003 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1051 | 1004 | if templ_7745c5c3_Err != nil { | |
| 1052 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 186, Col: 22} | |
| 1005 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 181, Col: 22} | |
| 1053 | 1006 | } | |
| 1054 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53) | |
| 1007 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50) | |
| 1055 | 1008 | if templ_7745c5c3_Err != nil { | |
| 1056 | 1009 | return templ_7745c5c3_Err | |
| 1057 | 1010 | } | |
| 1058 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\"") | |
| 1011 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\"") | |
| 1059 | 1012 | if templ_7745c5c3_Err != nil { | |
| 1060 | 1013 | return templ_7745c5c3_Err | |
| 1061 | 1014 | } | |
| 1062 | 1015 | } | |
| 1063 | 1016 | if p.Both && !p.Pinned { | |
| 1064 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 1017 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 1065 | 1018 | if templ_7745c5c3_Err != nil { | |
| 1066 | 1019 | return templ_7745c5c3_Err | |
| 1067 | 1020 | } | |
| 1068 | 1021 | } | |
| 1069 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, ">") | |
| 1022 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, ">") | |
| 1070 | 1023 | if templ_7745c5c3_Err != nil { | |
| 1071 | 1024 | return templ_7745c5c3_Err | |
| 1072 | 1025 | } | |
| @@ -1077,38 +1030,38 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1077 | 1030 | } | |
| 1078 | 1031 | } | |
| 1079 | 1032 | if len(p.Files) == 0 { | |
| 1080 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<p class=\"note\">no changed files.</p>") | |
| 1033 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<p class=\"note\">no changed files.</p>") | |
| 1081 | 1034 | if templ_7745c5c3_Err != nil { | |
| 1082 | 1035 | return templ_7745c5c3_Err | |
| 1083 | 1036 | } | |
| 1084 | 1037 | } | |
| 1085 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">") | |
| 1038 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">") | |
| 1086 | 1039 | if templ_7745c5c3_Err != nil { | |
| 1087 | 1040 | return templ_7745c5c3_Err | |
| 1088 | 1041 | } | |
| 1089 | - | var templ_7745c5c3_Var54 string | |
| 1090 | - | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | |
| 1042 | + | var templ_7745c5c3_Var51 string | |
| 1043 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | |
| 1091 | 1044 | if templ_7745c5c3_Err != nil { | |
| 1092 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 199, Col: 97} | |
| 1045 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 194, Col: 97} | |
| 1093 | 1046 | } | |
| 1094 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1047 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 1095 | 1048 | if templ_7745c5c3_Err != nil { | |
| 1096 | 1049 | return templ_7745c5c3_Err | |
| 1097 | 1050 | } | |
| 1098 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " open / ") | |
| 1051 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " open / ") | |
| 1099 | 1052 | if templ_7745c5c3_Err != nil { | |
| 1100 | 1053 | return templ_7745c5c3_Err | |
| 1101 | 1054 | } | |
| 1102 | - | var templ_7745c5c3_Var55 string | |
| 1103 | - | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | |
| 1055 | + | var templ_7745c5c3_Var52 string | |
| 1056 | + | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | |
| 1104 | 1057 | if templ_7745c5c3_Err != nil { | |
| 1105 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 199, Col: 126} | |
| 1058 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 194, Col: 126} | |
| 1106 | 1059 | } | |
| 1107 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1060 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) | |
| 1108 | 1061 | if templ_7745c5c3_Err != nil { | |
| 1109 | 1062 | return templ_7745c5c3_Err | |
| 1110 | 1063 | } | |
| 1111 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "</span></h2>") | |
| 1064 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</span></h2>") | |
| 1112 | 1065 | if templ_7745c5c3_Err != nil { | |
| 1113 | 1066 | return templ_7745c5c3_Err | |
| 1114 | 1067 | } | |
| @@ -1124,17 +1077,17 @@ func MergeRequest(l Layout, p MRPage) templ.Component { | |||
| 1124 | 1077 | return templ_7745c5c3_Err | |
| 1125 | 1078 | } | |
| 1126 | 1079 | } | |
| 1127 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, " ") | |
| 1080 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " ") | |
| 1128 | 1081 | if templ_7745c5c3_Err != nil { | |
| 1129 | 1082 | return templ_7745c5c3_Err | |
| 1130 | 1083 | } | |
| 1131 | 1084 | if p.NComments == 0 { | |
| 1132 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"dim\">no comments yet.</p>") | |
| 1085 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"dim\">no comments yet.</p>") | |
| 1133 | 1086 | if templ_7745c5c3_Err != nil { | |
| 1134 | 1087 | return templ_7745c5c3_Err | |
| 1135 | 1088 | } | |
| 1136 | 1089 | } | |
| 1137 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " ") | |
| 1090 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " ") | |
| 1138 | 1091 | if templ_7745c5c3_Err != nil { | |
| 1139 | 1092 | return templ_7745c5c3_Err | |
| 1140 | 1093 | } | |
| @@ -1170,49 +1123,49 @@ func branchLink(name, href string) templ.Component { | |||
| 1170 | 1123 | }() | |
| 1171 | 1124 | } | |
| 1172 | 1125 | ctx = templ.InitializeContext(ctx) | |
| 1173 | - | templ_7745c5c3_Var56 := templ.GetChildren(ctx) | |
| 1174 | - | if templ_7745c5c3_Var56 == nil { | |
| 1175 | - | templ_7745c5c3_Var56 = templ.NopComponent | |
| 1126 | + | templ_7745c5c3_Var53 := templ.GetChildren(ctx) | |
| 1127 | + | if templ_7745c5c3_Var53 == nil { | |
| 1128 | + | templ_7745c5c3_Var53 = templ.NopComponent | |
| 1176 | 1129 | } | |
| 1177 | 1130 | ctx = templ.ClearChildren(ctx) | |
| 1178 | 1131 | if href != "" { | |
| 1179 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<a href=\"") | |
| 1132 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "<a href=\"") | |
| 1180 | 1133 | if templ_7745c5c3_Err != nil { | |
| 1181 | 1134 | return templ_7745c5c3_Err | |
| 1182 | 1135 | } | |
| 1183 | - | var templ_7745c5c3_Var57 templ.SafeURL | |
| 1184 | - | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 1136 | + | var templ_7745c5c3_Var54 templ.SafeURL | |
| 1137 | + | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 1185 | 1138 | if templ_7745c5c3_Err != nil { | |
| 1186 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 31} | |
| 1139 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 212, Col: 31} | |
| 1187 | 1140 | } | |
| 1188 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1141 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1189 | 1142 | if templ_7745c5c3_Err != nil { | |
| 1190 | 1143 | return templ_7745c5c3_Err | |
| 1191 | 1144 | } | |
| 1192 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\">") | |
| 1145 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "\">") | |
| 1193 | 1146 | if templ_7745c5c3_Err != nil { | |
| 1194 | 1147 | return templ_7745c5c3_Err | |
| 1195 | 1148 | } | |
| 1196 | - | var templ_7745c5c3_Var58 string | |
| 1197 | - | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1149 | + | var templ_7745c5c3_Var55 string | |
| 1150 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1198 | 1151 | if templ_7745c5c3_Err != nil { | |
| 1199 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 40} | |
| 1152 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 212, Col: 40} | |
| 1200 | 1153 | } | |
| 1201 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1154 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1202 | 1155 | if templ_7745c5c3_Err != nil { | |
| 1203 | 1156 | return templ_7745c5c3_Err | |
| 1204 | 1157 | } | |
| 1205 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</a>") | |
| 1158 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a>") | |
| 1206 | 1159 | if templ_7745c5c3_Err != nil { | |
| 1207 | 1160 | return templ_7745c5c3_Err | |
| 1208 | 1161 | } | |
| 1209 | 1162 | } else { | |
| 1210 | - | var templ_7745c5c3_Var59 string | |
| 1211 | - | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1163 | + | var templ_7745c5c3_Var56 string | |
| 1164 | + | templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1212 | 1165 | if templ_7745c5c3_Err != nil { | |
| 1213 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 219, Col: 8} | |
| 1166 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 214, Col: 8} | |
| 1214 | 1167 | } | |
| 1215 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1168 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) | |
| 1216 | 1169 | if templ_7745c5c3_Err != nil { | |
| 1217 | 1170 | return templ_7745c5c3_Err | |
| 1218 | 1171 | } | |
| @@ -1240,9 +1193,9 @@ func lineNotes(notes []CommentView, form *CommentForm, suffix string) templ.Comp | |||
| 1240 | 1193 | }() | |
| 1241 | 1194 | } | |
| 1242 | 1195 | ctx = templ.InitializeContext(ctx) | |
| 1243 | - | templ_7745c5c3_Var60 := templ.GetChildren(ctx) | |
| 1244 | - | if templ_7745c5c3_Var60 == nil { | |
| 1245 | - | templ_7745c5c3_Var60 = templ.NopComponent | |
| 1196 | + | templ_7745c5c3_Var57 := templ.GetChildren(ctx) | |
| 1197 | + | if templ_7745c5c3_Var57 == nil { | |
| 1198 | + | templ_7745c5c3_Var57 = templ.NopComponent | |
| 1246 | 1199 | } | |
| 1247 | 1200 | ctx = templ.ClearChildren(ctx) | |
| 1248 | 1201 | for _, c := range notes { | |
| @@ -1280,167 +1233,167 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1280 | 1233 | }() | |
| 1281 | 1234 | } | |
| 1282 | 1235 | ctx = templ.InitializeContext(ctx) | |
| 1283 | - | templ_7745c5c3_Var61 := templ.GetChildren(ctx) | |
| 1284 | - | if templ_7745c5c3_Var61 == nil { | |
| 1285 | - | templ_7745c5c3_Var61 = templ.NopComponent | |
| 1236 | + | templ_7745c5c3_Var58 := templ.GetChildren(ctx) | |
| 1237 | + | if templ_7745c5c3_Var58 == nil { | |
| 1238 | + | templ_7745c5c3_Var58 = templ.NopComponent | |
| 1286 | 1239 | } | |
| 1287 | 1240 | ctx = templ.ClearChildren(ctx) | |
| 1288 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<div class=\"cmt\" id=\"") | |
| 1241 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<div class=\"cmt\" id=\"") | |
| 1289 | 1242 | if templ_7745c5c3_Err != nil { | |
| 1290 | 1243 | return templ_7745c5c3_Err | |
| 1291 | 1244 | } | |
| 1292 | - | var templ_7745c5c3_Var62 string | |
| 1293 | - | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | |
| 1245 | + | var templ_7745c5c3_Var59 string | |
| 1246 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | |
| 1294 | 1247 | if templ_7745c5c3_Err != nil { | |
| 1295 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 42} | |
| 1248 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 42} | |
| 1296 | 1249 | } | |
| 1297 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62) | |
| 1250 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59) | |
| 1298 | 1251 | if templ_7745c5c3_Err != nil { | |
| 1299 | 1252 | return templ_7745c5c3_Err | |
| 1300 | 1253 | } | |
| 1301 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\" data-resolved=\"") | |
| 1254 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\" data-resolved=\"") | |
| 1302 | 1255 | if templ_7745c5c3_Err != nil { | |
| 1303 | 1256 | return templ_7745c5c3_Err | |
| 1304 | 1257 | } | |
| 1305 | - | var templ_7745c5c3_Var63 string | |
| 1306 | - | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | |
| 1258 | + | var templ_7745c5c3_Var60 string | |
| 1259 | + | templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | |
| 1307 | 1260 | if templ_7745c5c3_Err != nil { | |
| 1308 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 81} | |
| 1261 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 81} | |
| 1309 | 1262 | } | |
| 1310 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | |
| 1263 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60) | |
| 1311 | 1264 | if templ_7745c5c3_Err != nil { | |
| 1312 | 1265 | return templ_7745c5c3_Err | |
| 1313 | 1266 | } | |
| 1314 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\"><p class=\"c-head\"><span class=\"c-who\">") | |
| 1267 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"><p class=\"c-head\"><span class=\"c-who\">") | |
| 1315 | 1268 | if templ_7745c5c3_Err != nil { | |
| 1316 | 1269 | return templ_7745c5c3_Err | |
| 1317 | 1270 | } | |
| 1318 | - | var templ_7745c5c3_Var64 string | |
| 1319 | - | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1271 | + | var templ_7745c5c3_Var61 string | |
| 1272 | + | templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1320 | 1273 | if templ_7745c5c3_Err != nil { | |
| 1321 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 33} | |
| 1274 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 33} | |
| 1322 | 1275 | } | |
| 1323 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1276 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) | |
| 1324 | 1277 | if templ_7745c5c3_Err != nil { | |
| 1325 | 1278 | return templ_7745c5c3_Err | |
| 1326 | 1279 | } | |
| 1327 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</span> ") | |
| 1280 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ") | |
| 1328 | 1281 | if templ_7745c5c3_Err != nil { | |
| 1329 | 1282 | return templ_7745c5c3_Err | |
| 1330 | 1283 | } | |
| 1331 | 1284 | if c.WhenAt != "" { | |
| 1332 | - | var templ_7745c5c3_Var65 string | |
| 1333 | - | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1285 | + | var templ_7745c5c3_Var62 string | |
| 1286 | + | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1334 | 1287 | if templ_7745c5c3_Err != nil { | |
| 1335 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 9} | |
| 1288 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 9} | |
| 1336 | 1289 | } | |
| 1337 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1290 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) | |
| 1338 | 1291 | if templ_7745c5c3_Err != nil { | |
| 1339 | 1292 | return templ_7745c5c3_Err | |
| 1340 | 1293 | } | |
| 1341 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "<time class=\"mute\" datetime=\"") | |
| 1294 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<time class=\"mute\" datetime=\"") | |
| 1342 | 1295 | if templ_7745c5c3_Err != nil { | |
| 1343 | 1296 | return templ_7745c5c3_Err | |
| 1344 | 1297 | } | |
| 1345 | - | var templ_7745c5c3_Var66 string | |
| 1346 | - | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1298 | + | var templ_7745c5c3_Var63 string | |
| 1299 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1347 | 1300 | if templ_7745c5c3_Err != nil { | |
| 1348 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 49} | |
| 1301 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 49} | |
| 1349 | 1302 | } | |
| 1350 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66) | |
| 1303 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | |
| 1351 | 1304 | if templ_7745c5c3_Err != nil { | |
| 1352 | 1305 | return templ_7745c5c3_Err | |
| 1353 | 1306 | } | |
| 1354 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\" title=\"") | |
| 1307 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\" title=\"") | |
| 1355 | 1308 | if templ_7745c5c3_Err != nil { | |
| 1356 | 1309 | return templ_7745c5c3_Err | |
| 1357 | 1310 | } | |
| 1358 | - | var templ_7745c5c3_Var67 string | |
| 1359 | - | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | |
| 1311 | + | var templ_7745c5c3_Var64 string | |
| 1312 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | |
| 1360 | 1313 | if templ_7745c5c3_Err != nil { | |
| 1361 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 65} | |
| 1314 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 65} | |
| 1362 | 1315 | } | |
| 1363 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67) | |
| 1316 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) | |
| 1364 | 1317 | if templ_7745c5c3_Err != nil { | |
| 1365 | 1318 | return templ_7745c5c3_Err | |
| 1366 | 1319 | } | |
| 1367 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\">") | |
| 1320 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\">") | |
| 1368 | 1321 | if templ_7745c5c3_Err != nil { | |
| 1369 | 1322 | return templ_7745c5c3_Err | |
| 1370 | 1323 | } | |
| 1371 | - | var templ_7745c5c3_Var68 string | |
| 1372 | - | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1324 | + | var templ_7745c5c3_Var65 string | |
| 1325 | + | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1373 | 1326 | if templ_7745c5c3_Err != nil { | |
| 1374 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 76} | |
| 1327 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 76} | |
| 1375 | 1328 | } | |
| 1376 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1329 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1377 | 1330 | if templ_7745c5c3_Err != nil { | |
| 1378 | 1331 | return templ_7745c5c3_Err | |
| 1379 | 1332 | } | |
| 1380 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "</time> ") | |
| 1333 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</time> ") | |
| 1381 | 1334 | if templ_7745c5c3_Err != nil { | |
| 1382 | 1335 | return templ_7745c5c3_Err | |
| 1383 | 1336 | } | |
| 1384 | 1337 | } | |
| 1385 | 1338 | if c.Resolved { | |
| 1386 | - | var templ_7745c5c3_Var69 string | |
| 1387 | - | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1339 | + | var templ_7745c5c3_Var66 string | |
| 1340 | + | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1388 | 1341 | if templ_7745c5c3_Err != nil { | |
| 1389 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 246, Col: 9} | |
| 1342 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 9} | |
| 1390 | 1343 | } | |
| 1391 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1344 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) | |
| 1392 | 1345 | if templ_7745c5c3_Err != nil { | |
| 1393 | 1346 | return templ_7745c5c3_Err | |
| 1394 | 1347 | } | |
| 1395 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "<span class=\"c-state\">resolved</span> ") | |
| 1348 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<span class=\"c-state\">resolved</span> ") | |
| 1396 | 1349 | if templ_7745c5c3_Err != nil { | |
| 1397 | 1350 | return templ_7745c5c3_Err | |
| 1398 | 1351 | } | |
| 1399 | 1352 | } | |
| 1400 | 1353 | if c.Outdated { | |
| 1401 | - | var templ_7745c5c3_Var70 string | |
| 1402 | - | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1354 | + | var templ_7745c5c3_Var67 string | |
| 1355 | + | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1403 | 1356 | if templ_7745c5c3_Err != nil { | |
| 1404 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9} | |
| 1357 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9} | |
| 1405 | 1358 | } | |
| 1406 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1359 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) | |
| 1407 | 1360 | if templ_7745c5c3_Err != nil { | |
| 1408 | 1361 | return templ_7745c5c3_Err | |
| 1409 | 1362 | } | |
| 1410 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ") | |
| 1363 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ") | |
| 1411 | 1364 | if templ_7745c5c3_Err != nil { | |
| 1412 | 1365 | return templ_7745c5c3_Err | |
| 1413 | 1366 | } | |
| 1414 | 1367 | } | |
| 1415 | 1368 | if c.Where != "" { | |
| 1416 | - | var templ_7745c5c3_Var71 string | |
| 1417 | - | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1369 | + | var templ_7745c5c3_Var68 string | |
| 1370 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1418 | 1371 | if templ_7745c5c3_Err != nil { | |
| 1419 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 9} | |
| 1372 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9} | |
| 1420 | 1373 | } | |
| 1421 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1374 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1422 | 1375 | if templ_7745c5c3_Err != nil { | |
| 1423 | 1376 | return templ_7745c5c3_Err | |
| 1424 | 1377 | } | |
| 1425 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span class=\"c-where mute\">") | |
| 1378 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-where mute\">") | |
| 1426 | 1379 | if templ_7745c5c3_Err != nil { | |
| 1427 | 1380 | return templ_7745c5c3_Err | |
| 1428 | 1381 | } | |
| 1429 | - | var templ_7745c5c3_Var72 string | |
| 1430 | - | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | |
| 1382 | + | var templ_7745c5c3_Var69 string | |
| 1383 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | |
| 1431 | 1384 | if templ_7745c5c3_Err != nil { | |
| 1432 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 47} | |
| 1385 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 47} | |
| 1433 | 1386 | } | |
| 1434 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1387 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1435 | 1388 | if templ_7745c5c3_Err != nil { | |
| 1436 | 1389 | return templ_7745c5c3_Err | |
| 1437 | 1390 | } | |
| 1438 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</span>") | |
| 1391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</span>") | |
| 1439 | 1392 | if templ_7745c5c3_Err != nil { | |
| 1440 | 1393 | return templ_7745c5c3_Err | |
| 1441 | 1394 | } | |
| 1442 | 1395 | } | |
| 1443 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</p><p class=\"c-text\">") | |
| 1396 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</p><p class=\"c-text\">") | |
| 1444 | 1397 | if templ_7745c5c3_Err != nil { | |
| 1445 | 1398 | return templ_7745c5c3_Err | |
| 1446 | 1399 | } | |
| @@ -1448,47 +1401,70 @@ func commentView(c CommentView, suffix string) templ.Component { | |||
| 1448 | 1401 | if templ_7745c5c3_Err != nil { | |
| 1449 | 1402 | return templ_7745c5c3_Err | |
| 1450 | 1403 | } | |
| 1451 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</p>") | |
| 1404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</p>") | |
| 1452 | 1405 | if templ_7745c5c3_Err != nil { | |
| 1453 | 1406 | return templ_7745c5c3_Err | |
| 1454 | 1407 | } | |
| 1455 | 1408 | if c.Resolve != nil { | |
| 1456 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<form class=\"c-act\" method=\"post\" action=\"") | |
| 1409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "<form class=\"c-act\" method=\"post\" action=\"") | |
| 1457 | 1410 | if templ_7745c5c3_Err != nil { | |
| 1458 | 1411 | return templ_7745c5c3_Err | |
| 1459 | 1412 | } | |
| 1460 | - | var templ_7745c5c3_Var73 templ.SafeURL | |
| 1461 | - | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | |
| 1413 | + | var templ_7745c5c3_Var70 templ.SafeURL | |
| 1414 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | |
| 1462 | 1415 | if templ_7745c5c3_Err != nil { | |
| 1463 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 257, Col: 77} | |
| 1416 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 77} | |
| 1464 | 1417 | } | |
| 1465 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | |
| 1418 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1466 | 1419 | if templ_7745c5c3_Err != nil { | |
| 1467 | 1420 | return templ_7745c5c3_Err | |
| 1468 | 1421 | } | |
| 1469 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\"><input type=\"hidden\" name=\"resolved\" value=\"") | |
| 1422 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\"><input type=\"hidden\" name=\"resolved\" value=\"") | |
| 1470 | 1423 | if templ_7745c5c3_Err != nil { | |
| 1471 | 1424 | return templ_7745c5c3_Err | |
| 1472 | 1425 | } | |
| 1473 | - | var templ_7745c5c3_Var74 string | |
| 1474 | - | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | |
| 1426 | + | var templ_7745c5c3_Var71 string | |
| 1427 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | |
| 1475 | 1428 | if templ_7745c5c3_Err != nil { | |
| 1476 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 74} | |
| 1429 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 74} | |
| 1477 | 1430 | } | |
| 1478 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1431 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71) | |
| 1479 | 1432 | if templ_7745c5c3_Err != nil { | |
| 1480 | 1433 | return templ_7745c5c3_Err | |
| 1481 | 1434 | } | |
| 1482 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\"> <button type=\"submit\" class=\"btn\">") | |
| 1435 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\"> ") | |
| 1483 | 1436 | if templ_7745c5c3_Err != nil { | |
| 1484 | 1437 | return templ_7745c5c3_Err | |
| 1485 | 1438 | } | |
| 1486 | - | var templ_7745c5c3_Var75 string | |
| 1487 | - | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | |
| 1439 | + | if c.Resolve.From > 0 { | |
| 1440 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<input type=\"hidden\" name=\"from\" value=\"") | |
| 1441 | + | if templ_7745c5c3_Err != nil { | |
| 1442 | + | return templ_7745c5c3_Err | |
| 1443 | + | } | |
| 1444 | + | var templ_7745c5c3_Var72 string | |
| 1445 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From)) | |
| 1446 | + | if templ_7745c5c3_Err != nil { | |
| 1447 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 255, Col: 66} | |
| 1448 | + | } | |
| 1449 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72) | |
| 1450 | + | if templ_7745c5c3_Err != nil { | |
| 1451 | + | return templ_7745c5c3_Err | |
| 1452 | + | } | |
| 1453 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\"> ") | |
| 1454 | + | if templ_7745c5c3_Err != nil { | |
| 1455 | + | return templ_7745c5c3_Err | |
| 1456 | + | } | |
| 1457 | + | } | |
| 1458 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<button type=\"submit\" class=\"btn\">") | |
| 1459 | + | if templ_7745c5c3_Err != nil { | |
| 1460 | + | return templ_7745c5c3_Err | |
| 1461 | + | } | |
| 1462 | + | var templ_7745c5c3_Var73 string | |
| 1463 | + | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | |
| 1488 | 1464 | if templ_7745c5c3_Err != nil { | |
| 1489 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 259, Col: 55} | |
| 1465 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 257, Col: 55} | |
| 1490 | 1466 | } | |
| 1491 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1467 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | |
| 1492 | 1468 | if templ_7745c5c3_Err != nil { | |
| 1493 | 1469 | return templ_7745c5c3_Err | |
| 1494 | 1470 | } | |
| @@ -1529,21 +1505,21 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1529 | 1505 | }() | |
| 1530 | 1506 | } | |
| 1531 | 1507 | ctx = templ.InitializeContext(ctx) | |
| 1532 | - | templ_7745c5c3_Var76 := templ.GetChildren(ctx) | |
| 1533 | - | if templ_7745c5c3_Var76 == nil { | |
| 1534 | - | templ_7745c5c3_Var76 = templ.NopComponent | |
| 1508 | + | templ_7745c5c3_Var74 := templ.GetChildren(ctx) | |
| 1509 | + | if templ_7745c5c3_Var74 == nil { | |
| 1510 | + | templ_7745c5c3_Var74 = templ.NopComponent | |
| 1535 | 1511 | } | |
| 1536 | 1512 | ctx = templ.ClearChildren(ctx) | |
| 1537 | 1513 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<form class=\"cform\" method=\"post\" action=\"") | |
| 1538 | 1514 | if templ_7745c5c3_Err != nil { | |
| 1539 | 1515 | return templ_7745c5c3_Err | |
| 1540 | 1516 | } | |
| 1541 | - | var templ_7745c5c3_Var77 templ.SafeURL | |
| 1542 | - | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | |
| 1517 | + | var templ_7745c5c3_Var75 templ.SafeURL | |
| 1518 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | |
| 1543 | 1519 | if templ_7745c5c3_Err != nil { | |
| 1544 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 67} | |
| 1520 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 272, Col: 67} | |
| 1545 | 1521 | } | |
| 1546 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) | |
| 1522 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1547 | 1523 | if templ_7745c5c3_Err != nil { | |
| 1548 | 1524 | return templ_7745c5c3_Err | |
| 1549 | 1525 | } | |
| @@ -1551,12 +1527,12 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1551 | 1527 | if templ_7745c5c3_Err != nil { | |
| 1552 | 1528 | return templ_7745c5c3_Err | |
| 1553 | 1529 | } | |
| 1554 | - | var templ_7745c5c3_Var78 string | |
| 1555 | - | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1530 | + | var templ_7745c5c3_Var76 string | |
| 1531 | + | templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1556 | 1532 | if templ_7745c5c3_Err != nil { | |
| 1557 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 28} | |
| 1533 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 28} | |
| 1558 | 1534 | } | |
| 1559 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) | |
| 1535 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76) | |
| 1560 | 1536 | if templ_7745c5c3_Err != nil { | |
| 1561 | 1537 | return templ_7745c5c3_Err | |
| 1562 | 1538 | } | |
| @@ -1564,12 +1540,12 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1564 | 1540 | if templ_7745c5c3_Err != nil { | |
| 1565 | 1541 | return templ_7745c5c3_Err | |
| 1566 | 1542 | } | |
| 1567 | - | var templ_7745c5c3_Var79 string | |
| 1568 | - | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | |
| 1543 | + | var templ_7745c5c3_Var77 string | |
| 1544 | + | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | |
| 1569 | 1545 | if templ_7745c5c3_Err != nil { | |
| 1570 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 40} | |
| 1546 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 40} | |
| 1571 | 1547 | } | |
| 1572 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) | |
| 1548 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) | |
| 1573 | 1549 | if templ_7745c5c3_Err != nil { | |
| 1574 | 1550 | return templ_7745c5c3_Err | |
| 1575 | 1551 | } | |
| @@ -1577,12 +1553,12 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1577 | 1553 | if templ_7745c5c3_Err != nil { | |
| 1578 | 1554 | return templ_7745c5c3_Err | |
| 1579 | 1555 | } | |
| 1580 | - | var templ_7745c5c3_Var80 string | |
| 1581 | - | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1556 | + | var templ_7745c5c3_Var78 string | |
| 1557 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1582 | 1558 | if templ_7745c5c3_Err != nil { | |
| 1583 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 277, Col: 21} | |
| 1559 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 21} | |
| 1584 | 1560 | } | |
| 1585 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1561 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) | |
| 1586 | 1562 | if templ_7745c5c3_Err != nil { | |
| 1587 | 1563 | return templ_7745c5c3_Err | |
| 1588 | 1564 | } | |
| @@ -1605,12 +1581,12 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1605 | 1581 | if templ_7745c5c3_Err != nil { | |
| 1606 | 1582 | return templ_7745c5c3_Err | |
| 1607 | 1583 | } | |
| 1608 | - | var templ_7745c5c3_Var81 string | |
| 1609 | - | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 1584 | + | var templ_7745c5c3_Var79 string | |
| 1585 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 1610 | 1586 | if templ_7745c5c3_Err != nil { | |
| 1611 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 50} | |
| 1587 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 286, Col: 50} | |
| 1612 | 1588 | } | |
| 1613 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81) | |
| 1589 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79) | |
| 1614 | 1590 | if templ_7745c5c3_Err != nil { | |
| 1615 | 1591 | return templ_7745c5c3_Err | |
| 1616 | 1592 | } | |
| @@ -1618,12 +1594,12 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1618 | 1594 | if templ_7745c5c3_Err != nil { | |
| 1619 | 1595 | return templ_7745c5c3_Err | |
| 1620 | 1596 | } | |
| 1621 | - | var templ_7745c5c3_Var82 string | |
| 1622 | - | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | |
| 1597 | + | var templ_7745c5c3_Var80 string | |
| 1598 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | |
| 1623 | 1599 | if templ_7745c5c3_Err != nil { | |
| 1624 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 289, Col: 50} | |
| 1600 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 287, Col: 50} | |
| 1625 | 1601 | } | |
| 1626 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1602 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1627 | 1603 | if templ_7745c5c3_Err != nil { | |
| 1628 | 1604 | return templ_7745c5c3_Err | |
| 1629 | 1605 | } | |
| @@ -1631,44 +1607,63 @@ func commentForm(f CommentForm, suffix string) templ.Component { | |||
| 1631 | 1607 | if templ_7745c5c3_Err != nil { | |
| 1632 | 1608 | return templ_7745c5c3_Err | |
| 1633 | 1609 | } | |
| 1634 | - | var templ_7745c5c3_Var83 string | |
| 1635 | - | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | |
| 1610 | + | var templ_7745c5c3_Var81 string | |
| 1611 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | |
| 1636 | 1612 | if templ_7745c5c3_Err != nil { | |
| 1637 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 290, Col: 50} | |
| 1613 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 50} | |
| 1638 | 1614 | } | |
| 1639 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | |
| 1615 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81) | |
| 1616 | + | if templ_7745c5c3_Err != nil { | |
| 1617 | + | return templ_7745c5c3_Err | |
| 1618 | + | } | |
| 1619 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\"> ") | |
| 1620 | + | if templ_7745c5c3_Err != nil { | |
| 1621 | + | return templ_7745c5c3_Err | |
| 1622 | + | } | |
| 1623 | + | } | |
| 1624 | + | if f.From > 0 { | |
| 1625 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<input type=\"hidden\" name=\"from\" value=\"") | |
| 1626 | + | if templ_7745c5c3_Err != nil { | |
| 1627 | + | return templ_7745c5c3_Err | |
| 1628 | + | } | |
| 1629 | + | var templ_7745c5c3_Var82 string | |
| 1630 | + | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From)) | |
| 1631 | + | if templ_7745c5c3_Err != nil { | |
| 1632 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 56} | |
| 1633 | + | } | |
| 1634 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1640 | 1635 | if templ_7745c5c3_Err != nil { | |
| 1641 | 1636 | return templ_7745c5c3_Err | |
| 1642 | 1637 | } | |
| 1643 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\">") | |
| 1638 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\">") | |
| 1644 | 1639 | if templ_7745c5c3_Err != nil { | |
| 1645 | 1640 | return templ_7745c5c3_Err | |
| 1646 | 1641 | } | |
| 1647 | 1642 | } | |
| 1648 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ") | |
| 1643 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ") | |
| 1649 | 1644 | if templ_7745c5c3_Err != nil { | |
| 1650 | 1645 | return templ_7745c5c3_Err | |
| 1651 | 1646 | } | |
| 1652 | 1647 | if f.Cancel != "" { | |
| 1653 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<a class=\"btn\" href=\"") | |
| 1648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<a class=\"btn\" href=\"") | |
| 1654 | 1649 | if templ_7745c5c3_Err != nil { | |
| 1655 | 1650 | return templ_7745c5c3_Err | |
| 1656 | 1651 | } | |
| 1657 | - | var templ_7745c5c3_Var84 templ.SafeURL | |
| 1658 | - | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | |
| 1652 | + | var templ_7745c5c3_Var83 templ.SafeURL | |
| 1653 | + | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | |
| 1659 | 1654 | if templ_7745c5c3_Err != nil { | |
| 1660 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 49} | |
| 1655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 49} | |
| 1661 | 1656 | } | |
| 1662 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84)) | |
| 1657 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) | |
| 1663 | 1658 | if templ_7745c5c3_Err != nil { | |
| 1664 | 1659 | return templ_7745c5c3_Err | |
| 1665 | 1660 | } | |
| 1666 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "\">Cancel</a>") | |
| 1661 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\">Cancel</a>") | |
| 1667 | 1662 | if templ_7745c5c3_Err != nil { | |
| 1668 | 1663 | return templ_7745c5c3_Err | |
| 1669 | 1664 | } | |
| 1670 | 1665 | } | |
| 1671 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "</div></form>") | |
| 1666 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</div></form>") | |
| 1672 | 1667 | if templ_7745c5c3_Err != nil { | |
| 1673 | 1668 | return templ_7745c5c3_Err | |
| 1674 | 1669 | } | |
| @@ -1692,241 +1687,349 @@ func mrRail(p MRPage) templ.Component { | |||
| 1692 | 1687 | }() | |
| 1693 | 1688 | } | |
| 1694 | 1689 | ctx = templ.InitializeContext(ctx) | |
| 1695 | - | templ_7745c5c3_Var85 := templ.GetChildren(ctx) | |
| 1696 | - | if templ_7745c5c3_Var85 == nil { | |
| 1697 | - | templ_7745c5c3_Var85 = templ.NopComponent | |
| 1690 | + | templ_7745c5c3_Var84 := templ.GetChildren(ctx) | |
| 1691 | + | if templ_7745c5c3_Var84 == nil { | |
| 1692 | + | templ_7745c5c3_Var84 = templ.NopComponent | |
| 1698 | 1693 | } | |
| 1699 | 1694 | ctx = templ.ClearChildren(ctx) | |
| 1700 | - | if p.Box != nil { | |
| 1701 | - | templ_7745c5c3_Err = mergeBox(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 1702 | - | if templ_7745c5c3_Err != nil { | |
| 1703 | - | return templ_7745c5c3_Err | |
| 1704 | - | } | |
| 1705 | - | } | |
| 1706 | - | templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer) | |
| 1695 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">") | |
| 1707 | 1696 | if templ_7745c5c3_Err != nil { | |
| 1708 | 1697 | return templ_7745c5c3_Err | |
| 1709 | 1698 | } | |
| 1710 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "<section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>") | |
| 1699 | + | var templ_7745c5c3_Var85 string | |
| 1700 | + | templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 1701 | + | if templ_7745c5c3_Err != nil { | |
| 1702 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 304, Col: 65} | |
| 1703 | + | } | |
| 1704 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85)) | |
| 1711 | 1705 | if templ_7745c5c3_Err != nil { | |
| 1712 | 1706 | return templ_7745c5c3_Err | |
| 1713 | 1707 | } | |
| 1714 | - | var templ_7745c5c3_Var86 string | |
| 1715 | - | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | |
| 1708 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "</span><kbd aria-hidden=\"true\">g f</kbd></h2>") | |
| 1716 | 1709 | if templ_7745c5c3_Err != nil { | |
| 1717 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 310, Col: 36} | |
| 1710 | + | return templ_7745c5c3_Err | |
| 1718 | 1711 | } | |
| 1719 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86)) | |
| 1712 | + | templ_7745c5c3_Err = FileList(p.Files).Render(ctx, templ_7745c5c3_Buffer) | |
| 1720 | 1713 | if templ_7745c5c3_Err != nil { | |
| 1721 | 1714 | return templ_7745c5c3_Err | |
| 1722 | 1715 | } | |
| 1723 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</dd><dt>checks</dt><dd>") | |
| 1716 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</section>") | |
| 1724 | 1717 | if templ_7745c5c3_Err != nil { | |
| 1725 | 1718 | return templ_7745c5c3_Err | |
| 1726 | 1719 | } | |
| 1727 | - | if p.ChecksBlock { | |
| 1728 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "failing ones block the merge") | |
| 1729 | - | if templ_7745c5c3_Err != nil { | |
| 1730 | - | return templ_7745c5c3_Err | |
| 1731 | - | } | |
| 1732 | - | } else { | |
| 1733 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "do not block the merge") | |
| 1734 | - | if templ_7745c5c3_Err != nil { | |
| 1735 | - | return templ_7745c5c3_Err | |
| 1736 | - | } | |
| 1720 | + | templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer) | |
| 1721 | + | if templ_7745c5c3_Err != nil { | |
| 1722 | + | return templ_7745c5c3_Err | |
| 1737 | 1723 | } | |
| 1738 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</dd></dl></section><section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">") | |
| 1724 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">") | |
| 1739 | 1725 | if templ_7745c5c3_Err != nil { | |
| 1740 | 1726 | return templ_7745c5c3_Err | |
| 1741 | 1727 | } | |
| 1742 | - | var templ_7745c5c3_Var87 string | |
| 1743 | - | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | |
| 1728 | + | var templ_7745c5c3_Var86 string | |
| 1729 | + | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | |
| 1744 | 1730 | if templ_7745c5c3_Err != nil { | |
| 1745 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 322, Col: 71} | |
| 1731 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 309, Col: 71} | |
| 1746 | 1732 | } | |
| 1747 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) | |
| 1733 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86)) | |
| 1748 | 1734 | if templ_7745c5c3_Err != nil { | |
| 1749 | 1735 | return templ_7745c5c3_Err | |
| 1750 | 1736 | } | |
| 1751 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</span></h2><ul class=\"pcommits\">") | |
| 1737 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</span></h2><ul class=\"pcommits\">") | |
| 1752 | 1738 | if templ_7745c5c3_Err != nil { | |
| 1753 | 1739 | return templ_7745c5c3_Err | |
| 1754 | 1740 | } | |
| 1755 | 1741 | for _, c := range p.Commits { | |
| 1756 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<li") | |
| 1742 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "<li") | |
| 1757 | 1743 | if templ_7745c5c3_Err != nil { | |
| 1758 | 1744 | return templ_7745c5c3_Err | |
| 1759 | 1745 | } | |
| 1760 | 1746 | if c.New { | |
| 1761 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, " data-new") | |
| 1747 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, " data-new") | |
| 1762 | 1748 | if templ_7745c5c3_Err != nil { | |
| 1763 | 1749 | return templ_7745c5c3_Err | |
| 1764 | 1750 | } | |
| 1765 | 1751 | } | |
| 1766 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "><a href=\"") | |
| 1752 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "><a href=\"") | |
| 1767 | 1753 | if templ_7745c5c3_Err != nil { | |
| 1768 | 1754 | return templ_7745c5c3_Err | |
| 1769 | 1755 | } | |
| 1770 | - | var templ_7745c5c3_Var88 templ.SafeURL | |
| 1771 | - | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 1756 | + | var templ_7745c5c3_Var87 templ.SafeURL | |
| 1757 | + | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 1772 | 1758 | if templ_7745c5c3_Err != nil { | |
| 1773 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 36} | |
| 1759 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 36} | |
| 1774 | 1760 | } | |
| 1775 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) | |
| 1761 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) | |
| 1776 | 1762 | if templ_7745c5c3_Err != nil { | |
| 1777 | 1763 | return templ_7745c5c3_Err | |
| 1778 | 1764 | } | |
| 1779 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\">") | |
| 1765 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\">") | |
| 1780 | 1766 | if templ_7745c5c3_Err != nil { | |
| 1781 | 1767 | return templ_7745c5c3_Err | |
| 1782 | 1768 | } | |
| 1783 | - | var templ_7745c5c3_Var89 string | |
| 1784 | - | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 1769 | + | var templ_7745c5c3_Var88 string | |
| 1770 | + | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 1785 | 1771 | if templ_7745c5c3_Err != nil { | |
| 1786 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 48} | |
| 1772 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 48} | |
| 1787 | 1773 | } | |
| 1788 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1774 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) | |
| 1789 | 1775 | if templ_7745c5c3_Err != nil { | |
| 1790 | 1776 | return templ_7745c5c3_Err | |
| 1791 | 1777 | } | |
| 1792 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "</a> <span class=\"s\">") | |
| 1778 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</a> <span class=\"s\">") | |
| 1793 | 1779 | if templ_7745c5c3_Err != nil { | |
| 1794 | 1780 | return templ_7745c5c3_Err | |
| 1795 | 1781 | } | |
| 1796 | - | var templ_7745c5c3_Var90 string | |
| 1797 | - | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 1782 | + | var templ_7745c5c3_Var89 string | |
| 1783 | + | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 1798 | 1784 | if templ_7745c5c3_Err != nil { | |
| 1799 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 32} | |
| 1785 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 314, Col: 32} | |
| 1800 | 1786 | } | |
| 1801 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90)) | |
| 1787 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1802 | 1788 | if templ_7745c5c3_Err != nil { | |
| 1803 | 1789 | return templ_7745c5c3_Err | |
| 1804 | 1790 | } | |
| 1805 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</span> <span class=\"mute\">") | |
| 1791 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</span> <span class=\"mute\">") | |
| 1806 | 1792 | if templ_7745c5c3_Err != nil { | |
| 1807 | 1793 | return templ_7745c5c3_Err | |
| 1808 | 1794 | } | |
| 1809 | - | var templ_7745c5c3_Var91 string | |
| 1810 | - | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1795 | + | var templ_7745c5c3_Var90 string | |
| 1796 | + | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1811 | 1797 | if templ_7745c5c3_Err != nil { | |
| 1812 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 329, Col: 16} | |
| 1798 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 316, Col: 16} | |
| 1813 | 1799 | } | |
| 1814 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91)) | |
| 1800 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90)) | |
| 1815 | 1801 | if templ_7745c5c3_Err != nil { | |
| 1816 | 1802 | return templ_7745c5c3_Err | |
| 1817 | 1803 | } | |
| 1818 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, " ") | |
| 1804 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, " ") | |
| 1819 | 1805 | if templ_7745c5c3_Err != nil { | |
| 1820 | 1806 | return templ_7745c5c3_Err | |
| 1821 | 1807 | } | |
| 1822 | 1808 | if c.WhenAt != "" { | |
| 1823 | - | var templ_7745c5c3_Var92 string | |
| 1824 | - | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 1809 | + | var templ_7745c5c3_Var91 string | |
| 1810 | + | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 1825 | 1811 | if templ_7745c5c3_Err != nil { | |
| 1826 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 15} | |
| 1812 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 318, Col: 15} | |
| 1827 | 1813 | } | |
| 1828 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92)) | |
| 1814 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91)) | |
| 1829 | 1815 | if templ_7745c5c3_Err != nil { | |
| 1830 | 1816 | return templ_7745c5c3_Err | |
| 1831 | 1817 | } | |
| 1832 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<time datetime=\"") | |
| 1818 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "<time datetime=\"") | |
| 1833 | 1819 | if templ_7745c5c3_Err != nil { | |
| 1834 | 1820 | return templ_7745c5c3_Err | |
| 1835 | 1821 | } | |
| 1836 | - | var templ_7745c5c3_Var93 string | |
| 1837 | - | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1822 | + | var templ_7745c5c3_Var92 string | |
| 1823 | + | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1838 | 1824 | if templ_7745c5c3_Err != nil { | |
| 1839 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 42} | |
| 1825 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 318, Col: 42} | |
| 1840 | 1826 | } | |
| 1841 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var93) | |
| 1827 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92) | |
| 1842 | 1828 | if templ_7745c5c3_Err != nil { | |
| 1843 | 1829 | return templ_7745c5c3_Err | |
| 1844 | 1830 | } | |
| 1845 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "\">") | |
| 1831 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "\">") | |
| 1846 | 1832 | if templ_7745c5c3_Err != nil { | |
| 1847 | 1833 | return templ_7745c5c3_Err | |
| 1848 | 1834 | } | |
| 1849 | - | var templ_7745c5c3_Var94 string | |
| 1850 | - | templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1835 | + | var templ_7745c5c3_Var93 string | |
| 1836 | + | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1851 | 1837 | if templ_7745c5c3_Err != nil { | |
| 1852 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 53} | |
| 1838 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 318, Col: 53} | |
| 1853 | 1839 | } | |
| 1854 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94)) | |
| 1840 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93)) | |
| 1855 | 1841 | if templ_7745c5c3_Err != nil { | |
| 1856 | 1842 | return templ_7745c5c3_Err | |
| 1857 | 1843 | } | |
| 1858 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</time> ") | |
| 1844 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "</time> ") | |
| 1859 | 1845 | if templ_7745c5c3_Err != nil { | |
| 1860 | 1846 | return templ_7745c5c3_Err | |
| 1861 | 1847 | } | |
| 1862 | 1848 | } | |
| 1863 | 1849 | if c.New { | |
| 1864 | - | var templ_7745c5c3_Var95 string | |
| 1865 | - | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | |
| 1850 | + | var templ_7745c5c3_Var94 string | |
| 1851 | + | templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | |
| 1866 | 1852 | if templ_7745c5c3_Err != nil { | |
| 1867 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 18} | |
| 1853 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 321, Col: 18} | |
| 1868 | 1854 | } | |
| 1869 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | |
| 1855 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94)) | |
| 1870 | 1856 | if templ_7745c5c3_Err != nil { | |
| 1871 | 1857 | return templ_7745c5c3_Err | |
| 1872 | 1858 | } | |
| 1873 | 1859 | } | |
| 1874 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "</span></li>") | |
| 1860 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</span></li>") | |
| 1875 | 1861 | if templ_7745c5c3_Err != nil { | |
| 1876 | 1862 | return templ_7745c5c3_Err | |
| 1877 | 1863 | } | |
| 1878 | 1864 | } | |
| 1879 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</ul>") | |
| 1865 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "</ul>") | |
| 1880 | 1866 | if templ_7745c5c3_Err != nil { | |
| 1881 | 1867 | return templ_7745c5c3_Err | |
| 1882 | 1868 | } | |
| 1883 | 1869 | if len(p.Commits) == 0 { | |
| 1884 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<p class=\"dim\">no commit to merge.</p>") | |
| 1870 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "<p class=\"dim\">no commit to merge.</p>") | |
| 1885 | 1871 | if templ_7745c5c3_Err != nil { | |
| 1886 | 1872 | return templ_7745c5c3_Err | |
| 1887 | 1873 | } | |
| 1888 | 1874 | } | |
| 1889 | 1875 | if p.CommitsNote != "" { | |
| 1890 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "<p class=\"note\">") | |
| 1876 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "<p class=\"note\">") | |
| 1891 | 1877 | if templ_7745c5c3_Err != nil { | |
| 1892 | 1878 | return templ_7745c5c3_Err | |
| 1893 | 1879 | } | |
| 1894 | - | var templ_7745c5c3_Var96 string | |
| 1895 | - | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | |
| 1880 | + | var templ_7745c5c3_Var95 string | |
| 1881 | + | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | |
| 1896 | 1882 | if templ_7745c5c3_Err != nil { | |
| 1897 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 344, Col: 34} | |
| 1883 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 34} | |
| 1898 | 1884 | } | |
| 1899 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | |
| 1885 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | |
| 1900 | 1886 | if templ_7745c5c3_Err != nil { | |
| 1901 | 1887 | return templ_7745c5c3_Err | |
| 1902 | 1888 | } | |
| 1903 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "</p>") | |
| 1889 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "</p>") | |
| 1904 | 1890 | if templ_7745c5c3_Err != nil { | |
| 1905 | 1891 | return templ_7745c5c3_Err | |
| 1906 | 1892 | } | |
| 1907 | 1893 | } | |
| 1908 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "</section><section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">") | |
| 1894 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>") | |
| 1909 | 1895 | if templ_7745c5c3_Err != nil { | |
| 1910 | 1896 | return templ_7745c5c3_Err | |
| 1911 | 1897 | } | |
| 1912 | - | var templ_7745c5c3_Var97 string | |
| 1913 | - | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 1898 | + | var templ_7745c5c3_Var96 string | |
| 1899 | + | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | |
| 1914 | 1900 | if templ_7745c5c3_Err != nil { | |
| 1915 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 348, Col: 65} | |
| 1901 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 338, Col: 36} | |
| 1916 | 1902 | } | |
| 1917 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) | |
| 1903 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | |
| 1918 | 1904 | if templ_7745c5c3_Err != nil { | |
| 1919 | 1905 | return templ_7745c5c3_Err | |
| 1920 | 1906 | } | |
| 1921 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</span></h2>") | |
| 1907 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "</dd><dt>checks</dt><dd>") | |
| 1922 | 1908 | if templ_7745c5c3_Err != nil { | |
| 1923 | 1909 | return templ_7745c5c3_Err | |
| 1924 | 1910 | } | |
| 1925 | - | templ_7745c5c3_Err = FileList(p.Files).Render(ctx, templ_7745c5c3_Buffer) | |
| 1911 | + | if p.ChecksBlock { | |
| 1912 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "failing ones block the merge") | |
| 1913 | + | if templ_7745c5c3_Err != nil { | |
| 1914 | + | return templ_7745c5c3_Err | |
| 1915 | + | } | |
| 1916 | + | } else { | |
| 1917 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "do not block the merge") | |
| 1918 | + | if templ_7745c5c3_Err != nil { | |
| 1919 | + | return templ_7745c5c3_Err | |
| 1920 | + | } | |
| 1921 | + | } | |
| 1922 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</dd></dl></section>") | |
| 1923 | + | if templ_7745c5c3_Err != nil { | |
| 1924 | + | return templ_7745c5c3_Err | |
| 1925 | + | } | |
| 1926 | + | return nil | |
| 1927 | + | }) | |
| 1928 | + | } | |
| 1929 | + | ||
| 1930 | + | // mrBar is the bar above the diff, which follows the review: the totals, the | |
| 1931 | + | // layout of the diff, the state of the checks and, for the owner of an open | |
| 1932 | + | // request, the merge menu and the other actions. Each menu is a <details>: it | |
| 1933 | + | // works without scripts; with them, M and the keys of the menu drive it. | |
| 1934 | + | func mrBar(p MRPage) templ.Component { | |
| 1935 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1936 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1937 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1938 | + | return templ_7745c5c3_CtxErr | |
| 1939 | + | } | |
| 1940 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1941 | + | if !templ_7745c5c3_IsBuffer { | |
| 1942 | + | defer func() { | |
| 1943 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1944 | + | if templ_7745c5c3_Err == nil { | |
| 1945 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1946 | + | } | |
| 1947 | + | }() | |
| 1948 | + | } | |
| 1949 | + | ctx = templ.InitializeContext(ctx) | |
| 1950 | + | templ_7745c5c3_Var97 := templ.GetChildren(ctx) | |
| 1951 | + | if templ_7745c5c3_Var97 == nil { | |
| 1952 | + | templ_7745c5c3_Var97 = templ.NopComponent | |
| 1953 | + | } | |
| 1954 | + | ctx = templ.ClearChildren(ctx) | |
| 1955 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<div class=\"diff-tools mr-bar\" role=\"toolbar\" aria-label=\"Diff view and merge\"><span class=\"dim\">") | |
| 1956 | + | if templ_7745c5c3_Err != nil { | |
| 1957 | + | return templ_7745c5c3_Err | |
| 1958 | + | } | |
| 1959 | + | var templ_7745c5c3_Var98 string | |
| 1960 | + | templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 1961 | + | if templ_7745c5c3_Err != nil { | |
| 1962 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 357, Col: 55} | |
| 1963 | + | } | |
| 1964 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98)) | |
| 1965 | + | if templ_7745c5c3_Err != nil { | |
| 1966 | + | return templ_7745c5c3_Err | |
| 1967 | + | } | |
| 1968 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, " · <span class=\"plus\">+") | |
| 1969 | + | if templ_7745c5c3_Err != nil { | |
| 1970 | + | return templ_7745c5c3_Err | |
| 1971 | + | } | |
| 1972 | + | var templ_7745c5c3_Var99 string | |
| 1973 | + | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 1974 | + | if templ_7745c5c3_Err != nil { | |
| 1975 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 357, Col: 95} | |
| 1976 | + | } | |
| 1977 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 1978 | + | if templ_7745c5c3_Err != nil { | |
| 1979 | + | return templ_7745c5c3_Err | |
| 1980 | + | } | |
| 1981 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "</span> <span class=\"minus\">-") | |
| 1982 | + | if templ_7745c5c3_Err != nil { | |
| 1983 | + | return templ_7745c5c3_Err | |
| 1984 | + | } | |
| 1985 | + | var templ_7745c5c3_Var100 string | |
| 1986 | + | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 1987 | + | if templ_7745c5c3_Err != nil { | |
| 1988 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 357, Col: 140} | |
| 1989 | + | } | |
| 1990 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 1991 | + | if templ_7745c5c3_Err != nil { | |
| 1992 | + | return templ_7745c5c3_Err | |
| 1993 | + | } | |
| 1994 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "</span></span>") | |
| 1995 | + | if templ_7745c5c3_Err != nil { | |
| 1996 | + | return templ_7745c5c3_Err | |
| 1997 | + | } | |
| 1998 | + | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 1999 | + | if templ_7745c5c3_Err != nil { | |
| 2000 | + | return templ_7745c5c3_Err | |
| 2001 | + | } | |
| 2002 | + | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "Split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 2003 | + | if templ_7745c5c3_Err != nil { | |
| 2004 | + | return templ_7745c5c3_Err | |
| 2005 | + | } | |
| 2006 | + | templ_7745c5c3_Err = collapseButton().Render(ctx, templ_7745c5c3_Buffer) | |
| 1926 | 2007 | if templ_7745c5c3_Err != nil { | |
| 1927 | 2008 | return templ_7745c5c3_Err | |
| 1928 | 2009 | } | |
| 1929 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "</section>") | |
| 2010 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<span class=\"grow\"></span>") | |
| 2011 | + | if templ_7745c5c3_Err != nil { | |
| 2012 | + | return templ_7745c5c3_Err | |
| 2013 | + | } | |
| 2014 | + | templ_7745c5c3_Err = MRChecksSum(p.ChecksSum).Render(ctx, templ_7745c5c3_Buffer) | |
| 2015 | + | if templ_7745c5c3_Err != nil { | |
| 2016 | + | return templ_7745c5c3_Err | |
| 2017 | + | } | |
| 2018 | + | if p.Box != nil { | |
| 2019 | + | templ_7745c5c3_Err = mergeMenu(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 2020 | + | if templ_7745c5c3_Err != nil { | |
| 2021 | + | return templ_7745c5c3_Err | |
| 2022 | + | } | |
| 2023 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, " ") | |
| 2024 | + | if templ_7745c5c3_Err != nil { | |
| 2025 | + | return templ_7745c5c3_Err | |
| 2026 | + | } | |
| 2027 | + | templ_7745c5c3_Err = moreMenu(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 2028 | + | if templ_7745c5c3_Err != nil { | |
| 2029 | + | return templ_7745c5c3_Err | |
| 2030 | + | } | |
| 2031 | + | } | |
| 2032 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "</div>") | |
| 1930 | 2033 | if templ_7745c5c3_Err != nil { | |
| 1931 | 2034 | return templ_7745c5c3_Err | |
| 1932 | 2035 | } | |
| @@ -1934,10 +2037,10 @@ func mrRail(p MRPage) templ.Component { | |||
| 1934 | 2037 | }) | |
| 1935 | 2038 | } | |
| 1936 | 2039 | ||
| 1937 | - | // mergeBox is the first block of the rail of an open request, for the owner: | |
| 1938 | - | // the merge form (it carries the head that was read), the terminal lines, and | |
| 1939 | - | // the other things one does with the request. | |
| 1940 | - | func mergeBox(p MRPage) templ.Component { | |
| 2040 | + | // mergeMenu is the merge of an open request, for the owner: the form (it | |
| 2041 | + | // carries the head that was read), or "Ready for review" for a draft. The | |
| 2042 | + | // mode and the branch deletion have keys (1-3, D) while it is open. | |
| 2043 | + | func mergeMenu(p MRPage) templ.Component { | |
| 1941 | 2044 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1942 | 2045 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1943 | 2046 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| @@ -1953,212 +2056,304 @@ func mergeBox(p MRPage) templ.Component { | |||
| 1953 | 2056 | }() | |
| 1954 | 2057 | } | |
| 1955 | 2058 | ctx = templ.InitializeContext(ctx) | |
| 1956 | - | templ_7745c5c3_Var98 := templ.GetChildren(ctx) | |
| 1957 | - | if templ_7745c5c3_Var98 == nil { | |
| 1958 | - | templ_7745c5c3_Var98 = templ.NopComponent | |
| 2059 | + | templ_7745c5c3_Var101 := templ.GetChildren(ctx) | |
| 2060 | + | if templ_7745c5c3_Var101 == nil { | |
| 2061 | + | templ_7745c5c3_Var101 = templ.NopComponent | |
| 1959 | 2062 | } | |
| 1960 | 2063 | ctx = templ.ClearChildren(ctx) | |
| 1961 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "<section class=\"decide\" aria-labelledby=\"r-merge\" data-state=\"open\"><h2 id=\"r-merge\">Merge</h2><dl class=\"kv\"><dt>head</dt><dd>") | |
| 2064 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">") | |
| 2065 | + | if templ_7745c5c3_Err != nil { | |
| 2066 | + | return templ_7745c5c3_Err | |
| 2067 | + | } | |
| 2068 | + | if p.Box.ReadyAction != "" { | |
| 2069 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "Draft ") | |
| 2070 | + | if templ_7745c5c3_Err != nil { | |
| 2071 | + | return templ_7745c5c3_Err | |
| 2072 | + | } | |
| 2073 | + | } else { | |
| 2074 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "Merge ") | |
| 2075 | + | if templ_7745c5c3_Err != nil { | |
| 2076 | + | return templ_7745c5c3_Err | |
| 2077 | + | } | |
| 2078 | + | } | |
| 2079 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>") | |
| 1962 | 2080 | if templ_7745c5c3_Err != nil { | |
| 1963 | 2081 | return templ_7745c5c3_Err | |
| 1964 | 2082 | } | |
| 1965 | 2083 | if p.Box.HeadGone { | |
| 1966 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "gone: the source branch was deleted") | |
| 2084 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "gone: the source branch was deleted") | |
| 1967 | 2085 | if templ_7745c5c3_Err != nil { | |
| 1968 | 2086 | return templ_7745c5c3_Err | |
| 1969 | 2087 | } | |
| 1970 | 2088 | } else { | |
| 1971 | - | var templ_7745c5c3_Var99 string | |
| 1972 | - | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort) | |
| 2089 | + | var templ_7745c5c3_Var102 string | |
| 2090 | + | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort) | |
| 1973 | 2091 | if templ_7745c5c3_Err != nil { | |
| 1974 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 365, Col: 18} | |
| 2092 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 390, Col: 19} | |
| 1975 | 2093 | } | |
| 1976 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 2094 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) | |
| 1977 | 2095 | if templ_7745c5c3_Err != nil { | |
| 1978 | 2096 | return templ_7745c5c3_Err | |
| 1979 | 2097 | } | |
| 1980 | 2098 | } | |
| 1981 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</dd><dt>into</dt><dd>") | |
| 2099 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "</dd><dt>into</dt><dd>") | |
| 1982 | 2100 | if templ_7745c5c3_Err != nil { | |
| 1983 | 2101 | return templ_7745c5c3_Err | |
| 1984 | 2102 | } | |
| 1985 | - | var templ_7745c5c3_Var100 string | |
| 1986 | - | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2103 | + | var templ_7745c5c3_Var103 string | |
| 2104 | + | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 1987 | 2105 | if templ_7745c5c3_Err != nil { | |
| 1988 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 369, Col: 17} | |
| 2106 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 18} | |
| 1989 | 2107 | } | |
| 1990 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 2108 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) | |
| 1991 | 2109 | if templ_7745c5c3_Err != nil { | |
| 1992 | 2110 | return templ_7745c5c3_Err | |
| 1993 | 2111 | } | |
| 1994 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</dd></dl>") | |
| 2112 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</dd></dl>") | |
| 1995 | 2113 | if templ_7745c5c3_Err != nil { | |
| 1996 | 2114 | return templ_7745c5c3_Err | |
| 1997 | 2115 | } | |
| 1998 | 2116 | if p.Box.ReadyAction != "" { | |
| 1999 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<p class=\"d-draft\">this is a draft: it cannot be merged until it is ready for review.</p><form class=\"d-btns\" method=\"post\" action=\"") | |
| 2117 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<p class=\"d-draft\">this is a draft: it cannot be merged until it is ready for review.</p><form class=\"d-btns\" method=\"post\" action=\"") | |
| 2000 | 2118 | if templ_7745c5c3_Err != nil { | |
| 2001 | 2119 | return templ_7745c5c3_Err | |
| 2002 | 2120 | } | |
| 2003 | - | var templ_7745c5c3_Var101 templ.SafeURL | |
| 2004 | - | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction)) | |
| 2121 | + | var templ_7745c5c3_Var104 templ.SafeURL | |
| 2122 | + | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction)) | |
| 2005 | 2123 | if templ_7745c5c3_Err != nil { | |
| 2006 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 79} | |
| 2124 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 398, Col: 80} | |
| 2007 | 2125 | } | |
| 2008 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | |
| 2126 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | |
| 2009 | 2127 | if templ_7745c5c3_Err != nil { | |
| 2010 | 2128 | return templ_7745c5c3_Err | |
| 2011 | 2129 | } | |
| 2012 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"><button type=\"submit\" class=\"btn primary\" title=\"remove the draft prefix from the title\">Ready for review</button></form>") | |
| 2130 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>") | |
| 2013 | 2131 | if templ_7745c5c3_Err != nil { | |
| 2014 | 2132 | return templ_7745c5c3_Err | |
| 2015 | 2133 | } | |
| 2016 | 2134 | } else if !p.Box.HeadGone { | |
| 2017 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<form class=\"d-acts\" method=\"post\" action=\"") | |
| 2135 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<form class=\"d-acts\" method=\"post\" action=\"") | |
| 2018 | 2136 | if templ_7745c5c3_Err != nil { | |
| 2019 | 2137 | return templ_7745c5c3_Err | |
| 2020 | 2138 | } | |
| 2021 | - | var templ_7745c5c3_Var102 templ.SafeURL | |
| 2022 | - | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction)) | |
| 2139 | + | var templ_7745c5c3_Var105 templ.SafeURL | |
| 2140 | + | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction)) | |
| 2023 | 2141 | if templ_7745c5c3_Err != nil { | |
| 2024 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 79} | |
| 2142 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 402, Col: 80} | |
| 2025 | 2143 | } | |
| 2026 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) | |
| 2144 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2027 | 2145 | if templ_7745c5c3_Err != nil { | |
| 2028 | 2146 | return templ_7745c5c3_Err | |
| 2029 | 2147 | } | |
| 2030 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2148 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2031 | 2149 | if templ_7745c5c3_Err != nil { | |
| 2032 | 2150 | return templ_7745c5c3_Err | |
| 2033 | 2151 | } | |
| 2034 | - | var templ_7745c5c3_Var103 string | |
| 2035 | - | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2152 | + | var templ_7745c5c3_Var106 string | |
| 2153 | + | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2036 | 2154 | if templ_7745c5c3_Err != nil { | |
| 2037 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 378, Col: 58} | |
| 2155 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 403, Col: 59} | |
| 2038 | 2156 | } | |
| 2039 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103) | |
| 2157 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var106) | |
| 2040 | 2158 | if templ_7745c5c3_Err != nil { | |
| 2041 | 2159 | return templ_7745c5c3_Err | |
| 2042 | 2160 | } | |
| 2043 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\"> <label class=\"sr\" for=\"merge-mode\">merge mode</label> <select id=\"merge-mode\" name=\"mode\">") | |
| 2161 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ") | |
| 2044 | 2162 | if templ_7745c5c3_Err != nil { | |
| 2045 | 2163 | return templ_7745c5c3_Err | |
| 2046 | 2164 | } | |
| 2047 | - | for _, m := range p.Box.Modes { | |
| 2048 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<option value=\"") | |
| 2165 | + | for i, m := range p.Box.Modes { | |
| 2166 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<label><input type=\"radio\" name=\"mode\" value=\"") | |
| 2049 | 2167 | if templ_7745c5c3_Err != nil { | |
| 2050 | 2168 | return templ_7745c5c3_Err | |
| 2051 | 2169 | } | |
| 2052 | - | var templ_7745c5c3_Var104 string | |
| 2053 | - | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value) | |
| 2170 | + | var templ_7745c5c3_Var107 string | |
| 2171 | + | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value) | |
| 2054 | 2172 | if templ_7745c5c3_Err != nil { | |
| 2055 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 29} | |
| 2173 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 55} | |
| 2056 | 2174 | } | |
| 2057 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var104) | |
| 2175 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107) | |
| 2058 | 2176 | if templ_7745c5c3_Err != nil { | |
| 2059 | 2177 | return templ_7745c5c3_Err | |
| 2060 | 2178 | } | |
| 2061 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">") | |
| 2179 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "\"") | |
| 2062 | 2180 | if templ_7745c5c3_Err != nil { | |
| 2063 | 2181 | return templ_7745c5c3_Err | |
| 2064 | 2182 | } | |
| 2065 | - | var templ_7745c5c3_Var105 string | |
| 2066 | - | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label) | |
| 2183 | + | if i == 0 { | |
| 2184 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, " checked") | |
| 2185 | + | if templ_7745c5c3_Err != nil { | |
| 2186 | + | return templ_7745c5c3_Err | |
| 2187 | + | } | |
| 2188 | + | } | |
| 2189 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "> ") | |
| 2067 | 2190 | if templ_7745c5c3_Err != nil { | |
| 2068 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 41} | |
| 2191 | + | return templ_7745c5c3_Err | |
| 2069 | 2192 | } | |
| 2070 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2193 | + | var templ_7745c5c3_Var108 string | |
| 2194 | + | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label) | |
| 2195 | + | if templ_7745c5c3_Err != nil { | |
| 2196 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 409, Col: 17} | |
| 2197 | + | } | |
| 2198 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) | |
| 2071 | 2199 | if templ_7745c5c3_Err != nil { | |
| 2072 | 2200 | return templ_7745c5c3_Err | |
| 2073 | 2201 | } | |
| 2074 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "</option>") | |
| 2202 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, " ") | |
| 2203 | + | if templ_7745c5c3_Err != nil { | |
| 2204 | + | return templ_7745c5c3_Err | |
| 2205 | + | } | |
| 2206 | + | if i < 9 { | |
| 2207 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "<kbd aria-hidden=\"true\">") | |
| 2208 | + | if templ_7745c5c3_Err != nil { | |
| 2209 | + | return templ_7745c5c3_Err | |
| 2210 | + | } | |
| 2211 | + | var templ_7745c5c3_Var109 string | |
| 2212 | + | templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1)) | |
| 2213 | + | if templ_7745c5c3_Err != nil { | |
| 2214 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 46} | |
| 2215 | + | } | |
| 2216 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) | |
| 2217 | + | if templ_7745c5c3_Err != nil { | |
| 2218 | + | return templ_7745c5c3_Err | |
| 2219 | + | } | |
| 2220 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</kbd>") | |
| 2221 | + | if templ_7745c5c3_Err != nil { | |
| 2222 | + | return templ_7745c5c3_Err | |
| 2223 | + | } | |
| 2224 | + | } | |
| 2225 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</label>") | |
| 2075 | 2226 | if templ_7745c5c3_Err != nil { | |
| 2076 | 2227 | return templ_7745c5c3_Err | |
| 2077 | 2228 | } | |
| 2078 | 2229 | } | |
| 2079 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</select> ") | |
| 2230 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "</fieldset>") | |
| 2080 | 2231 | if templ_7745c5c3_Err != nil { | |
| 2081 | 2232 | return templ_7745c5c3_Err | |
| 2082 | 2233 | } | |
| 2083 | 2234 | if p.Box.CanDeleteBranch { | |
| 2084 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ") | |
| 2235 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ") | |
| 2085 | 2236 | if templ_7745c5c3_Err != nil { | |
| 2086 | 2237 | return templ_7745c5c3_Err | |
| 2087 | 2238 | } | |
| 2088 | - | var templ_7745c5c3_Var106 string | |
| 2089 | - | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2239 | + | var templ_7745c5c3_Var110 string | |
| 2240 | + | templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2090 | 2241 | if templ_7745c5c3_Err != nil { | |
| 2091 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 101} | |
| 2242 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 417, Col: 102} | |
| 2092 | 2243 | } | |
| 2093 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) | |
| 2244 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110)) | |
| 2094 | 2245 | if templ_7745c5c3_Err != nil { | |
| 2095 | 2246 | return templ_7745c5c3_Err | |
| 2096 | 2247 | } | |
| 2097 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, " after the merge</label>") | |
| 2248 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>") | |
| 2098 | 2249 | if templ_7745c5c3_Err != nil { | |
| 2099 | 2250 | return templ_7745c5c3_Err | |
| 2100 | 2251 | } | |
| 2101 | 2252 | } | |
| 2102 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\">Merge</button></div></form>") | |
| 2253 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ") | |
| 2254 | + | if templ_7745c5c3_Err != nil { | |
| 2255 | + | return templ_7745c5c3_Err | |
| 2256 | + | } | |
| 2257 | + | var templ_7745c5c3_Var111 string | |
| 2258 | + | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2259 | + | if templ_7745c5c3_Err != nil { | |
| 2260 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 82} | |
| 2261 | + | } | |
| 2262 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | |
| 2263 | + | if templ_7745c5c3_Err != nil { | |
| 2264 | + | return templ_7745c5c3_Err | |
| 2265 | + | } | |
| 2266 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "</button></div></form>") | |
| 2103 | 2267 | if templ_7745c5c3_Err != nil { | |
| 2104 | 2268 | return templ_7745c5c3_Err | |
| 2105 | 2269 | } | |
| 2106 | 2270 | } | |
| 2107 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "<div class=\"d-btns\">") | |
| 2271 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</div></details>") | |
| 2272 | + | if templ_7745c5c3_Err != nil { | |
| 2273 | + | return templ_7745c5c3_Err | |
| 2274 | + | } | |
| 2275 | + | return nil | |
| 2276 | + | }) | |
| 2277 | + | } | |
| 2278 | + | ||
| 2279 | + | // moreMenu is what else the owner does with an open request: mark the head | |
| 2280 | + | // read, close it, merge it from a terminal. | |
| 2281 | + | func moreMenu(p MRPage) templ.Component { | |
| 2282 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2283 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 2284 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 2285 | + | return templ_7745c5c3_CtxErr | |
| 2286 | + | } | |
| 2287 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 2288 | + | if !templ_7745c5c3_IsBuffer { | |
| 2289 | + | defer func() { | |
| 2290 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 2291 | + | if templ_7745c5c3_Err == nil { | |
| 2292 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 2293 | + | } | |
| 2294 | + | }() | |
| 2295 | + | } | |
| 2296 | + | ctx = templ.InitializeContext(ctx) | |
| 2297 | + | templ_7745c5c3_Var112 := templ.GetChildren(ctx) | |
| 2298 | + | if templ_7745c5c3_Var112 == nil { | |
| 2299 | + | templ_7745c5c3_Var112 = templ.NopComponent | |
| 2300 | + | } | |
| 2301 | + | ctx = templ.ClearChildren(ctx) | |
| 2302 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\">") | |
| 2108 | 2303 | if templ_7745c5c3_Err != nil { | |
| 2109 | 2304 | return templ_7745c5c3_Err | |
| 2110 | 2305 | } | |
| 2111 | 2306 | if p.Box.CanMarkRead { | |
| 2112 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<form method=\"post\" action=\"") | |
| 2307 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<form method=\"post\" action=\"") | |
| 2113 | 2308 | if templ_7745c5c3_Err != nil { | |
| 2114 | 2309 | return templ_7745c5c3_Err | |
| 2115 | 2310 | } | |
| 2116 | - | var templ_7745c5c3_Var107 templ.SafeURL | |
| 2117 | - | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | |
| 2311 | + | var templ_7745c5c3_Var113 templ.SafeURL | |
| 2312 | + | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | |
| 2118 | 2313 | if templ_7745c5c3_Err != nil { | |
| 2119 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64} | |
| 2314 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 64} | |
| 2120 | 2315 | } | |
| 2121 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | |
| 2316 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | |
| 2122 | 2317 | if templ_7745c5c3_Err != nil { | |
| 2123 | 2318 | return templ_7745c5c3_Err | |
| 2124 | 2319 | } | |
| 2125 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2320 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2126 | 2321 | if templ_7745c5c3_Err != nil { | |
| 2127 | 2322 | return templ_7745c5c3_Err | |
| 2128 | 2323 | } | |
| 2129 | - | var templ_7745c5c3_Var108 string | |
| 2130 | - | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2324 | + | var templ_7745c5c3_Var114 string | |
| 2325 | + | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2131 | 2326 | if templ_7745c5c3_Err != nil { | |
| 2132 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 59} | |
| 2327 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 436, Col: 59} | |
| 2133 | 2328 | } | |
| 2134 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var108) | |
| 2329 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var114) | |
| 2135 | 2330 | if templ_7745c5c3_Err != nil { | |
| 2136 | 2331 | return templ_7745c5c3_Err | |
| 2137 | 2332 | } | |
| 2138 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\">Mark read</button></form>") | |
| 2333 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\" aria-keyshortcuts=\"Shift+R\" data-read>Mark read <kbd aria-hidden=\"true\">R</kbd></button></form>") | |
| 2139 | 2334 | if templ_7745c5c3_Err != nil { | |
| 2140 | 2335 | return templ_7745c5c3_Err | |
| 2141 | 2336 | } | |
| 2142 | 2337 | } | |
| 2143 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<form method=\"post\" action=\"") | |
| 2338 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<form method=\"post\" action=\"") | |
| 2144 | 2339 | if templ_7745c5c3_Err != nil { | |
| 2145 | 2340 | return templ_7745c5c3_Err | |
| 2146 | 2341 | } | |
| 2147 | - | var templ_7745c5c3_Var109 templ.SafeURL | |
| 2148 | - | templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | |
| 2342 | + | var templ_7745c5c3_Var115 templ.SafeURL | |
| 2343 | + | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | |
| 2149 | 2344 | if templ_7745c5c3_Err != nil { | |
| 2150 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 64} | |
| 2345 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 440, Col: 64} | |
| 2151 | 2346 | } | |
| 2152 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) | |
| 2347 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | |
| 2153 | 2348 | if templ_7745c5c3_Err != nil { | |
| 2154 | 2349 | return templ_7745c5c3_Err | |
| 2155 | 2350 | } | |
| 2156 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form></div>") | |
| 2351 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>") | |
| 2157 | 2352 | if templ_7745c5c3_Err != nil { | |
| 2158 | 2353 | return templ_7745c5c3_Err | |
| 2159 | 2354 | } | |
| 2160 | 2355 | if !p.Box.HeadGone && p.Box.ReadyAction == "" { | |
| 2161 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>") | |
| 2356 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>") | |
| 2162 | 2357 | if templ_7745c5c3_Err != nil { | |
| 2163 | 2358 | return templ_7745c5c3_Err | |
| 2164 | 2359 | } | |
| @@ -2166,12 +2361,12 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2166 | 2361 | if templ_7745c5c3_Err != nil { | |
| 2167 | 2362 | return templ_7745c5c3_Err | |
| 2168 | 2363 | } | |
| 2169 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</details>") | |
| 2364 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</details>") | |
| 2170 | 2365 | if templ_7745c5c3_Err != nil { | |
| 2171 | 2366 | return templ_7745c5c3_Err | |
| 2172 | 2367 | } | |
| 2173 | 2368 | } | |
| 2174 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</section>") | |
| 2369 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</div></details>") | |
| 2175 | 2370 | if templ_7745c5c3_Err != nil { | |
| 2176 | 2371 | return templ_7745c5c3_Err | |
| 2177 | 2372 | } | |
| @@ -2179,8 +2374,6 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2179 | 2374 | }) | |
| 2180 | 2375 | } | |
| 2181 | 2376 | ||
| 2182 | - | // Compare is a branch against the default branch: the commits it is ahead by, | |
| 2183 | - | // its diff when the API can give one, and the form that opens a merge request. | |
| 2184 | 2377 | func Compare(l Layout, p ComparePage) templ.Component { | |
| 2185 | 2378 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2186 | 2379 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| @@ -2197,12 +2390,12 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2197 | 2390 | }() | |
| 2198 | 2391 | } | |
| 2199 | 2392 | ctx = templ.InitializeContext(ctx) | |
| 2200 | - | templ_7745c5c3_Var110 := templ.GetChildren(ctx) | |
| 2201 | - | if templ_7745c5c3_Var110 == nil { | |
| 2202 | - | templ_7745c5c3_Var110 = templ.NopComponent | |
| 2393 | + | templ_7745c5c3_Var116 := templ.GetChildren(ctx) | |
| 2394 | + | if templ_7745c5c3_Var116 == nil { | |
| 2395 | + | templ_7745c5c3_Var116 = templ.NopComponent | |
| 2203 | 2396 | } | |
| 2204 | 2397 | ctx = templ.ClearChildren(ctx) | |
| 2205 | - | templ_7745c5c3_Var111 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2398 | + | templ_7745c5c3_Var117 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2206 | 2399 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 2207 | 2400 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 2208 | 2401 | if !templ_7745c5c3_IsBuffer { | |
| @@ -2214,263 +2407,263 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2214 | 2407 | }() | |
| 2215 | 2408 | } | |
| 2216 | 2409 | ctx = templ.InitializeContext(ctx) | |
| 2217 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "<header class=\"show mr-h\"><h1>") | |
| 2410 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<header class=\"show mr-h\"><h1>") | |
| 2218 | 2411 | if templ_7745c5c3_Err != nil { | |
| 2219 | 2412 | return templ_7745c5c3_Err | |
| 2220 | 2413 | } | |
| 2221 | - | var templ_7745c5c3_Var112 string | |
| 2222 | - | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2414 | + | var templ_7745c5c3_Var118 string | |
| 2415 | + | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2223 | 2416 | if templ_7745c5c3_Err != nil { | |
| 2224 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 17} | |
| 2417 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 456, Col: 17} | |
| 2225 | 2418 | } | |
| 2226 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | |
| 2419 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | |
| 2227 | 2420 | if templ_7745c5c3_Err != nil { | |
| 2228 | 2421 | return templ_7745c5c3_Err | |
| 2229 | 2422 | } | |
| 2230 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, " → ") | |
| 2423 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " → ") | |
| 2231 | 2424 | if templ_7745c5c3_Err != nil { | |
| 2232 | 2425 | return templ_7745c5c3_Err | |
| 2233 | 2426 | } | |
| 2234 | - | var templ_7745c5c3_Var113 string | |
| 2235 | - | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2427 | + | var templ_7745c5c3_Var119 string | |
| 2428 | + | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2236 | 2429 | if templ_7745c5c3_Err != nil { | |
| 2237 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 34} | |
| 2430 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 456, Col: 34} | |
| 2238 | 2431 | } | |
| 2239 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | |
| 2432 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | |
| 2240 | 2433 | if templ_7745c5c3_Err != nil { | |
| 2241 | 2434 | return templ_7745c5c3_Err | |
| 2242 | 2435 | } | |
| 2243 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") | |
| 2436 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") | |
| 2244 | 2437 | if templ_7745c5c3_Err != nil { | |
| 2245 | 2438 | return templ_7745c5c3_Err | |
| 2246 | 2439 | } | |
| 2247 | - | var templ_7745c5c3_Var114 templ.SafeURL | |
| 2248 | - | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 2440 | + | var templ_7745c5c3_Var120 templ.SafeURL | |
| 2441 | + | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 2249 | 2442 | if templ_7745c5c3_Err != nil { | |
| 2250 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 45} | |
| 2443 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 45} | |
| 2251 | 2444 | } | |
| 2252 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) | |
| 2445 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | |
| 2253 | 2446 | if templ_7745c5c3_Err != nil { | |
| 2254 | 2447 | return templ_7745c5c3_Err | |
| 2255 | 2448 | } | |
| 2256 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "\">") | |
| 2449 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "\">") | |
| 2257 | 2450 | if templ_7745c5c3_Err != nil { | |
| 2258 | 2451 | return templ_7745c5c3_Err | |
| 2259 | 2452 | } | |
| 2260 | - | var templ_7745c5c3_Var115 string | |
| 2261 | - | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2453 | + | var templ_7745c5c3_Var121 string | |
| 2454 | + | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2262 | 2455 | if templ_7745c5c3_Err != nil { | |
| 2263 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 58} | |
| 2456 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 58} | |
| 2264 | 2457 | } | |
| 2265 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | |
| 2458 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121)) | |
| 2266 | 2459 | if templ_7745c5c3_Err != nil { | |
| 2267 | 2460 | return templ_7745c5c3_Err | |
| 2268 | 2461 | } | |
| 2269 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</a></dd><dt>target</dt><dd><a href=\"") | |
| 2462 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</a></dd><dt>target</dt><dd><a href=\"") | |
| 2270 | 2463 | if templ_7745c5c3_Err != nil { | |
| 2271 | 2464 | return templ_7745c5c3_Err | |
| 2272 | 2465 | } | |
| 2273 | - | var templ_7745c5c3_Var116 templ.SafeURL | |
| 2274 | - | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | |
| 2466 | + | var templ_7745c5c3_Var122 templ.SafeURL | |
| 2467 | + | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | |
| 2275 | 2468 | if templ_7745c5c3_Err != nil { | |
| 2276 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 45} | |
| 2469 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 461, Col: 45} | |
| 2277 | 2470 | } | |
| 2278 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) | |
| 2471 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122)) | |
| 2279 | 2472 | if templ_7745c5c3_Err != nil { | |
| 2280 | 2473 | return templ_7745c5c3_Err | |
| 2281 | 2474 | } | |
| 2282 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "\">") | |
| 2475 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "\">") | |
| 2283 | 2476 | if templ_7745c5c3_Err != nil { | |
| 2284 | 2477 | return templ_7745c5c3_Err | |
| 2285 | 2478 | } | |
| 2286 | - | var templ_7745c5c3_Var117 string | |
| 2287 | - | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2479 | + | var templ_7745c5c3_Var123 string | |
| 2480 | + | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2288 | 2481 | if templ_7745c5c3_Err != nil { | |
| 2289 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 58} | |
| 2482 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 461, Col: 58} | |
| 2290 | 2483 | } | |
| 2291 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | |
| 2484 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123)) | |
| 2292 | 2485 | if templ_7745c5c3_Err != nil { | |
| 2293 | 2486 | return templ_7745c5c3_Err | |
| 2294 | 2487 | } | |
| 2295 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</a></dd><dt>ahead</dt><dd>") | |
| 2488 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "</a></dd><dt>ahead</dt><dd>") | |
| 2296 | 2489 | if templ_7745c5c3_Err != nil { | |
| 2297 | 2490 | return templ_7745c5c3_Err | |
| 2298 | 2491 | } | |
| 2299 | - | var templ_7745c5c3_Var118 string | |
| 2300 | - | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |
| 2492 | + | var templ_7745c5c3_Var124 string | |
| 2493 | + | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |
| 2301 | 2494 | if templ_7745c5c3_Err != nil { | |
| 2302 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 21} | |
| 2495 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 463, Col: 21} | |
| 2303 | 2496 | } | |
| 2304 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | |
| 2497 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124)) | |
| 2305 | 2498 | if templ_7745c5c3_Err != nil { | |
| 2306 | 2499 | return templ_7745c5c3_Err | |
| 2307 | 2500 | } | |
| 2308 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</dd></dl></header>") | |
| 2501 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "</dd></dl></header>") | |
| 2309 | 2502 | if templ_7745c5c3_Err != nil { | |
| 2310 | 2503 | return templ_7745c5c3_Err | |
| 2311 | 2504 | } | |
| 2312 | 2505 | if p.Note != "" { | |
| 2313 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">") | |
| 2506 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "<p class=\"notice\">") | |
| 2314 | 2507 | if templ_7745c5c3_Err != nil { | |
| 2315 | 2508 | return templ_7745c5c3_Err | |
| 2316 | 2509 | } | |
| 2317 | - | var templ_7745c5c3_Var119 string | |
| 2318 | - | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 2510 | + | var templ_7745c5c3_Var125 string | |
| 2511 | + | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 2319 | 2512 | if templ_7745c5c3_Err != nil { | |
| 2320 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 29} | |
| 2513 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 467, Col: 29} | |
| 2321 | 2514 | } | |
| 2322 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | |
| 2515 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) | |
| 2323 | 2516 | if templ_7745c5c3_Err != nil { | |
| 2324 | 2517 | return templ_7745c5c3_Err | |
| 2325 | 2518 | } | |
| 2326 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "</p>") | |
| 2519 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "</p>") | |
| 2327 | 2520 | if templ_7745c5c3_Err != nil { | |
| 2328 | 2521 | return templ_7745c5c3_Err | |
| 2329 | 2522 | } | |
| 2330 | 2523 | } | |
| 2331 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, " ") | |
| 2524 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, " ") | |
| 2332 | 2525 | if templ_7745c5c3_Err != nil { | |
| 2333 | 2526 | return templ_7745c5c3_Err | |
| 2334 | 2527 | } | |
| 2335 | 2528 | if p.Existing != nil { | |
| 2336 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") | |
| 2529 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") | |
| 2337 | 2530 | if templ_7745c5c3_Err != nil { | |
| 2338 | 2531 | return templ_7745c5c3_Err | |
| 2339 | 2532 | } | |
| 2340 | - | var templ_7745c5c3_Var120 templ.SafeURL | |
| 2341 | - | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | |
| 2533 | + | var templ_7745c5c3_Var126 templ.SafeURL | |
| 2534 | + | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | |
| 2342 | 2535 | if templ_7745c5c3_Err != nil { | |
| 2343 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 114} | |
| 2536 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 114} | |
| 2344 | 2537 | } | |
| 2345 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | |
| 2538 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | |
| 2346 | 2539 | if templ_7745c5c3_Err != nil { | |
| 2347 | 2540 | return templ_7745c5c3_Err | |
| 2348 | 2541 | } | |
| 2349 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\">") | |
| 2542 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "\">") | |
| 2350 | 2543 | if templ_7745c5c3_Err != nil { | |
| 2351 | 2544 | return templ_7745c5c3_Err | |
| 2352 | 2545 | } | |
| 2353 | - | var templ_7745c5c3_Var121 string | |
| 2354 | - | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | |
| 2546 | + | var templ_7745c5c3_Var127 string | |
| 2547 | + | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | |
| 2355 | 2548 | if templ_7745c5c3_Err != nil { | |
| 2356 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 135} | |
| 2549 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 135} | |
| 2357 | 2550 | } | |
| 2358 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121)) | |
| 2551 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127)) | |
| 2359 | 2552 | if templ_7745c5c3_Err != nil { | |
| 2360 | 2553 | return templ_7745c5c3_Err | |
| 2361 | 2554 | } | |
| 2362 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</a></p>") | |
| 2555 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</a></p>") | |
| 2363 | 2556 | if templ_7745c5c3_Err != nil { | |
| 2364 | 2557 | return templ_7745c5c3_Err | |
| 2365 | 2558 | } | |
| 2366 | 2559 | } | |
| 2367 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " ") | |
| 2560 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, " ") | |
| 2368 | 2561 | if templ_7745c5c3_Err != nil { | |
| 2369 | 2562 | return templ_7745c5c3_Err | |
| 2370 | 2563 | } | |
| 2371 | 2564 | if p.Create != nil { | |
| 2372 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<form class=\"cform create\" method=\"post\" action=\"") | |
| 2565 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "<form class=\"cform create\" method=\"post\" action=\"") | |
| 2373 | 2566 | if templ_7745c5c3_Err != nil { | |
| 2374 | 2567 | return templ_7745c5c3_Err | |
| 2375 | 2568 | } | |
| 2376 | - | var templ_7745c5c3_Var122 templ.SafeURL | |
| 2377 | - | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | |
| 2569 | + | var templ_7745c5c3_Var128 templ.SafeURL | |
| 2570 | + | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | |
| 2378 | 2571 | if templ_7745c5c3_Err != nil { | |
| 2379 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 83} | |
| 2572 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 473, Col: 83} | |
| 2380 | 2573 | } | |
| 2381 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122)) | |
| 2574 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128)) | |
| 2382 | 2575 | if templ_7745c5c3_Err != nil { | |
| 2383 | 2576 | return templ_7745c5c3_Err | |
| 2384 | 2577 | } | |
| 2385 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\"><input type=\"hidden\" name=\"source\" value=\"") | |
| 2578 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "\"><input type=\"hidden\" name=\"source\" value=\"") | |
| 2386 | 2579 | if templ_7745c5c3_Err != nil { | |
| 2387 | 2580 | return templ_7745c5c3_Err | |
| 2388 | 2581 | } | |
| 2389 | - | var templ_7745c5c3_Var123 string | |
| 2390 | - | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | |
| 2582 | + | var templ_7745c5c3_Var129 string | |
| 2583 | + | templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | |
| 2391 | 2584 | if templ_7745c5c3_Err != nil { | |
| 2392 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 436, Col: 62} | |
| 2585 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 474, Col: 62} | |
| 2393 | 2586 | } | |
| 2394 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) | |
| 2587 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129) | |
| 2395 | 2588 | if templ_7745c5c3_Err != nil { | |
| 2396 | 2589 | return templ_7745c5c3_Err | |
| 2397 | 2590 | } | |
| 2398 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "\"> <input type=\"hidden\" name=\"target\" value=\"") | |
| 2591 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\"> <input type=\"hidden\" name=\"target\" value=\"") | |
| 2399 | 2592 | if templ_7745c5c3_Err != nil { | |
| 2400 | 2593 | return templ_7745c5c3_Err | |
| 2401 | 2594 | } | |
| 2402 | - | var templ_7745c5c3_Var124 string | |
| 2403 | - | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | |
| 2595 | + | var templ_7745c5c3_Var130 string | |
| 2596 | + | templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | |
| 2404 | 2597 | if templ_7745c5c3_Err != nil { | |
| 2405 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 437, Col: 62} | |
| 2598 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 475, Col: 62} | |
| 2406 | 2599 | } | |
| 2407 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124) | |
| 2600 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130) | |
| 2408 | 2601 | if templ_7745c5c3_Err != nil { | |
| 2409 | 2602 | return templ_7745c5c3_Err | |
| 2410 | 2603 | } | |
| 2411 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") | |
| 2604 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") | |
| 2412 | 2605 | if templ_7745c5c3_Err != nil { | |
| 2413 | 2606 | return templ_7745c5c3_Err | |
| 2414 | 2607 | } | |
| 2415 | - | var templ_7745c5c3_Var125 string | |
| 2416 | - | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | |
| 2608 | + | var templ_7745c5c3_Var131 string | |
| 2609 | + | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | |
| 2417 | 2610 | if templ_7745c5c3_Err != nil { | |
| 2418 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 439, Col: 97} | |
| 2611 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 477, Col: 97} | |
| 2419 | 2612 | } | |
| 2420 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var125) | |
| 2613 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131) | |
| 2421 | 2614 | if templ_7745c5c3_Err != nil { | |
| 2422 | 2615 | return templ_7745c5c3_Err | |
| 2423 | 2616 | } | |
| 2424 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>") | |
| 2617 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>") | |
| 2425 | 2618 | if templ_7745c5c3_Err != nil { | |
| 2426 | 2619 | return templ_7745c5c3_Err | |
| 2427 | 2620 | } | |
| 2428 | 2621 | } | |
| 2429 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, " ") | |
| 2622 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, " ") | |
| 2430 | 2623 | if templ_7745c5c3_Err != nil { | |
| 2431 | 2624 | return templ_7745c5c3_Err | |
| 2432 | 2625 | } | |
| 2433 | 2626 | if p.HasDiff { | |
| 2434 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 2627 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 2435 | 2628 | if templ_7745c5c3_Err != nil { | |
| 2436 | 2629 | return templ_7745c5c3_Err | |
| 2437 | 2630 | } | |
| 2438 | - | var templ_7745c5c3_Var126 string | |
| 2439 | - | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2631 | + | var templ_7745c5c3_Var132 string | |
| 2632 | + | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2440 | 2633 | if templ_7745c5c3_Err != nil { | |
| 2441 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 57} | |
| 2634 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 57} | |
| 2442 | 2635 | } | |
| 2443 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | |
| 2636 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | |
| 2444 | 2637 | if templ_7745c5c3_Err != nil { | |
| 2445 | 2638 | return templ_7745c5c3_Err | |
| 2446 | 2639 | } | |
| 2447 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " · <span class=\"plus\">+") | |
| 2640 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, " · <span class=\"plus\">+") | |
| 2448 | 2641 | if templ_7745c5c3_Err != nil { | |
| 2449 | 2642 | return templ_7745c5c3_Err | |
| 2450 | 2643 | } | |
| 2451 | - | var templ_7745c5c3_Var127 string | |
| 2452 | - | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2644 | + | var templ_7745c5c3_Var133 string | |
| 2645 | + | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2453 | 2646 | if templ_7745c5c3_Err != nil { | |
| 2454 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 97} | |
| 2647 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 97} | |
| 2455 | 2648 | } | |
| 2456 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127)) | |
| 2649 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | |
| 2457 | 2650 | if templ_7745c5c3_Err != nil { | |
| 2458 | 2651 | return templ_7745c5c3_Err | |
| 2459 | 2652 | } | |
| 2460 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</span> <span class=\"minus\">-") | |
| 2653 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</span> <span class=\"minus\">-") | |
| 2461 | 2654 | if templ_7745c5c3_Err != nil { | |
| 2462 | 2655 | return templ_7745c5c3_Err | |
| 2463 | 2656 | } | |
| 2464 | - | var templ_7745c5c3_Var128 string | |
| 2465 | - | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2657 | + | var templ_7745c5c3_Var134 string | |
| 2658 | + | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2466 | 2659 | if templ_7745c5c3_Err != nil { | |
| 2467 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 142} | |
| 2660 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 142} | |
| 2468 | 2661 | } | |
| 2469 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128)) | |
| 2662 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | |
| 2470 | 2663 | if templ_7745c5c3_Err != nil { | |
| 2471 | 2664 | return templ_7745c5c3_Err | |
| 2472 | 2665 | } | |
| 2473 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</span></span> <span class=\"grow\"></span>") | |
| 2666 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</span></span> <span class=\"grow\"></span>") | |
| 2474 | 2667 | if templ_7745c5c3_Err != nil { | |
| 2475 | 2668 | return templ_7745c5c3_Err | |
| 2476 | 2669 | } | |
| @@ -2486,64 +2679,64 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2486 | 2679 | if templ_7745c5c3_Err != nil { | |
| 2487 | 2680 | return templ_7745c5c3_Err | |
| 2488 | 2681 | } | |
| 2489 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</div>") | |
| 2682 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</div>") | |
| 2490 | 2683 | if templ_7745c5c3_Err != nil { | |
| 2491 | 2684 | return templ_7745c5c3_Err | |
| 2492 | 2685 | } | |
| 2493 | 2686 | if p.Invisible { | |
| 2494 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 2687 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 2495 | 2688 | if templ_7745c5c3_Err != nil { | |
| 2496 | 2689 | return templ_7745c5c3_Err | |
| 2497 | 2690 | } | |
| 2498 | 2691 | } | |
| 2499 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " <div id=\"diffs\"") | |
| 2692 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, " <div id=\"diffs\"") | |
| 2500 | 2693 | if templ_7745c5c3_Err != nil { | |
| 2501 | 2694 | return templ_7745c5c3_Err | |
| 2502 | 2695 | } | |
| 2503 | 2696 | if p.Both { | |
| 2504 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, " data-diff=\"") | |
| 2697 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, " data-diff=\"") | |
| 2505 | 2698 | if templ_7745c5c3_Err != nil { | |
| 2506 | 2699 | return templ_7745c5c3_Err | |
| 2507 | 2700 | } | |
| 2508 | - | var templ_7745c5c3_Var129 string | |
| 2509 | - | templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2701 | + | var templ_7745c5c3_Var135 string | |
| 2702 | + | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2510 | 2703 | if templ_7745c5c3_Err != nil { | |
| 2511 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 464, Col: 23} | |
| 2704 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 502, Col: 23} | |
| 2512 | 2705 | } | |
| 2513 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129) | |
| 2706 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135) | |
| 2514 | 2707 | if templ_7745c5c3_Err != nil { | |
| 2515 | 2708 | return templ_7745c5c3_Err | |
| 2516 | 2709 | } | |
| 2517 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "\"") | |
| 2710 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "\"") | |
| 2518 | 2711 | if templ_7745c5c3_Err != nil { | |
| 2519 | 2712 | return templ_7745c5c3_Err | |
| 2520 | 2713 | } | |
| 2521 | 2714 | } else { | |
| 2522 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, " data-view=\"") | |
| 2715 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, " data-view=\"") | |
| 2523 | 2716 | if templ_7745c5c3_Err != nil { | |
| 2524 | 2717 | return templ_7745c5c3_Err | |
| 2525 | 2718 | } | |
| 2526 | - | var templ_7745c5c3_Var130 string | |
| 2527 | - | templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2719 | + | var templ_7745c5c3_Var136 string | |
| 2720 | + | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2528 | 2721 | if templ_7745c5c3_Err != nil { | |
| 2529 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 466, Col: 23} | |
| 2722 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 504, Col: 23} | |
| 2530 | 2723 | } | |
| 2531 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130) | |
| 2724 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136) | |
| 2532 | 2725 | if templ_7745c5c3_Err != nil { | |
| 2533 | 2726 | return templ_7745c5c3_Err | |
| 2534 | 2727 | } | |
| 2535 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "\"") | |
| 2728 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "\"") | |
| 2536 | 2729 | if templ_7745c5c3_Err != nil { | |
| 2537 | 2730 | return templ_7745c5c3_Err | |
| 2538 | 2731 | } | |
| 2539 | 2732 | } | |
| 2540 | 2733 | if p.Both && !p.Pinned { | |
| 2541 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 2734 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 2542 | 2735 | if templ_7745c5c3_Err != nil { | |
| 2543 | 2736 | return templ_7745c5c3_Err | |
| 2544 | 2737 | } | |
| 2545 | 2738 | } | |
| 2546 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, ">") | |
| 2739 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, ">") | |
| 2547 | 2740 | if templ_7745c5c3_Err != nil { | |
| 2548 | 2741 | return templ_7745c5c3_Err | |
| 2549 | 2742 | } | |
| @@ -2553,14 +2746,14 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2553 | 2746 | return templ_7745c5c3_Err | |
| 2554 | 2747 | } | |
| 2555 | 2748 | } | |
| 2556 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</div>") | |
| 2749 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "</div>") | |
| 2557 | 2750 | if templ_7745c5c3_Err != nil { | |
| 2558 | 2751 | return templ_7745c5c3_Err | |
| 2559 | 2752 | } | |
| 2560 | 2753 | } | |
| 2561 | 2754 | return nil | |
| 2562 | 2755 | }) | |
| 2563 | - | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var111), templ_7745c5c3_Buffer) | |
| 2756 | + | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var117), templ_7745c5c3_Buffer) | |
| 2564 | 2757 | if templ_7745c5c3_Err != nil { | |
| 2565 | 2758 | return templ_7745c5c3_Err | |
| 2566 | 2759 | } | |
| @@ -2584,173 +2777,173 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2584 | 2777 | }() | |
| 2585 | 2778 | } | |
| 2586 | 2779 | ctx = templ.InitializeContext(ctx) | |
| 2587 | - | templ_7745c5c3_Var131 := templ.GetChildren(ctx) | |
| 2588 | - | if templ_7745c5c3_Var131 == nil { | |
| 2589 | - | templ_7745c5c3_Var131 = templ.NopComponent | |
| 2780 | + | templ_7745c5c3_Var137 := templ.GetChildren(ctx) | |
| 2781 | + | if templ_7745c5c3_Var137 == nil { | |
| 2782 | + | templ_7745c5c3_Var137 = templ.NopComponent | |
| 2590 | 2783 | } | |
| 2591 | 2784 | ctx = templ.ClearChildren(ctx) | |
| 2592 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">") | |
| 2785 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">") | |
| 2593 | 2786 | if templ_7745c5c3_Err != nil { | |
| 2594 | 2787 | return templ_7745c5c3_Err | |
| 2595 | 2788 | } | |
| 2596 | - | var templ_7745c5c3_Var132 string | |
| 2597 | - | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 2789 | + | var templ_7745c5c3_Var138 string | |
| 2790 | + | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 2598 | 2791 | if templ_7745c5c3_Err != nil { | |
| 2599 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 67} | |
| 2792 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 520, Col: 67} | |
| 2600 | 2793 | } | |
| 2601 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | |
| 2794 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) | |
| 2602 | 2795 | if templ_7745c5c3_Err != nil { | |
| 2603 | 2796 | return templ_7745c5c3_Err | |
| 2604 | 2797 | } | |
| 2605 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</span></h2><ul class=\"pcommits\">") | |
| 2798 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</span></h2><ul class=\"pcommits\">") | |
| 2606 | 2799 | if templ_7745c5c3_Err != nil { | |
| 2607 | 2800 | return templ_7745c5c3_Err | |
| 2608 | 2801 | } | |
| 2609 | 2802 | for _, c := range p.Ahead { | |
| 2610 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "<li><a href=\"") | |
| 2803 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "<li><a href=\"") | |
| 2611 | 2804 | if templ_7745c5c3_Err != nil { | |
| 2612 | 2805 | return templ_7745c5c3_Err | |
| 2613 | 2806 | } | |
| 2614 | - | var templ_7745c5c3_Var133 templ.SafeURL | |
| 2615 | - | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 2807 | + | var templ_7745c5c3_Var139 templ.SafeURL | |
| 2808 | + | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 2616 | 2809 | if templ_7745c5c3_Err != nil { | |
| 2617 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 37} | |
| 2810 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 524, Col: 37} | |
| 2618 | 2811 | } | |
| 2619 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | |
| 2812 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | |
| 2620 | 2813 | if templ_7745c5c3_Err != nil { | |
| 2621 | 2814 | return templ_7745c5c3_Err | |
| 2622 | 2815 | } | |
| 2623 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\">") | |
| 2816 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "\">") | |
| 2624 | 2817 | if templ_7745c5c3_Err != nil { | |
| 2625 | 2818 | return templ_7745c5c3_Err | |
| 2626 | 2819 | } | |
| 2627 | - | var templ_7745c5c3_Var134 string | |
| 2628 | - | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2820 | + | var templ_7745c5c3_Var140 string | |
| 2821 | + | templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2629 | 2822 | if templ_7745c5c3_Err != nil { | |
| 2630 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 49} | |
| 2823 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 524, Col: 49} | |
| 2631 | 2824 | } | |
| 2632 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | |
| 2825 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140)) | |
| 2633 | 2826 | if templ_7745c5c3_Err != nil { | |
| 2634 | 2827 | return templ_7745c5c3_Err | |
| 2635 | 2828 | } | |
| 2636 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</a> <span class=\"s\">") | |
| 2829 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "</a> <span class=\"s\">") | |
| 2637 | 2830 | if templ_7745c5c3_Err != nil { | |
| 2638 | 2831 | return templ_7745c5c3_Err | |
| 2639 | 2832 | } | |
| 2640 | - | var templ_7745c5c3_Var135 string | |
| 2641 | - | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2833 | + | var templ_7745c5c3_Var141 string | |
| 2834 | + | templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2642 | 2835 | if templ_7745c5c3_Err != nil { | |
| 2643 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 32} | |
| 2836 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 32} | |
| 2644 | 2837 | } | |
| 2645 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) | |
| 2838 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141)) | |
| 2646 | 2839 | if templ_7745c5c3_Err != nil { | |
| 2647 | 2840 | return templ_7745c5c3_Err | |
| 2648 | 2841 | } | |
| 2649 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</span> <span class=\"mute\">") | |
| 2842 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</span> <span class=\"mute\">") | |
| 2650 | 2843 | if templ_7745c5c3_Err != nil { | |
| 2651 | 2844 | return templ_7745c5c3_Err | |
| 2652 | 2845 | } | |
| 2653 | - | var templ_7745c5c3_Var136 string | |
| 2654 | - | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2846 | + | var templ_7745c5c3_Var142 string | |
| 2847 | + | templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2655 | 2848 | if templ_7745c5c3_Err != nil { | |
| 2656 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 16} | |
| 2849 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 527, Col: 16} | |
| 2657 | 2850 | } | |
| 2658 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136)) | |
| 2851 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142)) | |
| 2659 | 2852 | if templ_7745c5c3_Err != nil { | |
| 2660 | 2853 | return templ_7745c5c3_Err | |
| 2661 | 2854 | } | |
| 2662 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " ") | |
| 2855 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, " ") | |
| 2663 | 2856 | if templ_7745c5c3_Err != nil { | |
| 2664 | 2857 | return templ_7745c5c3_Err | |
| 2665 | 2858 | } | |
| 2666 | 2859 | if c.WhenAt != "" { | |
| 2667 | - | var templ_7745c5c3_Var137 string | |
| 2668 | - | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2860 | + | var templ_7745c5c3_Var143 string | |
| 2861 | + | templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2669 | 2862 | if templ_7745c5c3_Err != nil { | |
| 2670 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 15} | |
| 2863 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 15} | |
| 2671 | 2864 | } | |
| 2672 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137)) | |
| 2865 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143)) | |
| 2673 | 2866 | if templ_7745c5c3_Err != nil { | |
| 2674 | 2867 | return templ_7745c5c3_Err | |
| 2675 | 2868 | } | |
| 2676 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<time datetime=\"") | |
| 2869 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<time datetime=\"") | |
| 2677 | 2870 | if templ_7745c5c3_Err != nil { | |
| 2678 | 2871 | return templ_7745c5c3_Err | |
| 2679 | 2872 | } | |
| 2680 | - | var templ_7745c5c3_Var138 string | |
| 2681 | - | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2873 | + | var templ_7745c5c3_Var144 string | |
| 2874 | + | templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2682 | 2875 | if templ_7745c5c3_Err != nil { | |
| 2683 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 42} | |
| 2876 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 42} | |
| 2684 | 2877 | } | |
| 2685 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138) | |
| 2878 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144) | |
| 2686 | 2879 | if templ_7745c5c3_Err != nil { | |
| 2687 | 2880 | return templ_7745c5c3_Err | |
| 2688 | 2881 | } | |
| 2689 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\">") | |
| 2882 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "\">") | |
| 2690 | 2883 | if templ_7745c5c3_Err != nil { | |
| 2691 | 2884 | return templ_7745c5c3_Err | |
| 2692 | 2885 | } | |
| 2693 | - | var templ_7745c5c3_Var139 string | |
| 2694 | - | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2886 | + | var templ_7745c5c3_Var145 string | |
| 2887 | + | templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2695 | 2888 | if templ_7745c5c3_Err != nil { | |
| 2696 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 53} | |
| 2889 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 53} | |
| 2697 | 2890 | } | |
| 2698 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | |
| 2891 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145)) | |
| 2699 | 2892 | if templ_7745c5c3_Err != nil { | |
| 2700 | 2893 | return templ_7745c5c3_Err | |
| 2701 | 2894 | } | |
| 2702 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</time>") | |
| 2895 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "</time>") | |
| 2703 | 2896 | if templ_7745c5c3_Err != nil { | |
| 2704 | 2897 | return templ_7745c5c3_Err | |
| 2705 | 2898 | } | |
| 2706 | 2899 | } | |
| 2707 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></li>") | |
| 2900 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</span></li>") | |
| 2708 | 2901 | if templ_7745c5c3_Err != nil { | |
| 2709 | 2902 | return templ_7745c5c3_Err | |
| 2710 | 2903 | } | |
| 2711 | 2904 | } | |
| 2712 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</ul>") | |
| 2905 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</ul>") | |
| 2713 | 2906 | if templ_7745c5c3_Err != nil { | |
| 2714 | 2907 | return templ_7745c5c3_Err | |
| 2715 | 2908 | } | |
| 2716 | 2909 | if len(p.Ahead) == 0 { | |
| 2717 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<p class=\"dim\">no commit ahead of ") | |
| 2910 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "<p class=\"dim\">no commit ahead of ") | |
| 2718 | 2911 | if templ_7745c5c3_Err != nil { | |
| 2719 | 2912 | return templ_7745c5c3_Err | |
| 2720 | 2913 | } | |
| 2721 | - | var templ_7745c5c3_Var140 string | |
| 2722 | - | templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2914 | + | var templ_7745c5c3_Var146 string | |
| 2915 | + | templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2723 | 2916 | if templ_7745c5c3_Err != nil { | |
| 2724 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 498, Col: 47} | |
| 2917 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 536, Col: 47} | |
| 2725 | 2918 | } | |
| 2726 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140)) | |
| 2919 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146)) | |
| 2727 | 2920 | if templ_7745c5c3_Err != nil { | |
| 2728 | 2921 | return templ_7745c5c3_Err | |
| 2729 | 2922 | } | |
| 2730 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, ".</p>") | |
| 2923 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, ".</p>") | |
| 2731 | 2924 | if templ_7745c5c3_Err != nil { | |
| 2732 | 2925 | return templ_7745c5c3_Err | |
| 2733 | 2926 | } | |
| 2734 | 2927 | } | |
| 2735 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>") | |
| 2928 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "</section>") | |
| 2736 | 2929 | if templ_7745c5c3_Err != nil { | |
| 2737 | 2930 | return templ_7745c5c3_Err | |
| 2738 | 2931 | } | |
| 2739 | 2932 | if p.HasDiff { | |
| 2740 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">") | |
| 2933 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">") | |
| 2741 | 2934 | if templ_7745c5c3_Err != nil { | |
| 2742 | 2935 | return templ_7745c5c3_Err | |
| 2743 | 2936 | } | |
| 2744 | - | var templ_7745c5c3_Var141 string | |
| 2745 | - | templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 2937 | + | var templ_7745c5c3_Var147 string | |
| 2938 | + | templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 2746 | 2939 | if templ_7745c5c3_Err != nil { | |
| 2747 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 503, Col: 66} | |
| 2940 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 541, Col: 66} | |
| 2748 | 2941 | } | |
| 2749 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141)) | |
| 2942 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147)) | |
| 2750 | 2943 | if templ_7745c5c3_Err != nil { | |
| 2751 | 2944 | return templ_7745c5c3_Err | |
| 2752 | 2945 | } | |
| 2753 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</span></h2>") | |
| 2946 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "</span></h2>") | |
| 2754 | 2947 | if templ_7745c5c3_Err != nil { | |
| 2755 | 2948 | return templ_7745c5c3_Err | |
| 2756 | 2949 | } | |
| @@ -2758,12 +2951,12 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2758 | 2951 | if templ_7745c5c3_Err != nil { | |
| 2759 | 2952 | return templ_7745c5c3_Err | |
| 2760 | 2953 | } | |
| 2761 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</section>") | |
| 2954 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "</section>") | |
| 2762 | 2955 | if templ_7745c5c3_Err != nil { | |
| 2763 | 2956 | return templ_7745c5c3_Err | |
| 2764 | 2957 | } | |
| 2765 | 2958 | } | |
| 2766 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>") | |
| 2959 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>") | |
| 2767 | 2960 | if templ_7745c5c3_Err != nil { | |
| 2768 | 2961 | return templ_7745c5c3_Err | |
| 2769 | 2962 | } | |
| @@ -2771,7 +2964,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2771 | 2964 | if templ_7745c5c3_Err != nil { | |
| 2772 | 2965 | return templ_7745c5c3_Err | |
| 2773 | 2966 | } | |
| 2774 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "</section>") | |
| 2967 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "</section>") | |
| 2775 | 2968 | if templ_7745c5c3_Err != nil { | |
| 2776 | 2969 | return templ_7745c5c3_Err | |
| 2777 | 2970 | } | |
M internal/adapters/web/overlays.templ +8 -2
| @@ -4,8 +4,9 @@ package web | |||
| 4 | 4 | ||
| 5 | 5 | import "strings" | |
| 6 | 6 | ||
| 7 | - | // OverlayHelp is the keyboard map panel. | |
| 8 | - | templ OverlayHelp() { | |
| 7 | + | // OverlayHelp is the keyboard map panel; the keys of the merge request page | |
| 8 | + | // show on that page only. | |
| 9 | + | templ OverlayHelp(mr bool) { | |
| 9 | 10 | <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()"> | |
| 10 | 11 | <div class="panel-head"><h2>Keyboard shortcuts</h2><button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd> close</button></div> | |
| 11 | 12 | <dl class="keys"> | |
| @@ -21,6 +22,11 @@ templ OverlayHelp() { | |||
| 21 | 22 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | |
| 22 | 23 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | |
| 23 | 24 | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | |
| 25 | + | if mr { | |
| 26 | + | <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div> | |
| 27 | + | <div><dt><kbd>M</kbd></dt><dd>merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges</dd></div> | |
| 28 | + | <div><dt><kbd>R</kbd></dt><dd>merge request: mark the head read</dd></div> | |
| 29 | + | } | |
| 24 | 30 | <div><dt><kbd>esc</kbd></dt><dd>close a dialog, leave a field</dd></div> | |
| 25 | 31 | </dl> | |
| 26 | 32 | </dialog> | |
M internal/adapters/web/overlays_templ.go +45 -34 generated file: collapsed
M internal/adapters/web/shell.templ +4 -1
| @@ -29,6 +29,9 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 29 | 29 | <script src={ l.Assets.Files } defer></script> | |
| 30 | 30 | <script src={ l.Assets.Passkey } defer></script> | |
| 31 | 31 | <script src={ l.Assets.Log } defer></script> | |
| 32 | + | if l.MR { | |
| 33 | + | <script src={ l.Assets.MR } defer></script> | |
| 34 | + | } | |
| 32 | 35 | </head> | |
| 33 | 36 | <body | |
| 34 | 37 | data-ref={ l.RefPath } | |
| @@ -57,7 +60,7 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 57 | 60 | </div> | |
| 58 | 61 | @Footer(l) | |
| 59 | 62 | @Hint(l) | |
| 60 | - | @OverlayHelp() | |
| 63 | + | @OverlayHelp(l.MR) | |
| 61 | 64 | @OverlayRefs(l) | |
| 62 | 65 | @OverlayCmd(l.Cmd) | |
| 63 | 66 | @Keys() | |
M internal/adapters/web/shell_templ.go +185 -162 generated file: collapsed
M internal/adapters/web/static/css/50-code.css +37 -11
| @@ -46,10 +46,18 @@ | |||
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | /* one file of a diff */ | |
| 49 | + | .dfile { --lnw: 4ch; } | |
| 50 | + | .lnw-2 { --lnw: 2ch; } .lnw-3 { --lnw: 3ch; } .lnw-4 { --lnw: 4ch; } .lnw-5 { --lnw: 5ch; } .lnw-6 { --lnw: 6ch; } .lnw-7 { --lnw: 7ch; } | |
| 49 | 51 | .dfile { | |
| 50 | - | margin-top: var(--s3); scroll-margin-top: calc(var(--hdr-h, 0px) + 56px); | |
| 52 | + | margin-top: var(--s3); scroll-margin-top: calc(var(--hdr-h, 0px) + var(--bar-h, 48px) + 8px); | |
| 53 | + | /* a file far from the screen is neither laid out nor painted: a diff of | |
| 54 | + | hundreds of files scrolls and switches layout like a short one. The | |
| 55 | + | browser remembers the last size it rendered (auto). */ | |
| 56 | + | content-visibility: auto; contain-intrinsic-size: auto 480px; | |
| 51 | 57 | & > summary { | |
| 52 | - | position: sticky; top: calc(var(--hdr-h, 0px) + 48px); z-index: 4; | |
| 58 | + | /* under the diff tools, whose height --bar-h is (two lines when they | |
| 59 | + | wrap: the merge request bar on a narrow screen) */ | |
| 60 | + | position: sticky; top: calc(var(--hdr-h, 0px) + var(--bar-h, 48px)); z-index: 4; | |
| 53 | 61 | background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--r) var(--r) 0 0; padding: var(--s2) var(--s3); | |
| 54 | 62 | font-family: var(--mono); font-size: var(--fs-s); | |
| 55 | 63 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; list-style: none; | |
| @@ -66,7 +74,9 @@ | |||
| 66 | 74 | width: max-content; min-width: 100%; font-family: var(--mono); | |
| 67 | 75 | font-size: var(--fs-code); line-height: var(--lh-code); | |
| 68 | 76 | & td { padding: 0; vertical-align: top; } | |
| 69 | - | & .ln { width: 6ch; min-width: 6ch; padding-right: 1ch; text-align: right; color: var(--ink-mute); user-select: none; } | |
| 77 | + | /* the gutter is as wide as the largest line number of the file (lnw-N on | |
| 78 | + | .dfile), plus 1ch on each side */ | |
| 79 | + | & .ln { width: calc(var(--lnw) + 2ch); min-width: calc(var(--lnw) + 2ch); padding: 0 1ch; text-align: right; color: var(--ink-mute); user-select: none; } | |
| 70 | 80 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | |
| 71 | 81 | & .tx { white-space: pre; } | |
| 72 | 82 | & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; } | |
| @@ -83,22 +93,38 @@ | |||
| 83 | 93 | the hunk header, which spans all columns); long lines wrap in their half */ | |
| 84 | 94 | .spl { | |
| 85 | 95 | width: 100%; table-layout: fixed; | |
| 86 | - | & .c-ln { width: 7ch; } & .c-m { width: 2.5ch; } | |
| 96 | + | & .c-ln { width: calc(var(--lnw) + 2ch); } & .c-m { width: 2.5ch; } | |
| 87 | 97 | & .tx, & .hunk td { white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 88 | 98 | & td.ln:nth-child(4) { border-left: 1px solid var(--border); } | |
| 89 | 99 | } | |
| 90 | 100 | #diffs[data-diff="unified"] .spl, #diffs[data-diff="split"] .uni { display: none; } | |
| 91 | 101 | ||
| 92 | - | /* diff table of contents in the rail */ | |
| 102 | + | /* diff table of contents in the rail: one line a file, the name first and | |
| 103 | + | its directory dimmed after it (cut from its start when long), the | |
| 104 | + | comments, the counts */ | |
| 93 | 105 | .files { | |
| 94 | - | font-size: var(--fs-s); | |
| 95 | - | & a { display: grid; grid-template-columns: 2ch minmax(0, 1fr) auto; column-gap: var(--s2); padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; text-decoration: none; font-family: var(--mono); } | |
| 106 | + | font-size: var(--fs-s); position: relative; /* the offsets of its links count from it (ermite.js) */ | |
| 107 | + | & li[hidden] { display: none; } | |
| 108 | + | & a { | |
| 109 | + | display: grid; grid-template-columns: 1.5ch minmax(0, 1fr) auto auto; column-gap: var(--s2); align-items: baseline; | |
| 110 | + | height: 26px; line-height: 26px; padding: 0 var(--s2); margin: 0 calc(-1 * var(--s2)); | |
| 111 | + | border-radius: 4px; border-left: 2px solid transparent; text-decoration: none; font-family: var(--mono); color: var(--ink); | |
| 112 | + | } | |
| 96 | 113 | & a:hover { background: var(--bg-raised); } | |
| 97 | 114 | & a:hover .p { text-decoration: underline; } | |
| 98 | - | & a[aria-current="true"] { background: var(--row); font-weight: bold; } | |
| 99 | - | & .p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; } | |
| 100 | - | & .n { color: var(--ink-dim); font-size: var(--fs-s); } | |
| 101 | - | & .b { grid-column: 2 / -1; color: var(--ink-mute); font-size: var(--fs-s); letter-spacing: .1ch; line-height: 1; padding-bottom: 4px; } | |
| 115 | + | & a[aria-current="true"] { background: var(--row); border-left-color: var(--ink-hi); } | |
| 116 | + | & a[aria-current="true"] .p { font-weight: bold; color: var(--ink-hi); } | |
| 117 | + | & .st { color: var(--ink-mute); font-weight: bold; } | |
| 118 | + | & .st[data-st="A"] { color: var(--teal); } | |
| 119 | + | & .st[data-st="D"] { color: var(--red); } | |
| 120 | + | & .nm { overflow: hidden; white-space: nowrap; display: flex; min-width: 0; } | |
| 121 | + | & .nm .p { flex: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; } | |
| 122 | + | /* cut from the start (rtl), the path itself kept in its order (bdi) */ | |
| 123 | + | & .nm .d { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; color: var(--ink-mute); padding-left: 1ch; } | |
| 124 | + | & .nm .d bdi { direction: ltr; } | |
| 125 | + | & .cn { font: 600 var(--fs-xs) / 16px var(--font); padding: 0 6px; border-radius: 999px; background: var(--ink-hi); color: var(--on-ink); align-self: center; } | |
| 126 | + | & .cn.done { background: var(--bg-strong); color: var(--ink-dim); } | |
| 127 | + | & .n { color: var(--ink-dim); font-size: var(--fs-xs); white-space: nowrap; } | |
| 102 | 128 | } | |
| 103 | 129 | ||
| 104 | 130 | @media (max-width: 700px) { | |
M internal/adapters/web/static/css/60-mr.css +91 -23
| @@ -18,41 +18,109 @@ | |||
| 18 | 18 | & .mute { font-weight: 400; font-family: var(--mono); } | |
| 19 | 19 | } | |
| 20 | 20 | ||
| 21 | - | /* merge block: first thing in the rail */ | |
| 22 | - | .decide { | |
| 23 | - | border-color: var(--border-strong) !important; | |
| 24 | - | & select { | |
| 25 | - | display: block; width: 100%; height: 36px; padding: 0 var(--s3); | |
| 26 | - | font: inherit; color: inherit; background: var(--bg-inset); | |
| 27 | - | border: 1px solid var(--border-strong); border-radius: var(--r-s); | |
| 21 | + | /* the bar above the diff of a merge request: the diff tools, the checks and | |
| 22 | + | the menus of the owner, on one line that follows the review */ | |
| 23 | + | /* on a narrow screen the bar takes two lines: the diff tools, then the | |
| 24 | + | checks and the menus; the file headers stick under both */ | |
| 25 | + | @media (max-width: 860px) { | |
| 26 | + | body:has(.mr-bar) { --bar-h: 96px; } | |
| 27 | + | .mr-bar .grow { flex-basis: 100%; height: 0; } | |
| 28 | + | } | |
| 29 | + | .mr-bar { | |
| 30 | + | gap: var(--s2); | |
| 31 | + | & .mr-ck { margin-left: 0; height: 32px; padding: 0 var(--s2); border-radius: var(--r-s); text-decoration: none; color: var(--ink-dim); font-size: var(--fs-s); } | |
| 32 | + | & .mr-ck:hover { background: var(--bg-raised); color: var(--ink-hi); } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | /* a menu of the bar: a <details> whose panel opens under its button */ | |
| 36 | + | .menu { | |
| 37 | + | position: relative; | |
| 38 | + | & > summary { list-style: none; } | |
| 39 | + | & > summary::-webkit-details-marker { display: none; } | |
| 40 | + | & > summary.primary::after { content: ""; width: 0; height: 0; margin-left: 2px; border: 4px solid transparent; border-top-color: currentColor; transform: translateY(2px); } | |
| 41 | + | & .menu-pop { | |
| 42 | + | position: absolute; right: 0; top: calc(100% + var(--s1)); z-index: 6; width: 320px; max-width: calc(100vw - 2 * var(--pad)); | |
| 43 | + | display: grid; gap: var(--s3); padding: var(--s3) var(--s4); | |
| 44 | + | background: var(--bg-surface); border: 1px solid var(--border-strong); border-radius: var(--r); box-shadow: 0 8px 24px rgb(0 0 0 / .18); | |
| 45 | + | font-size: var(--fs-s); | |
| 28 | 46 | } | |
| 29 | - | & .d-check { display: flex; gap: var(--s2); align-items: center; margin-top: var(--s2); color: var(--ink-dim); font-size: var(--fs-s); cursor: pointer; } | |
| 30 | - | & .d-check input { accent-color: var(--ink-hi); margin: 0; } | |
| 31 | - | & .d-btns { display: flex; gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap; } | |
| 32 | - | & .d-btns .primary { flex: 1; justify-content: center; } | |
| 33 | - | /* the terminal lines fold away: a reviewer who merges in the page does | |
| 34 | - | not need them in sight */ | |
| 35 | - | & .d-draft { font-size: var(--fs-s); color: var(--ink-dim); } | |
| 36 | - | & .d-or { font-size: var(--fs-s); } | |
| 37 | - | & .d-or > summary { cursor: pointer; list-style: none; } | |
| 47 | + | & .kv { margin: 0; } | |
| 48 | + | } | |
| 49 | + | .mmenu { | |
| 50 | + | & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; } | |
| 51 | + | & .d-modes label, & .d-check { | |
| 52 | + | display: flex; gap: var(--s2); align-items: center; padding: 4px var(--s2); margin: 0 calc(-1 * var(--s2)); | |
| 53 | + | border-radius: var(--r-s); color: var(--ink); cursor: pointer; | |
| 54 | + | } | |
| 55 | + | & .d-modes label:hover, & .d-check:hover { background: var(--bg-raised); } | |
| 56 | + | & .d-modes label:has(:checked) { background: var(--bg-raised); color: var(--ink-hi); font-weight: 600; } | |
| 57 | + | & .d-modes kbd, & .d-check kbd { margin-left: auto; } | |
| 58 | + | & input { accent-color: var(--ink-hi); margin: 0; } | |
| 59 | + | & .d-check { color: var(--ink-dim); } | |
| 60 | + | & .d-btns { display: flex; margin-top: var(--s1); } | |
| 61 | + | & .d-btns .btn { flex: 1; justify-content: center; } | |
| 62 | + | & .d-draft { color: var(--ink-dim); } | |
| 63 | + | } | |
| 64 | + | .omenu { | |
| 65 | + | & > summary { font-size: 18px; line-height: 1; } | |
| 66 | + | & .menu-pop { width: 280px; } | |
| 67 | + | & form .btn { width: 100%; justify-content: flex-start; } | |
| 68 | + | & .btn kbd { margin-left: auto; } | |
| 69 | + | & .d-or > summary { cursor: pointer; list-style: none; color: var(--ink-mute); } | |
| 38 | 70 | & .d-or > summary::-webkit-details-marker { display: none; } | |
| 39 | 71 | & .d-or > summary::before { content: "▸ "; } | |
| 40 | 72 | & .d-or[open] > summary::before { content: "▾ "; } | |
| 41 | 73 | & .d-or > summary:hover { color: var(--ink-hi); } | |
| 42 | 74 | & .d-or[open] > .clone { margin-top: var(--s2); } | |
| 43 | - | & .clone { margin-top: 0; } | |
| 44 | 75 | & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 45 | 76 | } | |
| 46 | 77 | ||
| 47 | - | /* the rail of a merge request follows the review: the merge block and the | |
| 48 | - | checks stay in reach at the end of a long diff. It scrolls on its own | |
| 49 | - | when it is taller than the window. Only on a two-column layout. */ | |
| 78 | + | /* the rail of a merge request: the list of files on top, which follows the | |
| 79 | + | review and scrolls on its own; checks, commits and settings scroll with | |
| 80 | + | the page. Only on a two-column layout. */ | |
| 50 | 81 | @media (min-width: 1081px) { | |
| 51 | - | .work.has-rail:has(#mr-checks) .rail { | |
| 52 | - | position: sticky; top: calc(var(--hdr-h, 0px) + var(--s4)); | |
| 82 | + | /* the rail runs the height of the content, so that its first block can | |
| 83 | + | stick all along the diff */ | |
| 84 | + | .work.has-rail:has(.r-files) { align-items: stretch; } | |
| 85 | + | .rail:has(.r-files) { align-self: stretch; } | |
| 86 | + | .rail .r-files { | |
| 87 | + | position: sticky; top: calc(var(--hdr-h, 0px) + var(--s4)); z-index: 3; | |
| 88 | + | display: flex; flex-direction: column; | |
| 53 | 89 | max-height: calc(100dvh - var(--hdr-h, 0px) - var(--s4) * 2); | |
| 54 | - | overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; | |
| 55 | 90 | } | |
| 91 | + | .rail .r-files > .files { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; min-height: 0; } | |
| 92 | + | } | |
| 93 | + | /* on one column the rail goes under the diff: the list of files keeps its | |
| 94 | + | own scroll, under a height */ | |
| 95 | + | @media (max-width: 1080px) { | |
| 96 | + | .rail .r-files > .files { max-height: 50dvh; overflow-y: auto; overscroll-behavior: contain; } | |
| 97 | + | } | |
| 98 | + | .r-files { | |
| 99 | + | & > h2 kbd { margin-left: auto; } | |
| 100 | + | & > .ffilter { margin: var(--s2) var(--s3) 0; } | |
| 101 | + | & > .ffilter input { | |
| 102 | + | width: 100%; height: 30px; padding: 0 var(--s2); font: inherit; font-size: var(--fs-s); color: inherit; | |
| 103 | + | background: var(--bg-inset); border: 1px solid var(--border-strong); border-radius: var(--r-s); | |
| 104 | + | } | |
| 105 | + | & > .files { margin: var(--s2) 0 !important; padding: 0 var(--s2); } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | /* a file of the diff whose window is not loaded yet: about its room */ | |
| 109 | + | .dlazy { | |
| 110 | + | display: flex; align-items: center; justify-content: center; | |
| 111 | + | background: repeating-linear-gradient(var(--bg-surface) 0 var(--lh-code), var(--paper-alt) var(--lh-code) calc(var(--lh-code) * 2)); | |
| 112 | + | border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--r) var(--r); | |
| 113 | + | font-size: var(--fs-s); | |
| 114 | + | & a { color: var(--ink-mute); } | |
| 115 | + | &.dlazy-1 { height: 120px; } &.dlazy-2 { height: 360px; } &.dlazy-3 { height: 900px; } &.dlazy-4 { height: 1600px; } | |
| 116 | + | /* with scripts the window comes by itself: the link of the page without | |
| 117 | + | scripts would only flash */ | |
| 118 | + | html.js & > a { visibility: hidden; } | |
| 119 | + | & .notice { margin: 0; } | |
| 120 | + | } | |
| 121 | + | .dloose { | |
| 122 | + | padding: var(--s2) var(--s3) var(--s3); background: var(--bg-surface); | |
| 123 | + | border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--r) var(--r); | |
| 56 | 124 | } | |
| 57 | 125 | ||
| 58 | 126 | .pcommits { font-size: var(--fs-s); display: grid; gap: var(--s1); } | |
M internal/adapters/web/static/js/ermite.js +42 -5
| @@ -41,6 +41,7 @@ | |||
| 41 | 41 | }; | |
| 42 | 42 | ||
| 43 | 43 | const ermite = { | |
| 44 | + | flash, | |
| 44 | 45 | fire(name) { | |
| 45 | 46 | body().dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | |
| 46 | 47 | }, | |
| @@ -48,6 +49,7 @@ | |||
| 48 | 49 | // Row cursor: j / k. | |
| 49 | 50 | move(d) { | |
| 50 | 51 | if ($('dialog[open]')) return; | |
| 52 | + | if (window.ermiteMR?.stepFiles(d)) return; | |
| 51 | 53 | const rows = visibleRows(); | |
| 52 | 54 | if (!rows.length) { | |
| 53 | 55 | scrollBy(0, d * 180); | |
| @@ -73,6 +75,11 @@ | |||
| 73 | 75 | open() { | |
| 74 | 76 | if (!$('dialog[open]')) $('[data-cursor] a')?.click(); | |
| 75 | 77 | }, | |
| 78 | + | // openFile opens a file of the diff the table of contents links to. | |
| 79 | + | openFile(id) { | |
| 80 | + | const f = document.getElementById(id); | |
| 81 | + | if (f) f.open = true; | |
| 82 | + | }, | |
| 76 | 83 | focusFilter() { | |
| 77 | 84 | $('[data-filter]')?.focus(); | |
| 78 | 85 | }, | |
| @@ -210,9 +217,11 @@ | |||
| 210 | 217 | ||
| 211 | 218 | // 1. Keys. | |
| 212 | 219 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | |
| 213 | - | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc' }; | |
| 220 | + | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc', | |
| 221 | + | // the merge request page (mr.js): the merge menu, mark read | |
| 222 | + | M: 'merge', R: 'read' }; | |
| 214 | 223 | // "g t" is the key "t": the tabs have one event each. | |
| 215 | - | const goto = { h: 'gh', t: 't', c: 'c', r: 'r', m: 'm', g: 'top' }; | |
| 224 | + | const goto = { h: 'gh', t: 't', c: 'c', r: 'r', m: 'm', g: 'top', f: 'gf' }; | |
| 216 | 225 | let pending = 0; | |
| 217 | 226 | let pendingTimer = 0; | |
| 218 | 227 | addEventListener('keydown', (e) => { | |
| @@ -221,7 +230,16 @@ | |||
| 221 | 230 | // field keeps ctrl k (kill to end of line on macOS). | |
| 222 | 231 | if ((e.metaKey || (e.ctrlKey && !typing)) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pending = 0; return ermite.fire('cmd'); } | |
| 223 | 232 | if (e.ctrlKey || e.metaKey || e.altKey) return; | |
| 224 | - | if (e.key === 'Escape') { pending = 0; if (typing) e.target.blur(); return ermite.fire('esc'); } | |
| 233 | + | const mr = window.ermiteMR; | |
| 234 | + | if (e.key === 'Escape') { | |
| 235 | + | pending = 0; | |
| 236 | + | if (mr?.closeMenus() || mr?.leaveFiles()) return; | |
| 237 | + | if (typing) e.target.blur(); | |
| 238 | + | return ermite.fire('esc'); | |
| 239 | + | } | |
| 240 | + | // An open merge menu takes its keys (1-9, D) before the field check: its | |
| 241 | + | // radios and its checkbox hold the focus. | |
| 242 | + | if (mr?.menuKey(e)) return; | |
| 225 | 243 | if (typing) return; | |
| 226 | 244 | // An open dialog takes only the keys that move between dialogs: the page | |
| 227 | 245 | // behind it does not react (scripted clicks would go through inert). | |
| @@ -273,7 +291,7 @@ | |||
| 273 | 291 | // line (the first file when none is). One observer tracks the files in the | |
| 274 | 292 | // band; it reports only when a file enters or leaves it, so a scroll costs | |
| 275 | 293 | // nothing in between, and a jump that skips the band is caught by the scan. | |
| 276 | - | const files = $$('.dfile'); | |
| 294 | + | let files = $$('.dfile'); | |
| 277 | 295 | const links = new Map($$('.files a[href^="#"]').map((a) => [a.hash.slice(1), a])); | |
| 278 | 296 | if (files.length && links.size && 'IntersectionObserver' in window) { | |
| 279 | 297 | const inBand = new Set(); | |
| @@ -284,7 +302,17 @@ | |||
| 284 | 302 | ?? files[0]; | |
| 285 | 303 | if (f === current) return; | |
| 286 | 304 | links.get(current?.id)?.removeAttribute('aria-current'); | |
| 287 | - | links.get(f.id)?.setAttribute('aria-current', 'true'); | |
| 305 | + | const a = links.get(f.id); | |
| 306 | + | a?.setAttribute('aria-current', 'true'); | |
| 307 | + | // The list scrolls on its own: the current file stays in it. Only the | |
| 308 | + | // list moves, never the page (scrollIntoView would scroll the page to | |
| 309 | + | // the list when the rail is under the diff). | |
| 310 | + | const box = a?.closest('.files'); | |
| 311 | + | if (a && box && box.scrollHeight > box.clientHeight && !box.contains(document.activeElement)) { | |
| 312 | + | const top = a.offsetTop; | |
| 313 | + | if (top < box.scrollTop) box.scrollTop = top; | |
| 314 | + | else if (top + a.offsetHeight > box.scrollTop + box.clientHeight) box.scrollTop = top + a.offsetHeight - box.clientHeight; | |
| 315 | + | } | |
| 288 | 316 | current = f; | |
| 289 | 317 | }; | |
| 290 | 318 | const io = new IntersectionObserver((entries) => { | |
| @@ -295,6 +323,15 @@ | |||
| 295 | 323 | mark(); | |
| 296 | 324 | }, { rootMargin: '0px 0px -67% 0px' }); | |
| 297 | 325 | for (const f of files) io.observe(f); | |
| 326 | + | // A window of files of a merge request came in (mr.js): its files replace | |
| 327 | + | // placeholders, which the observer and the list knew by element. | |
| 328 | + | ermite.watchFile = (f, old) => { | |
| 329 | + | if (old) { io.unobserve(old); inBand.delete(old); } | |
| 330 | + | files = $$('.dfile'); | |
| 331 | + | for (const x of [...inBand]) if (!x.isConnected) inBand.delete(x); | |
| 332 | + | if (current && !current.isConnected) current = document.getElementById(current.id); | |
| 333 | + | io.observe(f); | |
| 334 | + | }; | |
| 298 | 335 | } | |
| 299 | 336 | ||
| 300 | 337 | // 5. A failure of a handler or of a request (htmx logs it to the console | |
A internal/adapters/web/static/js/mr.js +249 -0
| @@ -0,0 +1,249 @@ | |||
| 1 | + | // mr.js: the merge request page beyond what htmx and hx-live give. | |
| 2 | + | // | |
| 3 | + | // 1. Windows of files. The page holds the hunks of one window of files; the | |
| 4 | + | // other files are placeholders (.dlazy[data-src]). A placeholder that comes | |
| 5 | + | // near the screen fetches its window (/files?from=&base=&head=), whose | |
| 6 | + | // files replace the placeholders of the same ids. A window is fetched once; | |
| 7 | + | // a link to a file or to a comment of a placeholder fetches it first. | |
| 8 | + | // 2. The menus of the bar (merge, more) and their keys: M opens the merge menu | |
| 9 | + | // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter | |
| 10 | + | // submits (the browser does it: the focus is on the button), Esc closes. | |
| 11 | + | // R marks the head read. Nothing merges on one key. | |
| 12 | + | // 3. g f puts the focus in the list of files; j / k move in it, Enter opens | |
| 13 | + | // the file, Esc goes back to the diff. | |
| 14 | + | // | |
| 15 | + | // Nothing runs on a page without #diffs[data-mr]. window.ermiteMR.core is pure | |
| 16 | + | // (jstest/mr.test.mjs). | |
| 17 | + | window.ermiteMR = (() => { | |
| 18 | + | // core: the decisions, without the DOM. | |
| 19 | + | const core = { | |
| 20 | + | // windowOf is the start of the window of file i (files are f1, f2, ...). | |
| 21 | + | windowOf(id, size) { | |
| 22 | + | const n = Number(String(id).replace(/^f/, '')) - 1; | |
| 23 | + | return Number.isInteger(n) && n >= 0 ? n - (n % size) : -1; | |
| 24 | + | }, | |
| 25 | + | // menuKey says what a key does while the merge menu is open: a mode | |
| 26 | + | // (index), the branch deletion, or nothing. | |
| 27 | + | menuKey(key, modes) { | |
| 28 | + | if (key === 'D') return { toggle: true }; | |
| 29 | + | if (/^[1-9]$/.test(key) && Number(key) <= modes) return { mode: Number(key) - 1 }; | |
| 30 | + | return null; | |
| 31 | + | }, | |
| 32 | + | // step moves an index within [0, n). | |
| 33 | + | step(i, d, n) { | |
| 34 | + | if (n === 0) return -1; | |
| 35 | + | if (i < 0) return d > 0 ? 0 : n - 1; | |
| 36 | + | return Math.max(0, Math.min(n - 1, i + d)); | |
| 37 | + | }, | |
| 38 | + | }; | |
| 39 | + | ||
| 40 | + | const $ = (sel, root = document) => root.querySelector(sel); | |
| 41 | + | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| 42 | + | const loading = new Map(); // src -> promise of the swap | |
| 43 | + | let moved = false; | |
| 44 | + | let lazyIO = null; // the observer of the placeholders (watch) | |
| 45 | + | let revealGen = 0; // the last reveal wins the scroll | |
| 46 | + | ||
| 47 | + | // load fetches the window of a placeholder and swaps its files in. The | |
| 48 | + | // promise resolves once they are in the page (or the page said to reload). | |
| 49 | + | function load(ph) { | |
| 50 | + | const src = ph?.dataset.src; | |
| 51 | + | if (!src || moved) return Promise.resolve(); | |
| 52 | + | if (loading.has(src)) return loading.get(src); | |
| 53 | + | for (const p of $$('.dlazy[data-src]')) if (p.dataset.src === src) p.setAttribute('data-loading', ''); | |
| 54 | + | // The window renders like the page: the layout shown, and both layouts | |
| 55 | + | // when the page switches them itself (data-diff) or one (data-view). | |
| 56 | + | const d = $('#diffs')?.dataset || {}; | |
| 57 | + | const view = d.diff || d.view; | |
| 58 | + | const layouts = d.diff ? 'both' : d.view ? 'one' : ''; | |
| 59 | + | const url = src + (view ? '&view=' + encodeURIComponent(view) : '') + (layouts ? '&layouts=' + layouts : ''); | |
| 60 | + | const p = fetch(url, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |
| 61 | + | .then(async (res) => { | |
| 62 | + | const html = await res.text(); | |
| 63 | + | if (res.status === 409) return stale(html); | |
| 64 | + | if (!res.ok) throw new Error('loading files: ' + res.status); | |
| 65 | + | // A window of the old diff after the page went stale stays out. | |
| 66 | + | if (!moved) swap(html); | |
| 67 | + | }) | |
| 68 | + | .catch((err) => { | |
| 69 | + | loading.delete(src); | |
| 70 | + | for (const p of $$('.dlazy[data-loading]')) if (p.dataset.src === src) p.removeAttribute('data-loading'); | |
| 71 | + | window.ermite?.flash?.(err.message, 4000); | |
| 72 | + | }); | |
| 73 | + | loading.set(src, p); | |
| 74 | + | return p; | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // swap replaces each placeholder file with the file of the same id. A | |
| 78 | + | // file the reader folded stays folded. | |
| 79 | + | function swap(html) { | |
| 80 | + | const t = document.createElement('template'); | |
| 81 | + | t.innerHTML = html; | |
| 82 | + | for (const f of [...t.content.querySelectorAll('details[data-file]')]) { | |
| 83 | + | const old = document.getElementById(f.id); | |
| 84 | + | if (!old || !old.querySelector('.dlazy')) continue; | |
| 85 | + | f.open = old.open; | |
| 86 | + | for (const p of $$('.dlazy', old)) lazyIO?.unobserve(p); | |
| 87 | + | old.replaceWith(f); | |
| 88 | + | window.htmx?.process(f); | |
| 89 | + | window.ermite?.watchFile?.(f, old); | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // stale: the diff changed under the page. The placeholders say so; no file | |
| 94 | + | // of another diff comes in. | |
| 95 | + | function stale(html) { | |
| 96 | + | moved = true; | |
| 97 | + | for (const p of $$('.dlazy')) { | |
| 98 | + | p.removeAttribute('data-loading'); | |
| 99 | + | p.innerHTML = html; | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // reveal loads the file of an id (a file "f12" or a comment "c3") when it is | |
| 104 | + | // a placeholder, then scrolls to it. It returns false when there is nothing | |
| 105 | + | // to load. | |
| 106 | + | function reveal(id, scroll = true) { | |
| 107 | + | // Any navigation, loaded or not, makes the reveals still loading stale. | |
| 108 | + | const gen = ++revealGen; | |
| 109 | + | let ph = document.getElementById(id)?.querySelector(':scope > .dlazy'); | |
| 110 | + | if (!ph && /^c\d+/.test(id) && !document.getElementById(id)) { | |
| 111 | + | const base = id.replace(/-s$/, ''); | |
| 112 | + | ph = $$('.dlazy[data-notes]').find((p) => p.dataset.notes.split(' ').includes(base)); | |
| 113 | + | } | |
| 114 | + | if (!ph) return false; | |
| 115 | + | load(ph).then(() => { | |
| 116 | + | // A later reveal (another file asked meanwhile) owns the view. | |
| 117 | + | if (gen !== revealGen) return; | |
| 118 | + | const el = document.getElementById(id); | |
| 119 | + | if (!el) return; | |
| 120 | + | const d = el.closest('details'); | |
| 121 | + | if (d) d.open = true; | |
| 122 | + | if (!scroll) return; | |
| 123 | + | // The files above may have changed size while it loaded (placeholders, | |
| 124 | + | // content-visibility): place it, then again once laid out. | |
| 125 | + | el.scrollIntoView({ block: 'start' }); | |
| 126 | + | requestAnimationFrame(() => requestAnimationFrame(() => { | |
| 127 | + | if (gen === revealGen) el.scrollIntoView({ block: 'start' }); | |
| 128 | + | })); | |
| 129 | + | }); | |
| 130 | + | return true; | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // Placeholders load as they come within two screens of the view; files | |
| 134 | + | // that come in are marked in the list of files (ermite.js marks the one in | |
| 135 | + | // view). | |
| 136 | + | function watch() { | |
| 137 | + | if (!('IntersectionObserver' in window)) { | |
| 138 | + | for (const p of $$('.dlazy[data-src]')) load(p); | |
| 139 | + | return; | |
| 140 | + | } | |
| 141 | + | lazyIO = new IntersectionObserver((entries) => { | |
| 142 | + | for (const e of entries) { | |
| 143 | + | if (!e.isIntersecting) continue; | |
| 144 | + | lazyIO.unobserve(e.target); | |
| 145 | + | load(e.target); | |
| 146 | + | } | |
| 147 | + | }, { rootMargin: '200% 0px 200% 0px' }); | |
| 148 | + | for (const p of $$('.dlazy[data-src]')) lazyIO.observe(p); | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | // The merge menu. | |
| 152 | + | const menu = () => $('#merge-menu'); | |
| 153 | + | function openMerge() { | |
| 154 | + | const m = menu(); | |
| 155 | + | if (!m) return false; | |
| 156 | + | for (const d of $$('details.menu[open]')) if (d !== m) d.open = false; | |
| 157 | + | m.open = true; | |
| 158 | + | $('[data-default]', m)?.focus(); | |
| 159 | + | return true; | |
| 160 | + | } | |
| 161 | + | function closeMenus() { | |
| 162 | + | const open = $$('details.menu[open]'); | |
| 163 | + | for (const d of open) d.open = false; | |
| 164 | + | if (open.length) $('summary', open[0])?.focus(); | |
| 165 | + | return open.length > 0; | |
| 166 | + | } | |
| 167 | + | // menuKey handles a key while the merge menu is open; true when it used it. | |
| 168 | + | function menuKey(e) { | |
| 169 | + | const m = menu(); | |
| 170 | + | if (!m?.open || e.ctrlKey || e.metaKey || e.altKey) return false; | |
| 171 | + | // An open dialog (help, commands) is above the menu: it keeps the keys. | |
| 172 | + | if ($('dialog[open]')) return false; | |
| 173 | + | // A field outside the menu (a comment being written) keeps its keys. | |
| 174 | + | const field = e.target.closest?.('input, textarea, select, [contenteditable]'); | |
| 175 | + | if (field && !m.contains(field)) return false; | |
| 176 | + | const radios = $$('input[name="mode"]', m); | |
| 177 | + | const act = core.menuKey(e.key, radios.length); | |
| 178 | + | if (!act) return false; | |
| 179 | + | e.preventDefault(); | |
| 180 | + | if (act.toggle) { | |
| 181 | + | const box = $('input[name="delete_branch"]', m); | |
| 182 | + | if (box) box.checked = !box.checked; | |
| 183 | + | } else { | |
| 184 | + | radios[act.mode].checked = true; | |
| 185 | + | } | |
| 186 | + | $('[data-default]', m)?.focus(); | |
| 187 | + | return true; | |
| 188 | + | } | |
| 189 | + | function markRead() { | |
| 190 | + | const b = $('button[data-read]'); | |
| 191 | + | if (!b) return false; | |
| 192 | + | b.closest('form').requestSubmit(b); | |
| 193 | + | return true; | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | // The list of files by keyboard: g f focuses the current file. | |
| 197 | + | const links = () => $$('.r-files .files > li:not([hidden]) > a'); | |
| 198 | + | function focusFiles() { | |
| 199 | + | const all = links(); | |
| 200 | + | if (!all.length) return false; | |
| 201 | + | const cur = all.find((a) => a.getAttribute('aria-current') === 'true') || all[0]; | |
| 202 | + | cur.focus(); | |
| 203 | + | cur.scrollIntoView({ block: 'nearest' }); | |
| 204 | + | return true; | |
| 205 | + | } | |
| 206 | + | // stepFiles moves the focus in the list; false when the focus is not in it. | |
| 207 | + | function stepFiles(d) { | |
| 208 | + | const all = links(); | |
| 209 | + | const i = all.indexOf(document.activeElement); | |
| 210 | + | if (i < 0) return false; | |
| 211 | + | const j = core.step(i, d, all.length); | |
| 212 | + | all[j].focus(); | |
| 213 | + | all[j].scrollIntoView({ block: 'nearest' }); | |
| 214 | + | return true; | |
| 215 | + | } | |
| 216 | + | function leaveFiles() { | |
| 217 | + | if (!links().includes(document.activeElement)) return false; | |
| 218 | + | document.getElementById('main')?.focus({ preventScroll: true }); | |
| 219 | + | return true; | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // openFile opens the file of a link of the list (and loads it first). | |
| 223 | + | function openFile(id) { | |
| 224 | + | const f = document.getElementById(id); | |
| 225 | + | if (f) f.open = true; | |
| 226 | + | reveal(id); | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | function start() { | |
| 230 | + | if (!$('#diffs[data-mr]')) return; | |
| 231 | + | watch(); | |
| 232 | + | // A link to a file or a comment that is not loaded yet. | |
| 233 | + | const hash = () => { | |
| 234 | + | const id = decodeURIComponent(location.hash.slice(1)); | |
| 235 | + | if (id) reveal(id); | |
| 236 | + | }; | |
| 237 | + | addEventListener('hashchange', hash); | |
| 238 | + | hash(); | |
| 239 | + | // A click outside an open menu closes it. | |
| 240 | + | addEventListener('click', (e) => { | |
| 241 | + | for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false; | |
| 242 | + | }); | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | return { core, start, load, reveal, openMerge, closeMenus, menuKey, markRead, focusFiles, stepFiles, leaveFiles, openFile }; | |
| 246 | + | })(); | |
| 247 | + | ||
| 248 | + | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start()); | |
| 249 | + | else window.ermiteMR.start(); | |
M internal/adapters/web/view.go +2 -0
| @@ -67,6 +67,8 @@ type Layout struct { | |||
| 67 | 67 | // Passkeys tells that the passkey routes exist: the header then links to | |
| 68 | 68 | // them. | |
| 69 | 69 | Passkeys bool | |
| 70 | + | // MR loads the script of the merge request page (mr.js). | |
| 71 | + | MR bool | |
| 70 | 72 | } | |
| 71 | 73 | ||
| 72 | 74 | // Link is a plain label + href pair. | |
M internal/adapters/web/web.go +1 -0
| @@ -177,6 +177,7 @@ func (s *Server) routes() *http.ServeMux { | |||
| 177 | 177 | mux.HandleFunc("GET /{repo}/merge-requests/{$}", s.page(s.mrList)) | |
| 178 | 178 | mux.HandleFunc("GET /{repo}/merge-requests/{n}", s.page(s.mrShow)) | |
| 179 | 179 | mux.HandleFunc("GET /{repo}/merge-requests/{n}/since", s.page(s.mrSince)) | |
| 180 | + | mux.HandleFunc("GET /{repo}/merge-requests/{n}/files", s.page(s.mrFiles)) | |
| 180 | 181 | mux.HandleFunc("GET /{repo}/compare/{rest...}", s.page(s.compare)) | |
| 181 | 182 | // The writes: POST only, behind the gate (ServeHTTP). | |
| 182 | 183 | mux.HandleFunc("POST /{repo}/merge-requests", s.mrAction(s.mrCreate)) | |
M internal/app/testdata/web/blob-binary.html +4 -1
| @@ -374,7 +374,10 @@ | |||
| 374 | 374 | k-split from:body -> ermite.layout('split'); | |
| 375 | 375 | k-unified from:body -> ermite.layout('unified'); | |
| 376 | 376 | k-collapse from:body -> ermite.collapse(); | |
| 377 | - | k-permalink from:body -> ermite.permalink()"> | |
| 377 | + | k-permalink from:body -> ermite.permalink(); | |
| 378 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 379 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 380 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 378 | 381 | </div> | |
| 379 | 382 | </body> | |
| 380 | 383 | </html> | |
M internal/app/testdata/web/blob-code.html +4 -1
| @@ -444,7 +444,10 @@ | |||
| 444 | 444 | k-split from:body -> ermite.layout('split'); | |
| 445 | 445 | k-unified from:body -> ermite.layout('unified'); | |
| 446 | 446 | k-collapse from:body -> ermite.collapse(); | |
| 447 | - | k-permalink from:body -> ermite.permalink()"> | |
| 447 | + | k-permalink from:body -> ermite.permalink(); | |
| 448 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 449 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 450 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 448 | 451 | </div> | |
| 449 | 452 | </body> | |
| 450 | 453 | </html> | |
M internal/app/testdata/web/blob-empty.html +4 -1
| @@ -374,7 +374,10 @@ | |||
| 374 | 374 | k-split from:body -> ermite.layout('split'); | |
| 375 | 375 | k-unified from:body -> ermite.layout('unified'); | |
| 376 | 376 | k-collapse from:body -> ermite.collapse(); | |
| 377 | - | k-permalink from:body -> ermite.permalink()"> | |
| 377 | + | k-permalink from:body -> ermite.permalink(); | |
| 378 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 379 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 380 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 378 | 381 | </div> | |
| 379 | 382 | </body> | |
| 380 | 383 | </html> | |
M internal/app/testdata/web/blob-image.html +4 -1
| @@ -376,7 +376,10 @@ | |||
| 376 | 376 | k-split from:body -> ermite.layout('split'); | |
| 377 | 377 | k-unified from:body -> ermite.layout('unified'); | |
| 378 | 378 | k-collapse from:body -> ermite.collapse(); | |
| 379 | - | k-permalink from:body -> ermite.permalink()"> | |
| 379 | + | k-permalink from:body -> ermite.permalink(); | |
| 380 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 381 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 382 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 380 | 383 | </div> | |
| 381 | 384 | </body> | |
| 382 | 385 | </html> | |
M internal/app/testdata/web/blob-markdown.html +4 -1
| @@ -441,7 +441,10 @@ | |||
| 441 | 441 | k-split from:body -> ermite.layout('split'); | |
| 442 | 442 | k-unified from:body -> ermite.layout('unified'); | |
| 443 | 443 | k-collapse from:body -> ermite.collapse(); | |
| 444 | - | k-permalink from:body -> ermite.permalink()"> | |
| 444 | + | k-permalink from:body -> ermite.permalink(); | |
| 445 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 446 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 447 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 445 | 448 | </div> | |
| 446 | 449 | </body> | |
| 447 | 450 | </html> | |
M internal/app/testdata/web/blob-svg.html +4 -1
| @@ -399,7 +399,10 @@ | |||
| 399 | 399 | k-split from:body -> ermite.layout('split'); | |
| 400 | 400 | k-unified from:body -> ermite.layout('unified'); | |
| 401 | 401 | k-collapse from:body -> ermite.collapse(); | |
| 402 | - | k-permalink from:body -> ermite.permalink()"> | |
| 402 | + | k-permalink from:body -> ermite.permalink(); | |
| 403 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 404 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 405 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 403 | 406 | </div> | |
| 404 | 407 | </body> | |
| 405 | 408 | </html> | |
M internal/app/testdata/web/blob-text.html +4 -1
| @@ -394,7 +394,10 @@ | |||
| 394 | 394 | k-split from:body -> ermite.layout('split'); | |
| 395 | 395 | k-unified from:body -> ermite.layout('unified'); | |
| 396 | 396 | k-collapse from:body -> ermite.collapse(); | |
| 397 | - | k-permalink from:body -> ermite.permalink()"> | |
| 397 | + | k-permalink from:body -> ermite.permalink(); | |
| 398 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 399 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 400 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 398 | 401 | </div> | |
| 399 | 402 | </body> | |
| 400 | 403 | </html> | |
M internal/app/testdata/web/blob-too-large.html +4 -1
| @@ -374,7 +374,10 @@ | |||
| 374 | 374 | k-split from:body -> ermite.layout('split'); | |
| 375 | 375 | k-unified from:body -> ermite.layout('unified'); | |
| 376 | 376 | k-collapse from:body -> ermite.collapse(); | |
| 377 | - | k-permalink from:body -> ermite.permalink()"> | |
| 377 | + | k-permalink from:body -> ermite.permalink(); | |
| 378 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 379 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 380 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 378 | 381 | </div> | |
| 379 | 382 | </body> | |
| 380 | 383 | </html> | |
M internal/app/testdata/web/blob-trojan.html +4 -1
| @@ -441,7 +441,10 @@ | |||
| 441 | 441 | k-split from:body -> ermite.layout('split'); | |
| 442 | 442 | k-unified from:body -> ermite.layout('unified'); | |
| 443 | 443 | k-collapse from:body -> ermite.collapse(); | |
| 444 | - | k-permalink from:body -> ermite.permalink()"> | |
| 444 | + | k-permalink from:body -> ermite.permalink(); | |
| 445 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 446 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 447 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 445 | 448 | </div> | |
| 446 | 449 | </body> | |
| 447 | 450 | </html> | |
M internal/app/testdata/web/commit-big.html +14 -10
| @@ -130,7 +130,7 @@ | |||
| 130 | 130 | </div> | |
| 131 | 131 | <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p> | |
| 132 | 132 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 133 | - | <details class="dfile" id="f1" data-file> | |
| 133 | + | <details class="dfile lnw-2" id="f1" data-file> | |
| 134 | 134 | <summary> | |
| 135 | 135 | <span class="st">A</span> | |
| 136 | 136 | <span class="p">big.txt</span> | |
| @@ -148,18 +148,19 @@ | |||
| 148 | 148 | </h2> | |
| 149 | 149 | <ol class="files" aria-label="Changed files"> | |
| 150 | 150 | <li> | |
| 151 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 152 | - | <span class="st" title="A">A</span> | |
| 153 | - | <span class="p" title="big.txt">big.txt</span> | |
| 151 | + | <a href="#f1" title="big.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 152 | + | <span class="st" data-st="A" title="added">A</span> | |
| 153 | + | <span class="nm"> | |
| 154 | + | <span class="p">big.txt</span> | |
| 155 | + | <span class="d"> | |
| 156 | + | <bdi> | |
| 157 | + | </bdi> | |
| 158 | + | </span> | |
| 159 | + | </span> | |
| 154 | 160 | <span class="n"> | |
| 155 | 161 | <span class="plus">+2600</span> | |
| 156 | 162 | <span class="minus">-0</span> | |
| 157 | 163 | </span> | |
| 158 | - | <span class="b" aria-hidden="true"> | |
| 159 | - | <span class="plus">++++++++++</span> | |
| 160 | - | <span class="minus"> | |
| 161 | - | </span> | |
| 162 | - | </span> | |
| 163 | 164 | </a> | |
| 164 | 165 | </li> | |
| 165 | 166 | </ol> | |
| @@ -411,7 +412,10 @@ | |||
| 411 | 412 | k-split from:body -> ermite.layout('split'); | |
| 412 | 413 | k-unified from:body -> ermite.layout('unified'); | |
| 413 | 414 | k-collapse from:body -> ermite.collapse(); | |
| 414 | - | k-permalink from:body -> ermite.permalink()"> | |
| 415 | + | k-permalink from:body -> ermite.permalink(); | |
| 416 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 417 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 418 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 415 | 419 | </div> | |
| 416 | 420 | </body> | |
| 417 | 421 | </html> | |
M internal/app/testdata/web/commit-binary.html +13 -12
| @@ -125,7 +125,7 @@ | |||
| 125 | 125 | </button> | |
| 126 | 126 | </div> | |
| 127 | 127 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 128 | - | <details class="dfile" id="f1" data-file> | |
| 128 | + | <details class="dfile lnw-2" id="f1" data-file> | |
| 129 | 129 | <summary> | |
| 130 | 130 | <span class="st">M</span> | |
| 131 | 131 | <span class="p">logo.bin</span> | |
| @@ -143,18 +143,16 @@ | |||
| 143 | 143 | </h2> | |
| 144 | 144 | <ol class="files" aria-label="Changed files"> | |
| 145 | 145 | <li> | |
| 146 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 147 | - | <span class="st" title="M">M</span> | |
| 148 | - | <span class="p" title="logo.bin">logo.bin</span> | |
| 149 | - | <span class="n"> | |
| 150 | - | <span class="plus">+0</span> | |
| 151 | - | <span class="minus">-0</span> | |
| 146 | + | <a href="#f1" title="logo.bin" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 147 | + | <span class="st" data-st="M" title="modified">M</span> | |
| 148 | + | <span class="nm"> | |
| 149 | + | <span class="p">logo.bin</span> | |
| 150 | + | <span class="d"> | |
| 151 | + | <bdi> | |
| 152 | + | </bdi> | |
| 152 | 153 | </span> | |
| 153 | - | <span class="b" aria-hidden="true"> | |
| 154 | - | <span class="plus"> | |
| 155 | 154 | </span> | |
| 156 | - | <span class="minus"> | |
| 157 | - | </span>bin</span> | |
| 155 | + | <span class="n mute">bin</span> | |
| 158 | 156 | </a> | |
| 159 | 157 | </li> | |
| 160 | 158 | </ol> | |
| @@ -406,7 +404,10 @@ | |||
| 406 | 404 | k-split from:body -> ermite.layout('split'); | |
| 407 | 405 | k-unified from:body -> ermite.layout('unified'); | |
| 408 | 406 | k-collapse from:body -> ermite.collapse(); | |
| 409 | - | k-permalink from:body -> ermite.permalink()"> | |
| 407 | + | k-permalink from:body -> ermite.permalink(); | |
| 408 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 409 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 410 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 410 | 411 | </div> | |
| 411 | 412 | </body> | |
| 412 | 413 | </html> | |
M internal/app/testdata/web/commit-merge.html +14 -10
| @@ -124,7 +124,7 @@ | |||
| 124 | 124 | </button> | |
| 125 | 125 | </div> | |
| 126 | 126 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 127 | - | <details class="dfile" id="f1" data-file open> | |
| 127 | + | <details class="dfile lnw-2" id="f1" data-file open> | |
| 128 | 128 | <summary> | |
| 129 | 129 | <span class="st">A</span> | |
| 130 | 130 | <span class="p">side.txt</span> | |
| @@ -182,18 +182,19 @@ | |||
| 182 | 182 | </h2> | |
| 183 | 183 | <ol class="files" aria-label="Changed files"> | |
| 184 | 184 | <li> | |
| 185 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 186 | - | <span class="st" title="A">A</span> | |
| 187 | - | <span class="p" title="side.txt">side.txt</span> | |
| 185 | + | <a href="#f1" title="side.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 186 | + | <span class="st" data-st="A" title="added">A</span> | |
| 187 | + | <span class="nm"> | |
| 188 | + | <span class="p">side.txt</span> | |
| 189 | + | <span class="d"> | |
| 190 | + | <bdi> | |
| 191 | + | </bdi> | |
| 192 | + | </span> | |
| 193 | + | </span> | |
| 188 | 194 | <span class="n"> | |
| 189 | 195 | <span class="plus">+1</span> | |
| 190 | 196 | <span class="minus">-0</span> | |
| 191 | 197 | </span> | |
| 192 | - | <span class="b" aria-hidden="true"> | |
| 193 | - | <span class="plus">++++++++++</span> | |
| 194 | - | <span class="minus"> | |
| 195 | - | </span> | |
| 196 | - | </span> | |
| 197 | 198 | </a> | |
| 198 | 199 | </li> | |
| 199 | 200 | </ol> | |
| @@ -445,7 +446,10 @@ | |||
| 445 | 446 | k-split from:body -> ermite.layout('split'); | |
| 446 | 447 | k-unified from:body -> ermite.layout('unified'); | |
| 447 | 448 | k-collapse from:body -> ermite.collapse(); | |
| 448 | - | k-permalink from:body -> ermite.permalink()"> | |
| 449 | + | k-permalink from:body -> ermite.permalink(); | |
| 450 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 451 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 452 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 449 | 453 | </div> | |
| 450 | 454 | </body> | |
| 451 | 455 | </html> | |
M internal/app/testdata/web/commit-modify-split.html +24 -18
| @@ -131,7 +131,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 131 | 131 | </button> | |
| 132 | 132 | </div> | |
| 133 | 133 | <div id="diffs" data-diff="split"> | |
| 134 | - | <details class="dfile" id="f1" data-file open> | |
| 134 | + | <details class="dfile lnw-2" id="f1" data-file open> | |
| 135 | 135 | <summary> | |
| 136 | 136 | <span class="st">M</span> | |
| 137 | 137 | <span class="p">a.txt</span> | |
| @@ -271,7 +271,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 271 | 271 | </table> | |
| 272 | 272 | </div> | |
| 273 | 273 | </details> | |
| 274 | - | <details class="dfile" id="f2" data-file> | |
| 274 | + | <details class="dfile lnw-2" id="f2" data-file> | |
| 275 | 275 | <summary> | |
| 276 | 276 | <span class="st">M</span> | |
| 277 | 277 | <span class="p">run.sh</span> | |
| @@ -289,32 +289,35 @@ And <b>html</b> & "quotes".</pre> | |||
| 289 | 289 | </h2> | |
| 290 | 290 | <ol class="files" aria-label="Changed files"> | |
| 291 | 291 | <li> | |
| 292 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 293 | - | <span class="st" title="M">M</span> | |
| 294 | - | <span class="p" title="a.txt">a.txt</span> | |
| 292 | + | <a href="#f1" title="a.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 293 | + | <span class="st" data-st="M" title="modified">M</span> | |
| 294 | + | <span class="nm"> | |
| 295 | + | <span class="p">a.txt</span> | |
| 296 | + | <span class="d"> | |
| 297 | + | <bdi> | |
| 298 | + | </bdi> | |
| 299 | + | </span> | |
| 300 | + | </span> | |
| 295 | 301 | <span class="n"> | |
| 296 | 302 | <span class="plus">+2</span> | |
| 297 | 303 | <span class="minus">-1</span> | |
| 298 | 304 | </span> | |
| 299 | - | <span class="b" aria-hidden="true"> | |
| 300 | - | <span class="plus">+++++++</span> | |
| 301 | - | <span class="minus">---</span> | |
| 302 | - | </span> | |
| 303 | 305 | </a> | |
| 304 | 306 | </li> | |
| 305 | 307 | <li> | |
| 306 | - | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 307 | - | <span class="st" title="M">M</span> | |
| 308 | - | <span class="p" title="run.sh">run.sh</span> | |
| 308 | + | <a href="#f2" title="run.sh" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 309 | + | <span class="st" data-st="M" title="modified">M</span> | |
| 310 | + | <span class="nm"> | |
| 311 | + | <span class="p">run.sh</span> | |
| 312 | + | <span class="d"> | |
| 313 | + | <bdi> | |
| 314 | + | </bdi> | |
| 315 | + | </span> | |
| 316 | + | </span> | |
| 309 | 317 | <span class="n"> | |
| 310 | 318 | <span class="plus">+0</span> | |
| 311 | 319 | <span class="minus">-0</span> | |
| 312 | 320 | </span> | |
| 313 | - | <span class="b" aria-hidden="true"> | |
| 314 | - | <span class="plus"> | |
| 315 | - | </span> | |
| 316 | - | <span class="minus"> | |
| 317 | - | </span>··········</span> | |
| 318 | 321 | </a> | |
| 319 | 322 | </li> | |
| 320 | 323 | </ol> | |
| @@ -566,7 +569,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 566 | 569 | k-split from:body -> ermite.layout('split'); | |
| 567 | 570 | k-unified from:body -> ermite.layout('unified'); | |
| 568 | 571 | k-collapse from:body -> ermite.collapse(); | |
| 569 | - | k-permalink from:body -> ermite.permalink()"> | |
| 572 | + | k-permalink from:body -> ermite.permalink(); | |
| 573 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 574 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 575 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 570 | 576 | </div> | |
| 571 | 577 | </body> | |
| 572 | 578 | </html> | |
M internal/app/testdata/web/commit-modify.html +24 -18
| @@ -131,7 +131,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 131 | 131 | </button> | |
| 132 | 132 | </div> | |
| 133 | 133 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 134 | - | <details class="dfile" id="f1" data-file open> | |
| 134 | + | <details class="dfile lnw-2" id="f1" data-file open> | |
| 135 | 135 | <summary> | |
| 136 | 136 | <span class="st">M</span> | |
| 137 | 137 | <span class="p">a.txt</span> | |
| @@ -271,7 +271,7 @@ And <b>html</b> & "quotes".</pre> | |||
| 271 | 271 | </table> | |
| 272 | 272 | </div> | |
| 273 | 273 | </details> | |
| 274 | - | <details class="dfile" id="f2" data-file> | |
| 274 | + | <details class="dfile lnw-2" id="f2" data-file> | |
| 275 | 275 | <summary> | |
| 276 | 276 | <span class="st">M</span> | |
| 277 | 277 | <span class="p">run.sh</span> | |
| @@ -289,32 +289,35 @@ And <b>html</b> & "quotes".</pre> | |||
| 289 | 289 | </h2> | |
| 290 | 290 | <ol class="files" aria-label="Changed files"> | |
| 291 | 291 | <li> | |
| 292 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 293 | - | <span class="st" title="M">M</span> | |
| 294 | - | <span class="p" title="a.txt">a.txt</span> | |
| 292 | + | <a href="#f1" title="a.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 293 | + | <span class="st" data-st="M" title="modified">M</span> | |
| 294 | + | <span class="nm"> | |
| 295 | + | <span class="p">a.txt</span> | |
| 296 | + | <span class="d"> | |
| 297 | + | <bdi> | |
| 298 | + | </bdi> | |
| 299 | + | </span> | |
| 300 | + | </span> | |
| 295 | 301 | <span class="n"> | |
| 296 | 302 | <span class="plus">+2</span> | |
| 297 | 303 | <span class="minus">-1</span> | |
| 298 | 304 | </span> | |
| 299 | - | <span class="b" aria-hidden="true"> | |
| 300 | - | <span class="plus">+++++++</span> | |
| 301 | - | <span class="minus">---</span> | |
| 302 | - | </span> | |
| 303 | 305 | </a> | |
| 304 | 306 | </li> | |
| 305 | 307 | <li> | |
| 306 | - | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 307 | - | <span class="st" title="M">M</span> | |
| 308 | - | <span class="p" title="run.sh">run.sh</span> | |
| 308 | + | <a href="#f2" title="run.sh" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 309 | + | <span class="st" data-st="M" title="modified">M</span> | |
| 310 | + | <span class="nm"> | |
| 311 | + | <span class="p">run.sh</span> | |
| 312 | + | <span class="d"> | |
| 313 | + | <bdi> | |
| 314 | + | </bdi> | |
| 315 | + | </span> | |
| 316 | + | </span> | |
| 309 | 317 | <span class="n"> | |
| 310 | 318 | <span class="plus">+0</span> | |
| 311 | 319 | <span class="minus">-0</span> | |
| 312 | 320 | </span> | |
| 313 | - | <span class="b" aria-hidden="true"> | |
| 314 | - | <span class="plus"> | |
| 315 | - | </span> | |
| 316 | - | <span class="minus"> | |
| 317 | - | </span>··········</span> | |
| 318 | 321 | </a> | |
| 319 | 322 | </li> | |
| 320 | 323 | </ol> | |
| @@ -566,7 +569,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 566 | 569 | k-split from:body -> ermite.layout('split'); | |
| 567 | 570 | k-unified from:body -> ermite.layout('unified'); | |
| 568 | 571 | k-collapse from:body -> ermite.collapse(); | |
| 569 | - | k-permalink from:body -> ermite.permalink()"> | |
| 572 | + | k-permalink from:body -> ermite.permalink(); | |
| 573 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 574 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 575 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 570 | 576 | </div> | |
| 571 | 577 | </body> | |
| 572 | 578 | </html> | |
M internal/app/testdata/web/commit-rename.html +24 -19
| @@ -125,7 +125,7 @@ | |||
| 125 | 125 | </button> | |
| 126 | 126 | </div> | |
| 127 | 127 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 128 | - | <details class="dfile" id="f1" data-file> | |
| 128 | + | <details class="dfile lnw-2" id="f1" data-file> | |
| 129 | 129 | <summary> | |
| 130 | 130 | <span class="st">R</span> | |
| 131 | 131 | <span class="p">old.txt → docs-old.txt</span> | |
| @@ -135,7 +135,7 @@ | |||
| 135 | 135 | </summary> | |
| 136 | 136 | <p class="note pad">renamed, 100% similar</p> | |
| 137 | 137 | </details> | |
| 138 | - | <details class="dfile" id="f2" data-file open> | |
| 138 | + | <details class="dfile lnw-2" id="f2" data-file open> | |
| 139 | 139 | <summary> | |
| 140 | 140 | <span class="st">D</span> | |
| 141 | 141 | <span class="p">gone.txt</span> | |
| @@ -193,33 +193,35 @@ | |||
| 193 | 193 | </h2> | |
| 194 | 194 | <ol class="files" aria-label="Changed files"> | |
| 195 | 195 | <li> | |
| 196 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 197 | - | <span class="st" title="R">R</span> | |
| 198 | - | <span class="p" title="old.txt → docs-old.txt">old.txt → docs-old.txt</span> | |
| 196 | + | <a href="#f1" title="old.txt → docs-old.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 197 | + | <span class="st" data-st="R" title="renamed">R</span> | |
| 198 | + | <span class="nm"> | |
| 199 | + | <span class="p">docs-old.txt</span> | |
| 200 | + | <span class="d"> | |
| 201 | + | <bdi> | |
| 202 | + | </bdi> | |
| 203 | + | </span> | |
| 204 | + | </span> | |
| 199 | 205 | <span class="n"> | |
| 200 | 206 | <span class="plus">+0</span> | |
| 201 | 207 | <span class="minus">-0</span> | |
| 202 | 208 | </span> | |
| 203 | - | <span class="b" aria-hidden="true"> | |
| 204 | - | <span class="plus"> | |
| 205 | - | </span> | |
| 206 | - | <span class="minus"> | |
| 207 | - | </span>··········</span> | |
| 208 | 209 | </a> | |
| 209 | 210 | </li> | |
| 210 | 211 | <li> | |
| 211 | - | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 212 | - | <span class="st" title="D">D</span> | |
| 213 | - | <span class="p" title="gone.txt">gone.txt</span> | |
| 212 | + | <a href="#f2" title="gone.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 213 | + | <span class="st" data-st="D" title="deleted">D</span> | |
| 214 | + | <span class="nm"> | |
| 215 | + | <span class="p">gone.txt</span> | |
| 216 | + | <span class="d"> | |
| 217 | + | <bdi> | |
| 218 | + | </bdi> | |
| 219 | + | </span> | |
| 220 | + | </span> | |
| 214 | 221 | <span class="n"> | |
| 215 | 222 | <span class="plus">+0</span> | |
| 216 | 223 | <span class="minus">-1</span> | |
| 217 | 224 | </span> | |
| 218 | - | <span class="b" aria-hidden="true"> | |
| 219 | - | <span class="plus"> | |
| 220 | - | </span> | |
| 221 | - | <span class="minus">----------</span> | |
| 222 | - | </span> | |
| 223 | 225 | </a> | |
| 224 | 226 | </li> | |
| 225 | 227 | </ol> | |
| @@ -471,7 +473,10 @@ | |||
| 471 | 473 | k-split from:body -> ermite.layout('split'); | |
| 472 | 474 | k-unified from:body -> ermite.layout('unified'); | |
| 473 | 475 | k-collapse from:body -> ermite.collapse(); | |
| 474 | - | k-permalink from:body -> ermite.permalink()"> | |
| 476 | + | k-permalink from:body -> ermite.permalink(); | |
| 477 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 478 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 479 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 475 | 480 | </div> | |
| 476 | 481 | </body> | |
| 477 | 482 | </html> | |
M internal/app/testdata/web/commit-root.html +53 -45
| @@ -123,7 +123,7 @@ | |||
| 123 | 123 | </button> | |
| 124 | 124 | </div> | |
| 125 | 125 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 126 | - | <details class="dfile" id="f1" data-file open> | |
| 126 | + | <details class="dfile lnw-2" id="f1" data-file open> | |
| 127 | 127 | <summary> | |
| 128 | 128 | <span class="st">A</span> | |
| 129 | 129 | <span class="p">a.txt</span> | |
| @@ -245,7 +245,7 @@ | |||
| 245 | 245 | </table> | |
| 246 | 246 | </div> | |
| 247 | 247 | </details> | |
| 248 | - | <details class="dfile" id="f2" data-file open> | |
| 248 | + | <details class="dfile lnw-2" id="f2" data-file open> | |
| 249 | 249 | <summary> | |
| 250 | 250 | <span class="st">A</span> | |
| 251 | 251 | <span class="p">gone.txt</span> | |
| @@ -295,7 +295,7 @@ | |||
| 295 | 295 | </table> | |
| 296 | 296 | </div> | |
| 297 | 297 | </details> | |
| 298 | - | <details class="dfile" id="f3" data-file> | |
| 298 | + | <details class="dfile lnw-2" id="f3" data-file> | |
| 299 | 299 | <summary> | |
| 300 | 300 | <span class="st">A</span> | |
| 301 | 301 | <span class="p">logo.bin</span> | |
| @@ -305,7 +305,7 @@ | |||
| 305 | 305 | </summary> | |
| 306 | 306 | <p class="note pad">binary file: no text diff</p> | |
| 307 | 307 | </details> | |
| 308 | - | <details class="dfile" id="f4" data-file open> | |
| 308 | + | <details class="dfile lnw-2" id="f4" data-file open> | |
| 309 | 309 | <summary> | |
| 310 | 310 | <span class="st">A</span> | |
| 311 | 311 | <span class="p">old.txt</span> | |
| @@ -391,7 +391,7 @@ | |||
| 391 | 391 | </table> | |
| 392 | 392 | </div> | |
| 393 | 393 | </details> | |
| 394 | - | <details class="dfile" id="f5" data-file open> | |
| 394 | + | <details class="dfile lnw-2" id="f5" data-file open> | |
| 395 | 395 | <summary> | |
| 396 | 396 | <span class="st">A</span> | |
| 397 | 397 | <span class="p">run.sh</span> | |
| @@ -467,75 +467,80 @@ | |||
| 467 | 467 | </h2> | |
| 468 | 468 | <ol class="files" aria-label="Changed files"> | |
| 469 | 469 | <li> | |
| 470 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 471 | - | <span class="st" title="A">A</span> | |
| 472 | - | <span class="p" title="a.txt">a.txt</span> | |
| 470 | + | <a href="#f1" title="a.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 471 | + | <span class="st" data-st="A" title="added">A</span> | |
| 472 | + | <span class="nm"> | |
| 473 | + | <span class="p">a.txt</span> | |
| 474 | + | <span class="d"> | |
| 475 | + | <bdi> | |
| 476 | + | </bdi> | |
| 477 | + | </span> | |
| 478 | + | </span> | |
| 473 | 479 | <span class="n"> | |
| 474 | 480 | <span class="plus">+5</span> | |
| 475 | 481 | <span class="minus">-0</span> | |
| 476 | 482 | </span> | |
| 477 | - | <span class="b" aria-hidden="true"> | |
| 478 | - | <span class="plus">++++++++++</span> | |
| 479 | - | <span class="minus"> | |
| 480 | - | </span> | |
| 481 | - | </span> | |
| 482 | 483 | </a> | |
| 483 | 484 | </li> | |
| 484 | 485 | <li> | |
| 485 | - | <a href="#f2" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 486 | - | <span class="st" title="A">A</span> | |
| 487 | - | <span class="p" title="gone.txt">gone.txt</span> | |
| 486 | + | <a href="#f2" title="gone.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 487 | + | <span class="st" data-st="A" title="added">A</span> | |
| 488 | + | <span class="nm"> | |
| 489 | + | <span class="p">gone.txt</span> | |
| 490 | + | <span class="d"> | |
| 491 | + | <bdi> | |
| 492 | + | </bdi> | |
| 493 | + | </span> | |
| 494 | + | </span> | |
| 488 | 495 | <span class="n"> | |
| 489 | 496 | <span class="plus">+1</span> | |
| 490 | 497 | <span class="minus">-0</span> | |
| 491 | 498 | </span> | |
| 492 | - | <span class="b" aria-hidden="true"> | |
| 493 | - | <span class="plus">++</span> | |
| 494 | - | <span class="minus"> | |
| 495 | - | </span>········</span> | |
| 496 | 499 | </a> | |
| 497 | 500 | </li> | |
| 498 | 501 | <li> | |
| 499 | - | <a href="#f3" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 500 | - | <span class="st" title="A">A</span> | |
| 501 | - | <span class="p" title="logo.bin">logo.bin</span> | |
| 502 | - | <span class="n"> | |
| 503 | - | <span class="plus">+0</span> | |
| 504 | - | <span class="minus">-0</span> | |
| 502 | + | <a href="#f3" title="logo.bin" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 503 | + | <span class="st" data-st="A" title="added">A</span> | |
| 504 | + | <span class="nm"> | |
| 505 | + | <span class="p">logo.bin</span> | |
| 506 | + | <span class="d"> | |
| 507 | + | <bdi> | |
| 508 | + | </bdi> | |
| 505 | 509 | </span> | |
| 506 | - | <span class="b" aria-hidden="true"> | |
| 507 | - | <span class="plus"> | |
| 508 | 510 | </span> | |
| 509 | - | <span class="minus"> | |
| 510 | - | </span>bin</span> | |
| 511 | + | <span class="n mute">bin</span> | |
| 511 | 512 | </a> | |
| 512 | 513 | </li> | |
| 513 | 514 | <li> | |
| 514 | - | <a href="#f4" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 515 | - | <span class="st" title="A">A</span> | |
| 516 | - | <span class="p" title="old.txt">old.txt</span> | |
| 515 | + | <a href="#f4" title="old.txt" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 516 | + | <span class="st" data-st="A" title="added">A</span> | |
| 517 | + | <span class="nm"> | |
| 518 | + | <span class="p">old.txt</span> | |
| 519 | + | <span class="d"> | |
| 520 | + | <bdi> | |
| 521 | + | </bdi> | |
| 522 | + | </span> | |
| 523 | + | </span> | |
| 517 | 524 | <span class="n"> | |
| 518 | 525 | <span class="plus">+3</span> | |
| 519 | 526 | <span class="minus">-0</span> | |
| 520 | 527 | </span> | |
| 521 | - | <span class="b" aria-hidden="true"> | |
| 522 | - | <span class="plus">++++++</span> | |
| 523 | - | <span class="minus"> | |
| 524 | - | </span>····</span> | |
| 525 | 528 | </a> | |
| 526 | 529 | </li> | |
| 527 | 530 | <li> | |
| 528 | - | <a href="#f5" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 529 | - | <span class="st" title="A">A</span> | |
| 530 | - | <span class="p" title="run.sh">run.sh</span> | |
| 531 | + | <a href="#f5" title="run.sh" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 532 | + | <span class="st" data-st="A" title="added">A</span> | |
| 533 | + | <span class="nm"> | |
| 534 | + | <span class="p">run.sh</span> | |
| 535 | + | <span class="d"> | |
| 536 | + | <bdi> | |
| 537 | + | </bdi> | |
| 538 | + | </span> | |
| 539 | + | </span> | |
| 531 | 540 | <span class="n"> | |
| 532 | 541 | <span class="plus">+2</span> | |
| 533 | 542 | <span class="minus">-0</span> | |
| 534 | 543 | </span> | |
| 535 | - | <span class="b" aria-hidden="true"> | |
| 536 | - | <span class="plus">++++</span> | |
| 537 | - | <span class="minus"> | |
| 538 | - | </span>······</span> | |
| 539 | 544 | </a> | |
| 540 | 545 | </li> | |
| 541 | 546 | </ol> | |
| @@ -787,7 +792,10 @@ | |||
| 787 | 792 | k-split from:body -> ermite.layout('split'); | |
| 788 | 793 | k-unified from:body -> ermite.layout('unified'); | |
| 789 | 794 | k-collapse from:body -> ermite.collapse(); | |
| 790 | - | k-permalink from:body -> ermite.permalink()"> | |
| 795 | + | k-permalink from:body -> ermite.permalink(); | |
| 796 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 797 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 798 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 791 | 799 | </div> | |
| 792 | 800 | </body> | |
| 793 | 801 | </html> | |
M internal/app/testdata/web/commit-trojan.html +14 -10
| @@ -127,7 +127,7 @@ | |||
| 127 | 127 | </div> | |
| 128 | 128 | <p class="notice">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class="inv"><U+XXXX></span> markers, never applied.</p> | |
| 129 | 129 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 130 | - | <details class="dfile" id="f1" data-file open> | |
| 130 | + | <details class="dfile lnw-2" id="f1" data-file open> | |
| 131 | 131 | <summary> | |
| 132 | 132 | <span class="st">A</span> | |
| 133 | 133 | <span class="p">evil.go</span> | |
| @@ -317,18 +317,19 @@ | |||
| 317 | 317 | </h2> | |
| 318 | 318 | <ol class="files" aria-label="Changed files"> | |
| 319 | 319 | <li> | |
| 320 | - | <a href="#f1" hx-on:click="document.getElementById(this.hash.slice(1)).open = true"> | |
| 321 | - | <span class="st" title="A">A</span> | |
| 322 | - | <span class="p" title="evil.go">evil.go</span> | |
| 320 | + | <a href="#f1" title="evil.go" hx-on:click="(window.ermiteMR?.openFile || ermite.openFile)(this.hash.slice(1))"> | |
| 321 | + | <span class="st" data-st="A" title="added">A</span> | |
| 322 | + | <span class="nm"> | |
| 323 | + | <span class="p">evil.go</span> | |
| 324 | + | <span class="d"> | |
| 325 | + | <bdi> | |
| 326 | + | </bdi> | |
| 327 | + | </span> | |
| 328 | + | </span> | |
| 323 | 329 | <span class="n"> | |
| 324 | 330 | <span class="plus">+8</span> | |
| 325 | 331 | <span class="minus">-0</span> | |
| 326 | 332 | </span> | |
| 327 | - | <span class="b" aria-hidden="true"> | |
| 328 | - | <span class="plus">++++++++++</span> | |
| 329 | - | <span class="minus"> | |
| 330 | - | </span> | |
| 331 | - | </span> | |
| 332 | 333 | </a> | |
| 333 | 334 | </li> | |
| 334 | 335 | </ol> | |
| @@ -580,7 +581,10 @@ | |||
| 580 | 581 | k-split from:body -> ermite.layout('split'); | |
| 581 | 582 | k-unified from:body -> ermite.layout('unified'); | |
| 582 | 583 | k-collapse from:body -> ermite.collapse(); | |
| 583 | - | k-permalink from:body -> ermite.permalink()"> | |
| 584 | + | k-permalink from:body -> ermite.permalink(); | |
| 585 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 586 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 587 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 584 | 588 | </div> | |
| 585 | 589 | </body> | |
| 586 | 590 | </html> | |
M internal/app/testdata/web/commits-page2.html +4 -1
| @@ -391,7 +391,10 @@ | |||
| 391 | 391 | k-split from:body -> ermite.layout('split'); | |
| 392 | 392 | k-unified from:body -> ermite.layout('unified'); | |
| 393 | 393 | k-collapse from:body -> ermite.collapse(); | |
| 394 | - | k-permalink from:body -> ermite.permalink()"> | |
| 394 | + | k-permalink from:body -> ermite.permalink(); | |
| 395 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 396 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 397 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 395 | 398 | </div> | |
| 396 | 399 | </body> | |
| 397 | 400 | </html> | |
M internal/app/testdata/web/commits-path.html +4 -1
| @@ -406,7 +406,10 @@ | |||
| 406 | 406 | k-split from:body -> ermite.layout('split'); | |
| 407 | 407 | k-unified from:body -> ermite.layout('unified'); | |
| 408 | 408 | k-collapse from:body -> ermite.collapse(); | |
| 409 | - | k-permalink from:body -> ermite.permalink()"> | |
| 409 | + | k-permalink from:body -> ermite.permalink(); | |
| 410 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 411 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 412 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 410 | 413 | </div> | |
| 411 | 414 | </body> | |
| 412 | 415 | </html> | |
M internal/app/testdata/web/commits-root.html +4 -1
| @@ -1291,7 +1291,10 @@ | |||
| 1291 | 1291 | k-split from:body -> ermite.layout('split'); | |
| 1292 | 1292 | k-unified from:body -> ermite.layout('unified'); | |
| 1293 | 1293 | k-collapse from:body -> ermite.collapse(); | |
| 1294 | - | k-permalink from:body -> ermite.permalink()"> | |
| 1294 | + | k-permalink from:body -> ermite.permalink(); | |
| 1295 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 1296 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 1297 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 1295 | 1298 | </div> | |
| 1296 | 1299 | </body> | |
| 1297 | 1300 | </html> | |
M internal/app/testdata/web/commits-small.html +4 -1
| @@ -572,7 +572,10 @@ | |||
| 572 | 572 | k-split from:body -> ermite.layout('split'); | |
| 573 | 573 | k-unified from:body -> ermite.layout('unified'); | |
| 574 | 574 | k-collapse from:body -> ermite.collapse(); | |
| 575 | - | k-permalink from:body -> ermite.permalink()"> | |
| 575 | + | k-permalink from:body -> ermite.permalink(); | |
| 576 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 577 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 578 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 576 | 579 | </div> | |
| 577 | 580 | </body> | |
| 578 | 581 | </html> | |
M internal/app/testdata/web/empty.html +4 -1
| @@ -240,7 +240,10 @@ | |||
| 240 | 240 | k-split from:body -> ermite.layout('split'); | |
| 241 | 241 | k-unified from:body -> ermite.layout('unified'); | |
| 242 | 242 | k-collapse from:body -> ermite.collapse(); | |
| 243 | - | k-permalink from:body -> ermite.permalink()"> | |
| 243 | + | k-permalink from:body -> ermite.permalink(); | |
| 244 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 245 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 246 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 244 | 247 | </div> | |
| 245 | 248 | </body> | |
| 246 | 249 | </html> | |
M internal/app/testdata/web/home.html +4 -1
| @@ -278,7 +278,10 @@ | |||
| 278 | 278 | k-split from:body -> ermite.layout('split'); | |
| 279 | 279 | k-unified from:body -> ermite.layout('unified'); | |
| 280 | 280 | k-collapse from:body -> ermite.collapse(); | |
| 281 | - | k-permalink from:body -> ermite.permalink()"> | |
| 281 | + | k-permalink from:body -> ermite.permalink(); | |
| 282 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 283 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 284 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 282 | 285 | </div> | |
| 283 | 286 | </body> | |
| 284 | 287 | </html> | |
M internal/app/testdata/web/not-found-page.html +4 -1
| @@ -223,7 +223,10 @@ | |||
| 223 | 223 | k-split from:body -> ermite.layout('split'); | |
| 224 | 224 | k-unified from:body -> ermite.layout('unified'); | |
| 225 | 225 | k-collapse from:body -> ermite.collapse(); | |
| 226 | - | k-permalink from:body -> ermite.permalink()"> | |
| 226 | + | k-permalink from:body -> ermite.permalink(); | |
| 227 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 228 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 229 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 227 | 230 | </div> | |
| 228 | 231 | </body> | |
| 229 | 232 | </html> | |
M internal/app/testdata/web/not-found.html +4 -1
| @@ -223,7 +223,10 @@ | |||
| 223 | 223 | k-split from:body -> ermite.layout('split'); | |
| 224 | 224 | k-unified from:body -> ermite.layout('unified'); | |
| 225 | 225 | k-collapse from:body -> ermite.collapse(); | |
| 226 | - | k-permalink from:body -> ermite.permalink()"> | |
| 226 | + | k-permalink from:body -> ermite.permalink(); | |
| 227 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 228 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 229 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 227 | 230 | </div> | |
| 228 | 231 | </body> | |
| 229 | 232 | </html> | |
M internal/app/testdata/web/refs.html +4 -1
| @@ -458,7 +458,10 @@ | |||
| 458 | 458 | k-split from:body -> ermite.layout('split'); | |
| 459 | 459 | k-unified from:body -> ermite.layout('unified'); | |
| 460 | 460 | k-collapse from:body -> ermite.collapse(); | |
| 461 | - | k-permalink from:body -> ermite.permalink()"> | |
| 461 | + | k-permalink from:body -> ermite.permalink(); | |
| 462 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 463 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 464 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 462 | 465 | </div> | |
| 463 | 466 | </body> | |
| 464 | 467 | </html> | |
M internal/app/testdata/web/summary.html +4 -1
| @@ -487,7 +487,10 @@ | |||
| 487 | 487 | k-split from:body -> ermite.layout('split'); | |
| 488 | 488 | k-unified from:body -> ermite.layout('unified'); | |
| 489 | 489 | k-collapse from:body -> ermite.collapse(); | |
| 490 | - | k-permalink from:body -> ermite.permalink()"> | |
| 490 | + | k-permalink from:body -> ermite.permalink(); | |
| 491 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 492 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 493 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 491 | 494 | </div> | |
| 492 | 495 | </body> | |
| 493 | 496 | </html> | |
M internal/app/testdata/web/tree-nested.html +4 -1
| @@ -394,7 +394,10 @@ | |||
| 394 | 394 | k-split from:body -> ermite.layout('split'); | |
| 395 | 395 | k-unified from:body -> ermite.layout('unified'); | |
| 396 | 396 | k-collapse from:body -> ermite.collapse(); | |
| 397 | - | k-permalink from:body -> ermite.permalink()"> | |
| 397 | + | k-permalink from:body -> ermite.permalink(); | |
| 398 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 399 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 400 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 398 | 401 | </div> | |
| 399 | 402 | </body> | |
| 400 | 403 | </html> | |
M internal/app/testdata/web/tree-root.html +4 -1
| @@ -621,7 +621,10 @@ | |||
| 621 | 621 | k-split from:body -> ermite.layout('split'); | |
| 622 | 622 | k-unified from:body -> ermite.layout('unified'); | |
| 623 | 623 | k-collapse from:body -> ermite.collapse(); | |
| 624 | - | k-permalink from:body -> ermite.permalink()"> | |
| 624 | + | k-permalink from:body -> ermite.permalink(); | |
| 625 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 626 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 627 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 625 | 628 | </div> | |
| 626 | 629 | </body> | |
| 627 | 630 | </html> | |
M internal/app/testdata/web/tree-slash-ref.html +4 -1
| @@ -391,7 +391,10 @@ | |||
| 391 | 391 | k-split from:body -> ermite.layout('split'); | |
| 392 | 392 | k-unified from:body -> ermite.layout('unified'); | |
| 393 | 393 | k-collapse from:body -> ermite.collapse(); | |
| 394 | - | k-permalink from:body -> ermite.permalink()"> | |
| 394 | + | k-permalink from:body -> ermite.permalink(); | |
| 395 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 396 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 397 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 395 | 398 | </div> | |
| 396 | 399 | </body> | |
| 397 | 400 | </html> | |
M internal/app/testdata/web/tree-subdir.html +4 -1
| @@ -416,7 +416,10 @@ | |||
| 416 | 416 | k-split from:body -> ermite.layout('split'); | |
| 417 | 417 | k-unified from:body -> ermite.layout('unified'); | |
| 418 | 418 | k-collapse from:body -> ermite.collapse(); | |
| 419 | - | k-permalink from:body -> ermite.permalink()"> | |
| 419 | + | k-permalink from:body -> ermite.permalink(); | |
| 420 | + | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 421 | + | k-read from:body -> window.ermiteMR?.markRead(); | |
| 422 | + | k-gf from:body -> window.ermiteMR?.focusFiles()"> | |
| 420 | 423 | </div> | |
| 421 | 424 | </body> | |
| 422 | 425 | </html> | |
M internal/app/web_mr_test.go +92 -7
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | package app | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "fmt" | |
| 6 | 7 | "html" | |
| 7 | 8 | "net/http" | |
| 8 | 9 | "net/url" | |
| @@ -141,7 +142,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||
| 141 | 142 | assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") | |
| 142 | 143 | assert.NotContains(t, mr.body, "<form", "an anonymous visitor gets no form") | |
| 143 | 144 | assert.NotContains(t, mr.body, "ermite mr merge", "nor the merge line") | |
| 144 | - | assert.NotContains(t, mr.body, ">Merge</button>") | |
| 145 | + | assert.NotContains(t, mr.body, `id="merge-menu"`) | |
| 145 | 146 | assert.Contains(t, mr.body, `href="/proj/commit/`+head+`"`, "the commits are in the rail") | |
| 146 | 147 | assert.NotContains(t, strings.ToLower(mr.body), "proposal") | |
| 147 | 148 | assertNoInline(t, mr.body) | |
| @@ -157,10 +158,11 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||
| 157 | 158 | assert.Contains(t, own.hdr.Values("Vary"), "Cookie") | |
| 158 | 159 | assert.Equal(t, head, formHead(t, own.body)) | |
| 159 | 160 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/merge"`) | |
| 160 | - | assert.Contains(t, own.body, `<option value="fast-forward">`) | |
| 161 | - | assert.Contains(t, own.body, `<option value="squash">`) | |
| 161 | + | assert.Contains(t, own.body, `id="merge-menu"`, "the merge is a menu of the bar above the diff") | |
| 162 | + | assert.Contains(t, own.body, `<input type="radio" name="mode" value="fast-forward" checked>`, "the first mode is the default") | |
| 163 | + | assert.Contains(t, own.body, `<input type="radio" name="mode" value="squash">`) | |
| 162 | 164 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head) | |
| 163 | - | assert.Contains(t, own.body, `name="delete_branch" value="1"> delete feat/search after the merge`) | |
| 165 | + | assert.Contains(t, own.body, `name="delete_branch" value="1"> delete feat/search after the merge <kbd aria-hidden="true">D</kbd>`) | |
| 164 | 166 | assert.NotContains(t, own.body, `name="delete_branch" value="1" checked>`, "unchecked by default") | |
| 165 | 167 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --squash") | |
| 166 | 168 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/comments"`) | |
| @@ -313,7 +315,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 313 | 315 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/read"`) | |
| 314 | 316 | res = b.form(base+"/read", token, url.Values{"head_id": {head1}}) | |
| 315 | 317 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | |
| 316 | - | assert.NotContains(t, b.get(base, as(token)).body, ">Mark read</button>", "the head is the one that was read") | |
| 318 | + | assert.NotContains(t, b.get(base, as(token)).body, "data-read>Mark read", "the head is the one that was read") | |
| 317 | 319 | r.writeFile(r.wt, "extra.txt", []byte("extra\n")) | |
| 318 | 320 | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | |
| 319 | 321 | head2 := r.headOf("feat/search") | |
| @@ -322,7 +324,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 322 | 324 | assert.Contains(t, all.body, `href="/proj/merge-requests/1/since"`) | |
| 323 | 325 | assert.Contains(t, all.body, "since your last read") | |
| 324 | 326 | assert.Contains(t, all.body, "<li data-new>", "the commit pushed since is marked") | |
| 325 | - | assert.Contains(t, all.body, ">Mark read</button>") | |
| 327 | + | assert.Contains(t, all.body, "data-read>Mark read") | |
| 326 | 328 | assert.Contains(t, all.body, "outdated", "the notes were written on the earlier head") | |
| 327 | 329 | since := b.get(base+"/since", as(token)) | |
| 328 | 330 | require.Equal(t, 200, since.status, since.body) | |
| @@ -357,7 +359,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) { | |||
| 357 | 359 | assert.Contains(t, done.body, "fast-forward") | |
| 358 | 360 | assert.Contains(t, done.body, `href="/proj/commit/`+head2+`"`) | |
| 359 | 361 | assert.NotContains(t, done.body, "<form") | |
| 360 | - | assert.NotContains(t, b.get(base, as(token)).body, ">Merge</button>", "nothing to merge any more") | |
| 362 | + | assert.NotContains(t, b.get(base, as(token)).body, `id="merge-menu"`, "nothing to merge any more") | |
| 361 | 363 | // Writes on a merged request are refused with a page. | |
| 362 | 364 | late := b.form(base+"/comments", token, url.Values{"text": {"too late"}}) | |
| 363 | 365 | assert.Equal(t, http.StatusConflict, late.status) | |
| @@ -628,3 +630,86 @@ func TestWebMergeRequestSinceAfterForcePush(t *testing.T) { | |||
| 628 | 630 | assert.Contains(t, lost.body, "is gone, the branch was rewritten") | |
| 629 | 631 | assert.Contains(t, lost.body, "other.txt", "the whole change is shown") | |
| 630 | 632 | } | |
| 633 | + | ||
| 634 | + | // A request of many files: the page lists every file and holds the hunks of | |
| 635 | + | // the first window only; the other files are placeholders that load their | |
| 636 | + | // window from /files, which carries the base and the head of the diff. A | |
| 637 | + | // comment on a far file comes with its window; a moved head answers 409; the | |
| 638 | + | // page of a window (?from=, the links of a page without scripts) shows its | |
| 639 | + | // hunks and its line forms. | |
| 640 | + | func TestWebMergeRequestFileWindows(t *testing.T) { | |
| 641 | + | r := newMRRepo(t, domain.VisibilityPublic) | |
| 642 | + | b := browser{r.harness} | |
| 643 | + | token := r.signIn(t) | |
| 644 | + | const n = 60 | |
| 645 | + | for i := range n { | |
| 646 | + | r.writeFile(r.wt, fmt.Sprintf("many/f%02d.txt", i), []byte(fmt.Sprintf("line %d\n", i))) | |
| 647 | + | } | |
| 648 | + | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | |
| 649 | + | head := r.headOf("feat/search") | |
| 650 | + | r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Many").ok(t) | |
| 651 | + | r.cli(t, adminToken, "mr", "comment", "proj", "1", "--path", "many/f55.txt", "--line", "1", "-m", "far note").ok(t) | |
| 652 | + | ||
| 653 | + | page := b.get("/proj/merge-requests/1", nil) | |
| 654 | + | require.Equal(t, 200, page.status) | |
| 655 | + | assert.Equal(t, n+1, strings.Count(page.body, `<li><a href="#f`), "every file is listed") | |
| 656 | + | assert.Equal(t, n+1-25, strings.Count(page.body, `class="dlazy`), "the files past the first window are placeholders") | |
| 657 | + | assert.NotContains(t, page.body, "far note", "the comment of a far file comes with its window") | |
| 658 | + | assert.Contains(t, page.body, `data-notes="c1"`, "its placeholder knows it") | |
| 659 | + | assert.Contains(t, page.body, `<span class="cn" title="1 open comment">1</span>`, "the list counts it") | |
| 660 | + | m := reWindowSrc.FindStringSubmatch(page.body) | |
| 661 | + | require.Len(t, m, 2, "a placeholder holds the address of its window") | |
| 662 | + | src := html.UnescapeString(m[1]) | |
| 663 | + | assert.Contains(t, src, "head="+head) | |
| 664 | + | ||
| 665 | + | win := b.get(strings.Replace(src, "from=25", "from=50", 1), nil) | |
| 666 | + | require.Equal(t, 200, win.status, win.body) | |
| 667 | + | assert.Contains(t, win.body, "far note") | |
| 668 | + | assert.Contains(t, win.body, `line 55</td>`) | |
| 669 | + | assert.NotContains(t, win.body, "<html", "a window is a fragment") | |
| 670 | + | assert.NotContains(t, win.body, `class="dlazy`) | |
| 671 | + | // The window renders the layouts of the page: both when it switches them, | |
| 672 | + | // the one it shows otherwise. | |
| 673 | + | both := b.get(src+"&view=split&layouts=both", nil) | |
| 674 | + | assert.Contains(t, both.body, `class="dt uni"`) | |
| 675 | + | assert.Contains(t, both.body, `class="dt spl"`) | |
| 676 | + | one := b.get(src+"&view=split&layouts=one", nil) | |
| 677 | + | assert.NotContains(t, one.body, `class="dt uni"`) | |
| 678 | + | assert.Contains(t, one.body, `class="dt spl"`) | |
| 679 | + | assert.Equal(t, 404, b.get(strings.Replace(src, "from=25", "from=7", 1), nil).status, "not a window start") | |
| 680 | + | assert.Equal(t, 404, b.get(strings.Replace(src, "from=25", "from=100", 1), nil).status, "past the files") | |
| 681 | + | ||
| 682 | + | // The page of a window, without scripts: its hunks, the line forms of the | |
| 683 | + | // owner point back to it. | |
| 684 | + | own := b.get("/proj/merge-requests/1?from=50", as(token)) | |
| 685 | + | require.Equal(t, 200, own.status) | |
| 686 | + | assert.Contains(t, own.body, "line 55</td>") | |
| 687 | + | assert.Contains(t, own.body, `href="/proj/merge-requests/1?from=50&on=new:1:many%2Ff55.txt#f`) | |
| 688 | + | assert.NotContains(t, own.body, "line 0</td>", "the first window is a placeholder there") | |
| 689 | + | assert.Contains(t, own.body, `href="/proj/merge-requests/1?view=split&from=50"`, "the layout links keep the window") | |
| 690 | + | assert.Equal(t, 404, b.get("/proj/merge-requests/1?from=3", nil).status) | |
| 691 | + | ||
| 692 | + | // A comment on a line of a far window: its form, its cancel link and the | |
| 693 | + | // answer return to that window. | |
| 694 | + | form := b.get("/proj/merge-requests/1?from=50&on=new:1:many%2Ff55.txt", as(token)) | |
| 695 | + | require.Equal(t, 200, form.status) | |
| 696 | + | assert.Contains(t, form.body, `name="from" value="50"`) | |
| 697 | + | assert.Contains(t, form.body, `class="btn" href="/proj/merge-requests/1?from=50#f`) | |
| 698 | + | res := b.form("/proj/merge-requests/1/comments", token, url.Values{"text": {"near it"}, "path": {"many/f55.txt"}, "line": {"1"}, "side": {"new"}, "from": {"50"}}) | |
| 699 | + | require.Equal(t, http.StatusSeeOther, res.status, res.body) | |
| 700 | + | assert.Equal(t, "/proj/merge-requests/1?from=50#c2", res.hdr.Get("Location")) | |
| 701 | + | assert.Contains(t, b.get("/proj/merge-requests/1?from=50", as(token)).body, `<input type="hidden" name="from" value="50">`, "its resolve form too") | |
| 702 | + | res = b.form("/proj/merge-requests/1/comments/2/resolve", token, url.Values{"resolved": {"true"}, "from": {"50"}}) | |
| 703 | + | assert.Equal(t, "/proj/merge-requests/1?from=50#c2", res.hdr.Get("Location")) | |
| 704 | + | res = b.form("/proj/merge-requests/1/comments/2/resolve", token, url.Values{"resolved": {"false"}, "from": {"7"}}) | |
| 705 | + | assert.Equal(t, "/proj/merge-requests/1#c2", res.hdr.Get("Location"), "a window that is none is dropped") | |
| 706 | + | ||
| 707 | + | // The head moved: the window says to reload, never a file of another diff. | |
| 708 | + | r.writeFile(r.wt, "later.txt", []byte("later\n")) | |
| 709 | + | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | |
| 710 | + | stale := b.get(src, nil) | |
| 711 | + | assert.Equal(t, http.StatusConflict, stale.status) | |
| 712 | + | assert.Contains(t, stale.body, "data-moved") | |
| 713 | + | } | |
| 714 | + | ||
| 715 | + | var reWindowSrc = regexp.MustCompile(`data-src="([^"]*from=25[^"]*)"`) | |
M internal/domain/git.go +14 -3
| @@ -493,7 +493,8 @@ type FileDiff struct { | |||
| 493 | 493 | // Similarity is a percentage for a renamed or copied file, 0 otherwise. | |
| 494 | 494 | Similarity int | |
| 495 | 495 | // Truncated means the file exceeded a diff limit: Hunks is empty although | |
| 496 | - | // the file changed. Additions and Deletions stay accurate. | |
| 496 | + | // the file changed. Additions and Deletions stay accurate. A file outside | |
| 497 | + | // the DiffLimits window has no Hunks and is not Truncated. | |
| 497 | 498 | Truncated bool | |
| 498 | 499 | Hunks []Hunk | |
| 499 | 500 | } | |
| @@ -502,9 +503,11 @@ type FileDiff struct { | |||
| 502 | 503 | // parent, or against the empty tree for a root commit. | |
| 503 | 504 | type Diff struct { | |
| 504 | 505 | // Files are ordered as Git reports them (by path). Files past | |
| 505 | - | // DiffLimits.MaxFiles are left out. | |
| 506 | + | // DiffLimits.MaxFiles (when it is not 0) are left out. | |
| 506 | 507 | Files []FileDiff | |
| 507 | 508 | // Truncated is true when files were left out or a listed file is truncated. | |
| 509 | + | // Files listed without hunks because they are outside the DiffLimits window | |
| 510 | + | // do not make it true. | |
| 508 | 511 | Truncated bool | |
| 509 | 512 | } | |
| 510 | 513 | ||
| @@ -523,7 +526,8 @@ type CommitDetail struct { | |||
| 523 | 526 | ||
| 524 | 527 | // DiffLimits bound the size of a Diff. Lines are hunk lines, context included. | |
| 525 | 528 | type DiffLimits struct { | |
| 526 | - | // MaxFiles is the number of files kept. | |
| 529 | + | // MaxFiles is the number of files kept; 0 means no limit on the number of | |
| 530 | + | // files (every changed file is listed). | |
| 527 | 531 | MaxFiles int | |
| 528 | 532 | // MaxFileLines is the most lines one file may have; a file over it is | |
| 529 | 533 | // truncated. | |
| @@ -536,6 +540,13 @@ type DiffLimits struct { | |||
| 536 | 540 | // the memory of a diff whose lines are very long; files are truncated | |
| 537 | 541 | // like for MaxTotalLines. | |
| 538 | 542 | MaxTotalBytes int | |
| 543 | + | // From and To select the files whose hunks are read: those with an index in | |
| 544 | + | // [From, To), counted from 0 in the order of Diff.Files. The other files | |
| 545 | + | // are listed (paths, status, modes, ids, Binary, Additions, Deletions, | |
| 546 | + | // Similarity) without hunks; they are not Truncated and use none of | |
| 547 | + | // MaxTotalLines and MaxTotalBytes. To == 0 means no end, so the zero values | |
| 548 | + | // select every file. MaxFileLines applies to each file of the window. | |
| 549 | + | From, To int | |
| 539 | 550 | } | |
| 540 | 551 | ||
| 541 | 552 | // DefaultDiffLimits are the limits of the API contract: 300 files, 2,000 | |
M internal/ports/dto.go +20 -0
| @@ -194,6 +194,26 @@ type MergeRequestDetail struct { | |||
| 194 | 194 | LastReadLost bool | |
| 195 | 195 | } | |
| 196 | 196 | ||
| 197 | + | // GetMergeRequestOptions say which diff MergeRequestService.Get reads. | |
| 198 | + | type GetMergeRequestOptions struct { | |
| 199 | + | // SinceLastRead compares the head recorded by MarkRead with the head (see Get). | |
| 200 | + | SinceLastRead bool | |
| 201 | + | // Files, when set, lists every changed file, without the file-count | |
| 202 | + | // limit, and reads the hunks of the files in the window only, each within | |
| 203 | + | // the per-file and the total limits of domain.DefaultDiffLimits. Unset, | |
| 204 | + | // the diff is bounded by domain.DefaultDiffLimits (the API contract). | |
| 205 | + | Files *FileWindow | |
| 206 | + | // Base and Head, when not zero, are the diff the caller shows: Get | |
| 207 | + | // returns domain.ErrConflict when the request's diff base or head differ | |
| 208 | + | // (the branch moved, a read was marked). | |
| 209 | + | Base, Head domain.ObjectID | |
| 210 | + | // DiffOnly skips the commits of the request (Commits stays empty). | |
| 211 | + | DiffOnly bool | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // FileWindow is the range [From, To) of files whose hunks are read. | |
| 215 | + | type FileWindow struct{ From, To int } | |
| 216 | + | ||
| 197 | 217 | // AddCommentParams describes the comment to add. | |
| 198 | 218 | type AddCommentParams struct { | |
| 199 | 219 | // Path, Line and Side designate a line of the diff; all empty for a | |
M internal/ports/mocks/MergeRequestService.go +15 -15
| @@ -361,8 +361,8 @@ func (_c *MergeRequestService_Create_Call) RunAndReturn(run func(ctx context.Con | |||
| 361 | 361 | } | |
| 362 | 362 | ||
| 363 | 363 | // Get provides a mock function for the type MergeRequestService | |
| 364 | - | func (_mock *MergeRequestService) Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, sinceLastRead bool) (ports.MergeRequestDetail, error) { | |
| 365 | - | ret := _mock.Called(ctx, p, name, number, sinceLastRead) | |
| 364 | + | func (_mock *MergeRequestService) Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, opts ports.GetMergeRequestOptions) (ports.MergeRequestDetail, error) { | |
| 365 | + | ret := _mock.Called(ctx, p, name, number, opts) | |
| 366 | 366 | ||
| 367 | 367 | if len(ret) == 0 { | |
| 368 | 368 | panic("no return value specified for Get") | |
| @@ -370,16 +370,16 @@ func (_mock *MergeRequestService) Get(ctx context.Context, p domain.Principal, n | |||
| 370 | 370 | ||
| 371 | 371 | var r0 ports.MergeRequestDetail | |
| 372 | 372 | var r1 error | |
| 373 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, bool) (ports.MergeRequestDetail, error)); ok { | |
| 374 | - | return returnFunc(ctx, p, name, number, sinceLastRead) | |
| 373 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.GetMergeRequestOptions) (ports.MergeRequestDetail, error)); ok { | |
| 374 | + | return returnFunc(ctx, p, name, number, opts) | |
| 375 | 375 | } | |
| 376 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, bool) ports.MergeRequestDetail); ok { | |
| 377 | - | r0 = returnFunc(ctx, p, name, number, sinceLastRead) | |
| 376 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.GetMergeRequestOptions) ports.MergeRequestDetail); ok { | |
| 377 | + | r0 = returnFunc(ctx, p, name, number, opts) | |
| 378 | 378 | } else { | |
| 379 | 379 | r0 = ret.Get(0).(ports.MergeRequestDetail) | |
| 380 | 380 | } | |
| 381 | - | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, bool) error); ok { | |
| 382 | - | r1 = returnFunc(ctx, p, name, number, sinceLastRead) | |
| 381 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.GetMergeRequestOptions) error); ok { | |
| 382 | + | r1 = returnFunc(ctx, p, name, number, opts) | |
| 383 | 383 | } else { | |
| 384 | 384 | r1 = ret.Error(1) | |
| 385 | 385 | } | |
| @@ -396,12 +396,12 @@ type MergeRequestService_Get_Call struct { | |||
| 396 | 396 | // - p domain.Principal | |
| 397 | 397 | // - name domain.RepoName | |
| 398 | 398 | // - number int64 | |
| 399 | - | // - sinceLastRead bool | |
| 400 | - | func (_e *MergeRequestService_Expecter) Get(ctx any, p any, name any, number any, sinceLastRead any) *MergeRequestService_Get_Call { | |
| 401 | - | return &MergeRequestService_Get_Call{Call: _e.mock.On("Get", ctx, p, name, number, sinceLastRead)} | |
| 399 | + | // - opts ports.GetMergeRequestOptions | |
| 400 | + | func (_e *MergeRequestService_Expecter) Get(ctx any, p any, name any, number any, opts any) *MergeRequestService_Get_Call { | |
| 401 | + | return &MergeRequestService_Get_Call{Call: _e.mock.On("Get", ctx, p, name, number, opts)} | |
| 402 | 402 | } | |
| 403 | 403 | ||
| 404 | - | func (_c *MergeRequestService_Get_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, sinceLastRead bool)) *MergeRequestService_Get_Call { | |
| 404 | + | func (_c *MergeRequestService_Get_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, opts ports.GetMergeRequestOptions)) *MergeRequestService_Get_Call { | |
| 405 | 405 | _c.Call.Run(func(args mock.Arguments) { | |
| 406 | 406 | var arg0 context.Context | |
| 407 | 407 | if args[0] != nil { | |
| @@ -419,9 +419,9 @@ func (_c *MergeRequestService_Get_Call) Run(run func(ctx context.Context, p doma | |||
| 419 | 419 | if args[3] != nil { | |
| 420 | 420 | arg3 = args[3].(int64) | |
| 421 | 421 | } | |
| 422 | - | var arg4 bool | |
| 422 | + | var arg4 ports.GetMergeRequestOptions | |
| 423 | 423 | if args[4] != nil { | |
| 424 | - | arg4 = args[4].(bool) | |
| 424 | + | arg4 = args[4].(ports.GetMergeRequestOptions) | |
| 425 | 425 | } | |
| 426 | 426 | run( | |
| 427 | 427 | arg0, | |
| @@ -439,7 +439,7 @@ func (_c *MergeRequestService_Get_Call) Return(mergeRequestDetail ports.MergeReq | |||
| 439 | 439 | return _c | |
| 440 | 440 | } | |
| 441 | 441 | ||
| 442 | - | func (_c *MergeRequestService_Get_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, sinceLastRead bool) (ports.MergeRequestDetail, error)) *MergeRequestService_Get_Call { | |
| 442 | + | func (_c *MergeRequestService_Get_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, opts ports.GetMergeRequestOptions) (ports.MergeRequestDetail, error)) *MergeRequestService_Get_Call { | |
| 443 | 443 | _c.Call.Return(run) | |
| 444 | 444 | return _c | |
| 445 | 445 | } | |
M internal/ports/services.go +1 -1
| @@ -108,7 +108,7 @@ type EventService interface { | |||
| 108 | 108 | type MergeRequestService interface { | |
| 109 | 109 | Create(ctx context.Context, p domain.Principal, name domain.RepoName, params CreateMergeRequestParams) (domain.MergeRequest, error) | |
| 110 | 110 | List(ctx context.Context, p domain.Principal, name domain.RepoName, params ListMergeRequestsParams) (MergeRequestPage, error) | |
| 111 | - | Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, sinceLastRead bool) (MergeRequestDetail, error) | |
| 111 | + | Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, opts GetMergeRequestOptions) (MergeRequestDetail, error) | |
| 112 | 112 | AddComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params AddCommentParams) (domain.Comment, error) | |
| 113 | 113 | ResolveComment(ctx context.Context, p domain.Principal, name domain.RepoName, number, commentID int64, resolved bool) (domain.Comment, error) | |
| 114 | 114 | Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error) | |
M internal/usecase/ci_actions_test.go +1 -1
| @@ -75,7 +75,7 @@ func TestPickRemoteRef(t *testing.T) { | |||
| 75 | 75 | ||
| 76 | 76 | // countingRemote answers ListRefs from a function and counts the calls. | |
| 77 | 77 | type countingRemote struct { | |
| 78 | - | calls int | |
| 78 | + | calls int | |
| 79 | 79 | answer func() ([]ports.RemoteRef, error) | |
| 80 | 80 | } | |
| 81 | 81 | ||
M internal/usecase/mergerequests.go +72 -14
| @@ -7,6 +7,8 @@ import ( | |||
| 7 | 7 | "encoding/base64" | |
| 8 | 8 | "errors" | |
| 9 | 9 | "fmt" | |
| 10 | + | "math" | |
| 11 | + | "slices" | |
| 10 | 12 | "strconv" | |
| 11 | 13 | "strings" | |
| 12 | 14 | "sync" | |
| @@ -419,12 +421,24 @@ func (m *MergeRequests) get(ctx context.Context, p domain.Principal, name domain | |||
| 419 | 421 | // Get returns a merge request with its commits, its diff, its comments and the | |
| 420 | 422 | // read position. The diff compares the merge base of the target and the head | |
| 421 | 423 | // with the head ("target...head"), within domain.DefaultDiffLimits. With | |
| 422 | - | // sinceLastRead it compares the head recorded by MarkRead with the current | |
| 423 | - | // head instead, which works after a force-push as long as the recorded commit | |
| 424 | - | // still exists; when none was recorded the diff is the default one, and when | |
| 425 | - | // the recorded commit is gone too, with LastReadLost set. A merged or closed | |
| 426 | - | // request keeps the head and the base it had. | |
| 427 | - | func (m *MergeRequests) Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, sinceLastRead bool) (ports.MergeRequestDetail, error) { | |
| 424 | + | // opts.SinceLastRead it compares the head recorded by MarkRead with the | |
| 425 | + | // current head instead, which works after a force-push as long as the recorded | |
| 426 | + | // commit still exists; when none was recorded the diff is the default one, and | |
| 427 | + | // when the recorded commit is gone too, with LastReadLost set. A merged or | |
| 428 | + | // closed request keeps the head and the base it had. | |
| 429 | + | // | |
| 430 | + | // opts.Files lists every changed file and reads the hunks of a window of them | |
| 431 | + | // only; opts.Base and opts.Head make Get fail with domain.ErrConflict when the | |
| 432 | + | // diff is no longer the one the caller shows (before any diff is read); | |
| 433 | + | // opts.DiffOnly skips the commits. An invalid window gives domain.ErrInvalid. | |
| 434 | + | func (m *MergeRequests) Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, opts ports.GetMergeRequestOptions) (ports.MergeRequestDetail, error) { | |
| 435 | + | limits := domain.DefaultDiffLimits() | |
| 436 | + | if w := opts.Files; w != nil { | |
| 437 | + | if w.From < 0 || w.To < 0 || (w.To != 0 && w.To < w.From) { | |
| 438 | + | return ports.MergeRequestDetail{}, fmt.Errorf("%w: file window [%d, %d)", domain.ErrInvalid, w.From, w.To) | |
| 439 | + | } | |
| 440 | + | limits.MaxFiles, limits.From, limits.To = 0, w.From, w.To | |
| 441 | + | } | |
| 428 | 442 | _, mr, err := m.get(ctx, p, name, number) | |
| 429 | 443 | if err != nil { | |
| 430 | 444 | return ports.MergeRequestDetail{}, err | |
| @@ -432,6 +446,16 @@ func (m *MergeRequests) Get(ctx context.Context, p domain.Principal, name domain | |||
| 432 | 446 | if mr, err = m.withHead(ctx, mr); err != nil { | |
| 433 | 447 | return ports.MergeRequestDetail{}, err | |
| 434 | 448 | } | |
| 449 | + | if !opts.Head.IsZero() && opts.Head != mr.HeadID { | |
| 450 | + | return ports.MergeRequestDetail{}, fmt.Errorf("%w: the head of !%d moved to %s", domain.ErrConflict, number, mr.HeadID) | |
| 451 | + | } | |
| 452 | + | // checkBase fails when the caller shows another diff base than base. | |
| 453 | + | checkBase := func(base domain.ObjectID) error { | |
| 454 | + | if !opts.Base.IsZero() && opts.Base != base { | |
| 455 | + | return fmt.Errorf("%w: the diff base of !%d is now %s", domain.ErrConflict, number, base) | |
| 456 | + | } | |
| 457 | + | return nil | |
| 458 | + | } | |
| 435 | 459 | d := ports.MergeRequestDetail{MergeRequest: mr} | |
| 436 | 460 | if d.Comments, err = m.store.ListComments(ctx, name, number); err != nil { | |
| 437 | 461 | return ports.MergeRequestDetail{}, fmt.Errorf("list comments of !%d in %q: %w", number, name, err) | |
| @@ -446,7 +470,7 @@ func (m *MergeRequests) Get(ctx context.Context, p domain.Principal, name domain | |||
| 446 | 470 | } | |
| 447 | 471 | head := mr.HeadID | |
| 448 | 472 | if head.IsZero() { | |
| 449 | - | return d, nil // the source branch is gone: no commits, no diff | |
| 473 | + | return d, checkBase(domain.ObjectID{}) // the source branch is gone: no commits, no diff | |
| 450 | 474 | } | |
| 451 | 475 | if !mr.IsOpen() { | |
| 452 | 476 | // After a squash the reviewed commits are only reachable from the | |
| @@ -455,13 +479,19 @@ func (m *MergeRequests) Get(ctx context.Context, p domain.Principal, name domain | |||
| 455 | 479 | case err != nil: | |
| 456 | 480 | return ports.MergeRequestDetail{}, err | |
| 457 | 481 | case !ok: | |
| 458 | - | return d, nil // the reviewed commits are gone: no commits, no diff | |
| 482 | + | // The reviewed commits are gone: no commits, no diff. | |
| 483 | + | if err := checkBase(domain.ObjectID{}); err != nil { | |
| 484 | + | return ports.MergeRequestDetail{}, err | |
| 485 | + | } | |
| 486 | + | return d, nil | |
| 459 | 487 | } | |
| 460 | 488 | } | |
| 461 | - | if d.Commits, d.CommitsTruncated, err = m.git.ListRange(ctx, name, exclude, head, MaxMergeRequestCommits); err != nil { | |
| 462 | - | return ports.MergeRequestDetail{}, err | |
| 489 | + | if !opts.DiffOnly { | |
| 490 | + | if d.Commits, d.CommitsTruncated, err = m.git.ListRange(ctx, name, exclude, head, MaxMergeRequestCommits); err != nil { | |
| 491 | + | return ports.MergeRequestDetail{}, err | |
| 492 | + | } | |
| 463 | 493 | } | |
| 464 | - | if sinceLastRead && !d.LastReadID.IsZero() { | |
| 494 | + | if opts.SinceLastRead && !d.LastReadID.IsZero() { | |
| 465 | 495 | switch ok, err := m.git.HasCommit(ctx, name, d.LastReadID); { | |
| 466 | 496 | case err != nil: | |
| 467 | 497 | return ports.MergeRequestDetail{}, err | |
| @@ -471,11 +501,14 @@ func (m *MergeRequests) Get(ctx context.Context, p domain.Principal, name domain | |||
| 471 | 501 | d.LastReadLost = true | |
| 472 | 502 | } | |
| 473 | 503 | } | |
| 504 | + | if err := checkBase(base); err != nil { | |
| 505 | + | return ports.MergeRequestDetail{}, err | |
| 506 | + | } | |
| 474 | 507 | if base.IsZero() { | |
| 475 | 508 | return d, nil | |
| 476 | 509 | } | |
| 477 | 510 | d.DiffBaseID = base | |
| 478 | - | if d.Stats, d.Diff, err = m.git.DiffCommits(ctx, name, base, head, domain.DefaultDiffLimits()); err != nil { | |
| 511 | + | if d.Stats, d.Diff, err = m.git.DiffCommits(ctx, name, base, head, limits); err != nil { | |
| 479 | 512 | return ports.MergeRequestDetail{}, err | |
| 480 | 513 | } | |
| 481 | 514 | return d, nil | |
| @@ -601,16 +634,41 @@ func (m *MergeRequests) checkLine(ctx context.Context, mr domain.MergeRequest, p | |||
| 601 | 634 | v.Add("path", "the merge request has no diff to comment on") | |
| 602 | 635 | return v.Err() | |
| 603 | 636 | } | |
| 604 | - | _, diff, err := m.git.DiffCommits(ctx, mr.Repo, base, mr.HeadID, domain.DefaultDiffLimits()) | |
| 637 | + | // A comment may be on a file past the 300th that the web view lists, so | |
| 638 | + | // the file is looked up among all the files (MaxFiles 0). Reading every | |
| 639 | + | // file's hunks within the default total limits would reject a comment on a | |
| 640 | + | // file the earlier ones left no budget for. So a first read lists the files | |
| 641 | + | // with no hunks (From past the end: cheap, git is stopped at once), and a | |
| 642 | + | // second reads the hunks of the one file within the default per-file limit | |
| 643 | + | // and total limits, as the web view does for a window holding it. | |
| 644 | + | list := domain.DefaultDiffLimits() | |
| 645 | + | list.MaxFiles, list.From = 0, math.MaxInt | |
| 646 | + | _, diff, err := m.git.DiffCommits(ctx, mr.Repo, base, mr.HeadID, list) | |
| 605 | 647 | if err != nil { | |
| 606 | 648 | return err | |
| 607 | 649 | } | |
| 608 | - | if !diff.HasLine(params.Path, params.Side, params.Line) { | |
| 650 | + | idx := slices.IndexFunc(diff.Files, func(f domain.FileDiff) bool { return diffFileName(f) == params.Path }) | |
| 651 | + | if idx >= 0 { | |
| 652 | + | one := domain.DefaultDiffLimits() | |
| 653 | + | one.MaxFiles, one.From, one.To = 0, idx, idx+1 | |
| 654 | + | if _, diff, err = m.git.DiffCommits(ctx, mr.Repo, base, mr.HeadID, one); err != nil { | |
| 655 | + | return err | |
| 656 | + | } | |
| 657 | + | } | |
| 658 | + | if idx < 0 || !diff.HasLine(params.Path, params.Side, params.Line) { | |
| 609 | 659 | v.Add("path", fmt.Sprintf("%s:%d on the %s side is not a line of the diff", params.Path, params.Line, params.Side)) | |
| 610 | 660 | } | |
| 611 | 661 | return v.Err() | |
| 612 | 662 | } | |
| 613 | 663 | ||
| 664 | + | // diffFileName is the path a comment designates for f, as Diff.HasLine reads it. | |
| 665 | + | func diffFileName(f domain.FileDiff) string { | |
| 666 | + | if f.Status == domain.FileDeleted { | |
| 667 | + | return f.OldPath | |
| 668 | + | } | |
| 669 | + | return f.NewPath | |
| 670 | + | } | |
| 671 | + | ||
| 614 | 672 | // ResolveComment marks a comment resolved or unresolved. The principal needs | |
| 615 | 673 | // mr:write. An unknown comment gives domain.ErrNotFound and a request that is | |
| 616 | 674 | // not open domain.ErrPreconditionFailed. Setting the state the comment already | |
M internal/usecase/mergerequests_branches_test.go +1 -1
| @@ -39,7 +39,7 @@ func TestMergeWithBranchLimitedToken(t *testing.T) { | |||
| 39 | 39 | } | |
| 40 | 40 | assert.Equal(t, main, r.branchHead("main"), "main did not move") | |
| 41 | 41 | assert.Equal(t, head, r.branchHead("agent/claude/feat"), "the source stays") | |
| 42 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 42 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 43 | 43 | require.NoError(t, err) | |
| 44 | 44 | assert.Equal(t, domain.MRStateOpen, d.MergeRequest.State) | |
| 45 | 45 | assert.Empty(t, r.drain(), "no event") | |
M internal/usecase/mergerequests_delete_test.go +3 -3
| @@ -42,7 +42,7 @@ func TestMergeDeletesTheSourceBranch(t *testing.T) { | |||
| 42 | 42 | }, evs[1].(domain.PushEvent).Updates) | |
| 43 | 43 | ||
| 44 | 44 | // The merged request still reads, with its commits and diff. | |
| 45 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 45 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 46 | 46 | require.NoError(t, err) | |
| 47 | 47 | assert.Len(t, d.Commits, 2) | |
| 48 | 48 | assert.Len(t, d.Diff.Files, 2) | |
| @@ -82,7 +82,7 @@ func TestMergeRefusesToDeleteANeededBranch(t *testing.T) { | |||
| 82 | 82 | assert.Contains(t, err.Error(), "!2 still uses") | |
| 83 | 83 | assert.Equal(t, main, r.branchHead("main"), "not merged") | |
| 84 | 84 | assert.Equal(t, head, r.branchHead("feat")) | |
| 85 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 85 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 86 | 86 | require.NoError(t, err) | |
| 87 | 87 | assert.Equal(t, domain.MRStateOpen, d.MergeRequest.State) | |
| 88 | 88 | }) | |
| @@ -138,7 +138,7 @@ func TestSquashMergedRequestReadsAfterItsCommitsArePruned(t *testing.T) { | |||
| 138 | 138 | require.NoError(t, err) | |
| 139 | 139 | require.False(t, ok, "the reviewed commits are pruned") | |
| 140 | 140 | ||
| 141 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 141 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 142 | 142 | require.NoError(t, err) | |
| 143 | 143 | assert.Equal(t, domain.MRStateMerged, d.MergeRequest.State) | |
| 144 | 144 | assert.Equal(t, res.MergedCommitID, d.MergeRequest.MergedCommitID) | |
A internal/usecase/mergerequests_files_test.go +192 -0
| @@ -0,0 +1,192 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package usecase_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "fmt" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | "testing" | |
| 10 | + | ||
| 11 | + | "github.com/stretchr/testify/assert" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ||
| 14 | + | "bnema.dev/ermite/internal/domain" | |
| 15 | + | "bnema.dev/ermite/internal/ports" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // manyFiles commits n one-line files m/f000... in one commit and | |
| 19 | + | // returns the commit id. | |
| 20 | + | func (r *mrRig) manyFiles(n int) domain.ObjectID { | |
| 21 | + | r.t.Helper() | |
| 22 | + | for i := range n { | |
| 23 | + | full := filepath.Join(r.work, "m", fmt.Sprintf("f%03d", i)) | |
| 24 | + | require.NoError(r.t, os.MkdirAll(filepath.Dir(full), 0o755)) | |
| 25 | + | require.NoError(r.t, os.WriteFile(full, []byte(fmt.Sprintf("line %d\n", i)), 0o644)) | |
| 26 | + | } | |
| 27 | + | r.sh("add", "m") | |
| 28 | + | r.sh("commit", "-q", "-m", "many files") | |
| 29 | + | return r.head() | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | const manyFilesCount = 320 | |
| 33 | + | ||
| 34 | + | // bigMR opens a request that changes manyFilesCount files. | |
| 35 | + | func bigMR(t *testing.T) (*mrRig, domain.MergeRequest) { | |
| 36 | + | t.Helper() | |
| 37 | + | r := newMRRig(t) | |
| 38 | + | r.seed() | |
| 39 | + | r.sh("checkout", "-q", "-B", "feat", "main") | |
| 40 | + | r.manyFiles(manyFilesCount) | |
| 41 | + | r.push("feat") | |
| 42 | + | return r, r.open("feat") | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func TestGetMergeRequestFilesWindow(t *testing.T) { | |
| 46 | + | r, mr := bigMR(t) | |
| 47 | + | n := mr.Number | |
| 48 | + | ||
| 49 | + | t.Run("the default diff keeps the contract limit", func(t *testing.T) { | |
| 50 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{}) | |
| 51 | + | require.NoError(t, err) | |
| 52 | + | assert.Len(t, d.Diff.Files, 300) | |
| 53 | + | assert.True(t, d.Diff.Truncated) | |
| 54 | + | assert.Equal(t, manyFilesCount, d.Stats.FilesChanged) | |
| 55 | + | }) | |
| 56 | + | ||
| 57 | + | t.Run("every file is listed, the hunks are those of the window", func(t *testing.T) { | |
| 58 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &ports.FileWindow{From: 305, To: 310}}) | |
| 59 | + | require.NoError(t, err) | |
| 60 | + | require.Len(t, d.Diff.Files, manyFilesCount) | |
| 61 | + | assert.Equal(t, manyFilesCount, d.Stats.FilesChanged) | |
| 62 | + | assert.False(t, d.Diff.Truncated) | |
| 63 | + | for i, f := range d.Diff.Files { | |
| 64 | + | assert.Equal(t, fmt.Sprintf("m/f%03d", i), f.NewPath) | |
| 65 | + | assert.Equal(t, 1, f.Additions) | |
| 66 | + | assert.False(t, f.Truncated) | |
| 67 | + | if i >= 305 && i < 310 { | |
| 68 | + | require.Len(t, f.Hunks, 1, f.NewPath) | |
| 69 | + | assert.Equal(t, fmt.Sprintf("line %d", i), f.Hunks[0].Lines[0].Text) | |
| 70 | + | } else { | |
| 71 | + | assert.Empty(t, f.Hunks, f.NewPath) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | }) | |
| 75 | + | ||
| 76 | + | t.Run("an empty window past the end lists the files only", func(t *testing.T) { | |
| 77 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &ports.FileWindow{From: 1000}}) | |
| 78 | + | require.NoError(t, err) | |
| 79 | + | require.Len(t, d.Diff.Files, manyFilesCount) | |
| 80 | + | for _, f := range d.Diff.Files { | |
| 81 | + | assert.Empty(t, f.Hunks) | |
| 82 | + | } | |
| 83 | + | }) | |
| 84 | + | ||
| 85 | + | t.Run("an open window reads through the end", func(t *testing.T) { | |
| 86 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &ports.FileWindow{From: 315}}) | |
| 87 | + | require.NoError(t, err) | |
| 88 | + | require.Len(t, d.Diff.Files, manyFilesCount) | |
| 89 | + | assert.Empty(t, d.Diff.Files[314].Hunks) | |
| 90 | + | for _, f := range d.Diff.Files[315:] { | |
| 91 | + | assert.Len(t, f.Hunks, 1) | |
| 92 | + | } | |
| 93 | + | }) | |
| 94 | + | ||
| 95 | + | t.Run("an invalid window is refused", func(t *testing.T) { | |
| 96 | + | for _, w := range []ports.FileWindow{{From: -1}, {To: -1}, {From: 5, To: 3}} { | |
| 97 | + | _, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &w}) | |
| 98 | + | require.ErrorIs(t, err, domain.ErrInvalid, "%+v", w) | |
| 99 | + | } | |
| 100 | + | }) | |
| 101 | + | ||
| 102 | + | t.Run("a comment on a line of a file past the 300th is accepted", func(t *testing.T) { | |
| 103 | + | c, err := r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f315", Line: 1, Side: domain.SideNew, Text: "late file"}) | |
| 104 | + | require.NoError(t, err) | |
| 105 | + | assert.Equal(t, "m/f315", c.Path) | |
| 106 | + | _, err = r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f319", Line: 1, Side: domain.SideNew, Text: "last file"}) | |
| 107 | + | require.NoError(t, err) | |
| 108 | + | ||
| 109 | + | _, err = r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f315", Line: 2, Side: domain.SideNew, Text: "no such line"}) | |
| 110 | + | require.ErrorIs(t, err, domain.ErrInvalid) | |
| 111 | + | _, err = r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f999", Line: 1, Side: domain.SideNew, Text: "no such file"}) | |
| 112 | + | require.ErrorIs(t, err, domain.ErrInvalid) | |
| 113 | + | }) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestGetMergeRequestBaseAndHeadChecks(t *testing.T) { | |
| 117 | + | r := newMRRig(t) | |
| 118 | + | base := r.seed() | |
| 119 | + | head1 := r.feature("feat") | |
| 120 | + | mr := r.open("feat") | |
| 121 | + | n := mr.Number | |
| 122 | + | other := oidFrom(t, "1111111111111111111111111111111111111111") | |
| 123 | + | ||
| 124 | + | t.Run("matching or unset base and head", func(t *testing.T) { | |
| 125 | + | for _, o := range []ports.GetMergeRequestOptions{ | |
| 126 | + | {}, {Base: base}, {Head: head1}, {Base: base, Head: head1}, {Base: base, Head: head1, Files: &ports.FileWindow{To: 1}}, | |
| 127 | + | } { | |
| 128 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, o) | |
| 129 | + | require.NoError(t, err, "%+v", o) | |
| 130 | + | assert.Equal(t, base, d.DiffBaseID) | |
| 131 | + | } | |
| 132 | + | }) | |
| 133 | + | ||
| 134 | + | t.Run("another base or head is a conflict", func(t *testing.T) { | |
| 135 | + | for _, o := range []ports.GetMergeRequestOptions{ | |
| 136 | + | {Base: other}, {Head: other}, {Base: base, Head: other}, {Base: other, Head: head1}, | |
| 137 | + | {Base: head1}, // the head is not the base | |
| 138 | + | {Base: other, Files: &ports.FileWindow{From: 1, To: 2}}, | |
| 139 | + | } { | |
| 140 | + | _, err := r.mrs.Get(r.ctx, reader, r.repo, n, o) | |
| 141 | + | require.ErrorIs(t, err, domain.ErrConflict, "%+v", o) | |
| 142 | + | } | |
| 143 | + | }) | |
| 144 | + | ||
| 145 | + | t.Run("the head moved", func(t *testing.T) { | |
| 146 | + | r.commit(author, "d.txt", "dee\n", "add d") | |
| 147 | + | head2 := r.head() | |
| 148 | + | r.push("feat") | |
| 149 | + | _, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Base: base, Head: head1}) | |
| 150 | + | require.ErrorIs(t, err, domain.ErrConflict) | |
| 151 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Base: base, Head: head2}) | |
| 152 | + | require.NoError(t, err) | |
| 153 | + | assert.Equal(t, head2, d.MergeRequest.HeadID) | |
| 154 | + | }) | |
| 155 | + | ||
| 156 | + | t.Run("the base is the one after since-last-read", func(t *testing.T) { | |
| 157 | + | head2 := r.branchHead("feat") | |
| 158 | + | _, err := r.mrs.MarkRead(r.ctx, mrW, r.repo, n, head1) | |
| 159 | + | require.NoError(t, err) | |
| 160 | + | // The whole diff (base) is no longer what since-last-read shows (head1). | |
| 161 | + | _, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true, Base: base, Head: head2}) | |
| 162 | + | require.ErrorIs(t, err, domain.ErrConflict) | |
| 163 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true, Base: head1, Head: head2}) | |
| 164 | + | require.NoError(t, err) | |
| 165 | + | assert.Equal(t, head1, d.DiffBaseID) | |
| 166 | + | require.Len(t, d.Diff.Files, 1) | |
| 167 | + | assert.Equal(t, "d.txt", d.Diff.Files[0].NewPath) | |
| 168 | + | // Without since-last-read the base is still the merge base. | |
| 169 | + | _, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Base: head1, Head: head2}) | |
| 170 | + | require.ErrorIs(t, err, domain.ErrConflict) | |
| 171 | + | }) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | func TestGetMergeRequestDiffOnly(t *testing.T) { | |
| 175 | + | r := newMRRig(t) | |
| 176 | + | base := r.seed() | |
| 177 | + | r.feature("feat") | |
| 178 | + | mr := r.open("feat") | |
| 179 | + | ||
| 180 | + | full, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 181 | + | require.NoError(t, err) | |
| 182 | + | require.Len(t, full.Commits, 2) | |
| 183 | + | ||
| 184 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{DiffOnly: true}) | |
| 185 | + | require.NoError(t, err) | |
| 186 | + | assert.Empty(t, d.Commits) | |
| 187 | + | assert.False(t, d.CommitsTruncated) | |
| 188 | + | assert.Equal(t, base, d.DiffBaseID) | |
| 189 | + | assert.Equal(t, full.Diff, d.Diff) | |
| 190 | + | assert.Equal(t, full.Stats, d.Stats) | |
| 191 | + | assert.Equal(t, full.Comments, d.Comments) | |
| 192 | + | } | |
M internal/usecase/mergerequests_integration_test.go +22 -22
| @@ -345,7 +345,7 @@ func TestMergeRequestAccess(t *testing.T) { | |||
| 345 | 345 | return err | |
| 346 | 346 | }, | |
| 347 | 347 | "Get": func(p domain.Principal) error { | |
| 348 | - | _, err := r.mrs.Get(r.ctx, p, r.repo, n, false) | |
| 348 | + | _, err := r.mrs.Get(r.ctx, p, r.repo, n, ports.GetMergeRequestOptions{}) | |
| 349 | 349 | return err | |
| 350 | 350 | }, | |
| 351 | 351 | } | |
| @@ -356,7 +356,7 @@ func TestMergeRequestAccess(t *testing.T) { | |||
| 356 | 356 | assert.NoError(t, call(reader), what+" with repo:read") | |
| 357 | 357 | assert.NoError(t, call(root), what+" by root") | |
| 358 | 358 | } | |
| 359 | - | _, err := r.mrs.Get(r.ctx, reader, r.repo, n+1, false) | |
| 359 | + | _, err := r.mrs.Get(r.ctx, reader, r.repo, n+1, ports.GetMergeRequestOptions{}) | |
| 360 | 360 | assert.ErrorIs(t, err, domain.ErrNotFound, "unknown number") | |
| 361 | 361 | _, err = r.mrs.List(r.ctx, root, name(t, "ghost"), ports.ListMergeRequestsParams{}) | |
| 362 | 362 | assert.ErrorIs(t, err, domain.ErrNotFound, "unknown repository") | |
| @@ -505,7 +505,7 @@ func TestGetMergeRequest(t *testing.T) { | |||
| 505 | 505 | _, err = r.mrs.AddComment(r.ctx, root, r.repo, mr.Number, ports.AddCommentParams{Text: "second"}) | |
| 506 | 506 | require.NoError(t, err) | |
| 507 | 507 | ||
| 508 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 508 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 509 | 509 | require.NoError(t, err) | |
| 510 | 510 | assert.Equal(t, head, d.MergeRequest.HeadID) | |
| 511 | 511 | assert.Equal(t, main, d.DiffBaseID, "the base is the merge base of the target and the head") | |
| @@ -523,14 +523,14 @@ func TestGetMergeRequest(t *testing.T) { | |||
| 523 | 523 | r.sh("checkout", "-q", "main") | |
| 524 | 524 | r.commit(author, "later.txt", "x\n", "later on main") | |
| 525 | 525 | r.push("main") | |
| 526 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 526 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 527 | 527 | require.NoError(t, err) | |
| 528 | 528 | assert.Equal(t, main, d.DiffBaseID) | |
| 529 | 529 | assert.Len(t, d.Commits, 2, "the commits that main lacks") | |
| 530 | 530 | ||
| 531 | 531 | // A deleted source branch: no commits, no diff, an empty head. | |
| 532 | 532 | r.run(r.bare(), r.env(author), "branch", "-D", "feat") | |
| 533 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 533 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 534 | 534 | require.NoError(t, err) | |
| 535 | 535 | assert.True(t, d.MergeRequest.HeadID.IsZero()) | |
| 536 | 536 | assert.Empty(t, d.Commits) | |
| @@ -548,7 +548,7 @@ func TestGetMergeRequestTruncatesCommits(t *testing.T) { | |||
| 548 | 548 | } | |
| 549 | 549 | r.push("long") | |
| 550 | 550 | mr := r.open("long") | |
| 551 | - | d, err := r.mrs.Get(r.ctx, root, r.repo, mr.Number, false) | |
| 551 | + | d, err := r.mrs.Get(r.ctx, root, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 552 | 552 | require.NoError(t, err) | |
| 553 | 553 | assert.Len(t, d.Commits, usecase.MaxMergeRequestCommits) | |
| 554 | 554 | assert.True(t, d.CommitsTruncated) | |
| @@ -610,7 +610,7 @@ func TestAddComment(t *testing.T) { | |||
| 610 | 610 | } | |
| 611 | 611 | ||
| 612 | 612 | // Ids count 1, 2, ... in order of creation and only the valid ones are kept. | |
| 613 | - | d, err := r.mrs.Get(r.ctx, root, r.repo, n, false) | |
| 613 | + | d, err := r.mrs.Get(r.ctx, root, r.repo, n, ports.GetMergeRequestOptions{}) | |
| 614 | 614 | require.NoError(t, err) | |
| 615 | 615 | var kept int | |
| 616 | 616 | for _, tc := range tests { | |
| @@ -657,7 +657,7 @@ func TestConcurrentComments(t *testing.T) { | |||
| 657 | 657 | seen[id] = true | |
| 658 | 658 | } | |
| 659 | 659 | assert.Len(t, seen, workers) | |
| 660 | - | d, err := r.mrs.Get(r.ctx, root, r.repo, mr.Number, false) | |
| 660 | + | d, err := r.mrs.Get(r.ctx, root, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 661 | 661 | require.NoError(t, err) | |
| 662 | 662 | assert.Len(t, d.Comments, workers) | |
| 663 | 663 | } | |
| @@ -752,7 +752,7 @@ func TestMarkReadAndSinceLastRead(t *testing.T) { | |||
| 752 | 752 | n := mr.Number | |
| 753 | 753 | ||
| 754 | 754 | // Nothing recorded: the flag changes nothing. | |
| 755 | - | d, err := r.mrs.Get(r.ctx, mrW, r.repo, n, true) | |
| 755 | + | d, err := r.mrs.Get(r.ctx, mrW, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true}) | |
| 756 | 756 | require.NoError(t, err) | |
| 757 | 757 | assert.True(t, d.LastReadID.IsZero()) | |
| 758 | 758 | assert.False(t, d.LastReadLost) | |
| @@ -762,7 +762,7 @@ func TestMarkReadAndSinceLastRead(t *testing.T) { | |||
| 762 | 762 | got, err := r.mrs.MarkRead(r.ctx, mrW, r.repo, n, domain.ObjectID{}) | |
| 763 | 763 | require.NoError(t, err) | |
| 764 | 764 | assert.Equal(t, head1, got) | |
| 765 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, n, false) | |
| 765 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{}) | |
| 766 | 766 | require.NoError(t, err) | |
| 767 | 767 | assert.Equal(t, head1, d.LastReadID, "reported whether or not since_last_read is set") | |
| 768 | 768 | assert.Equal(t, base, d.DiffBaseID) | |
| @@ -771,7 +771,7 @@ func TestMarkReadAndSinceLastRead(t *testing.T) { | |||
| 771 | 771 | r.commit(author, "d.txt", "dee\n", "add d") | |
| 772 | 772 | head2 := r.head() | |
| 773 | 773 | r.push("feat") | |
| 774 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, n, true) | |
| 774 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true}) | |
| 775 | 775 | require.NoError(t, err) | |
| 776 | 776 | assert.Equal(t, head1, d.DiffBaseID) | |
| 777 | 777 | assert.Equal(t, head2, d.MergeRequest.HeadID) | |
| @@ -791,7 +791,7 @@ func TestMarkReadAndSinceLastRead(t *testing.T) { | |||
| 791 | 791 | // An explicit head is recorded as given. | |
| 792 | 792 | _, err = r.mrs.MarkRead(r.ctx, mrW, r.repo, n, base) | |
| 793 | 793 | require.NoError(t, err) | |
| 794 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, n, true) | |
| 794 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true}) | |
| 795 | 795 | require.NoError(t, err) | |
| 796 | 796 | assert.Equal(t, base, d.DiffBaseID) | |
| 797 | 797 | } | |
| @@ -819,7 +819,7 @@ func TestSinceLastReadAfterForcePush(t *testing.T) { | |||
| 819 | 819 | // head1 is not an ancestor of head2 any more. | |
| 820 | 820 | r.run(r.bare(), r.env(author), "cat-file", "-e", head1.String()) | |
| 821 | 821 | ||
| 822 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, true) | |
| 822 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{SinceLastRead: true}) | |
| 823 | 823 | require.NoError(t, err) | |
| 824 | 824 | assert.False(t, d.LastReadLost) | |
| 825 | 825 | assert.Equal(t, head1, d.DiffBaseID, "the diff starts at the head that was read") | |
| @@ -838,14 +838,14 @@ func TestSinceLastReadAfterForcePush(t *testing.T) { | |||
| 838 | 838 | assert.Equal(t, []string{"see"}, removed) | |
| 839 | 839 | ||
| 840 | 840 | // The whole change is still available without the flag. | |
| 841 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 841 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 842 | 842 | require.NoError(t, err) | |
| 843 | 843 | assert.Equal(t, base, d.DiffBaseID) | |
| 844 | 844 | assert.Len(t, d.Diff.Files, 2) | |
| 845 | 845 | ||
| 846 | 846 | // Once the recorded commit is pruned, the response says so. | |
| 847 | 847 | r.run(r.bare(), r.env(author), "gc", "-q", "--prune=now") | |
| 848 | - | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, true) | |
| 848 | + | d, err = r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{SinceLastRead: true}) | |
| 849 | 849 | require.NoError(t, err) | |
| 850 | 850 | assert.True(t, d.LastReadLost, "the recorded head is gone") | |
| 851 | 851 | assert.Equal(t, head1, d.LastReadID, "still reported") | |
| @@ -871,7 +871,7 @@ func TestMergeFastForward(t *testing.T) { | |||
| 871 | 871 | _ = main | |
| 872 | 872 | ||
| 873 | 873 | // The stored request is the merged one, and reads keep a diff. | |
| 874 | - | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 874 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 875 | 875 | require.NoError(t, err) | |
| 876 | 876 | assert.Equal(t, domain.MRStateMerged, d.MergeRequest.State) | |
| 877 | 877 | assert.Equal(t, head, d.MergeRequest.HeadID) | |
| @@ -940,7 +940,7 @@ func TestMergeRefusals(t *testing.T) { | |||
| 940 | 940 | assert.Contains(t, err.Error(), "branch moved, read again") | |
| 941 | 941 | assert.NotEqual(t, head, r.branchHead("main"), "main did not move") | |
| 942 | 942 | // Reading again gives the new head, which merges. | |
| 943 | - | d, err := r.mrs.Get(r.ctx, mrM, r.repo, mr.Number, false) | |
| 943 | + | d, err := r.mrs.Get(r.ctx, mrM, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 944 | 944 | require.NoError(t, err) | |
| 945 | 945 | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, ports.MergeParams{HeadID: d.MergeRequest.HeadID}) | |
| 946 | 946 | require.NoError(t, err) | |
| @@ -962,7 +962,7 @@ func TestMergeRefusals(t *testing.T) { | |||
| 962 | 962 | assert.Contains(t, err.Error(), "rebase", mode) | |
| 963 | 963 | } | |
| 964 | 964 | assert.Equal(t, tip, r.branchHead("main")) | |
| 965 | - | d, err := r.mrs.Get(r.ctx, mrM, r.repo, mr.Number, false) | |
| 965 | + | d, err := r.mrs.Get(r.ctx, mrM, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 966 | 966 | require.NoError(t, err) | |
| 967 | 967 | assert.Equal(t, domain.MRStateOpen, d.MergeRequest.State) | |
| 968 | 968 | ||
| @@ -1081,14 +1081,14 @@ func TestConcurrentMergesSerialize(t *testing.T) { | |||
| 1081 | 1081 | require.Len(t, won, 1, "exactly one merge request was merged: %v", won) | |
| 1082 | 1082 | for number, times := range won { | |
| 1083 | 1083 | assert.Equal(t, 1, times, "!%d merged twice", number) | |
| 1084 | - | mr, err := r.mrs.Get(r.ctx, root, r.repo, number, false) | |
| 1084 | + | mr, err := r.mrs.Get(r.ctx, root, r.repo, number, ports.GetMergeRequestOptions{}) | |
| 1085 | 1085 | require.NoError(t, err) | |
| 1086 | 1086 | assert.Equal(t, domain.MRStateMerged, mr.MergeRequest.State) | |
| 1087 | 1087 | assert.Equal(t, mr.MergeRequest.MergedCommitID, r.branchHead("main")) | |
| 1088 | 1088 | } | |
| 1089 | 1089 | for number := range heads { | |
| 1090 | 1090 | if won[number] == 0 { | |
| 1091 | - | d, err := r.mrs.Get(r.ctx, root, r.repo, number, false) | |
| 1091 | + | d, err := r.mrs.Get(r.ctx, root, r.repo, number, ports.GetMergeRequestOptions{}) | |
| 1092 | 1092 | require.NoError(t, err) | |
| 1093 | 1093 | assert.Equal(t, domain.MRStateOpen, d.MergeRequest.State, "!%d lost and stays open", number) | |
| 1094 | 1094 | } | |
| @@ -1275,7 +1275,7 @@ func TestAgentPresetWorksOnItsPrivateRepository(t *testing.T) { | |||
| 1275 | 1275 | // repo:write alone does not read merge requests. | |
| 1276 | 1276 | _, err = r.mrs.List(r.ctx, agent, r.repo, ports.ListMergeRequestsParams{}) | |
| 1277 | 1277 | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 1278 | - | _, err = r.mrs.Get(r.ctx, agent, r.repo, mr.Number, false) | |
| 1278 | + | _, err = r.mrs.Get(r.ctx, agent, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 1279 | 1279 | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 1280 | 1280 | ||
| 1281 | 1281 | closed, err := r.mrs.Close(r.ctx, agent, r.repo, mr.Number) | |
| @@ -1328,7 +1328,7 @@ func TestCommentsPerMergeRequestAreBounded(t *testing.T) { | |||
| 1328 | 1328 | } | |
| 1329 | 1329 | _, err := r.mrs.AddComment(r.ctx, mrW, r.repo, mr.Number, ports.AddCommentParams{Text: "one too many"}) | |
| 1330 | 1330 | assert.ErrorIs(t, err, domain.ErrResourceExhausted) | |
| 1331 | - | d, err := r.mrs.Get(r.ctx, root, r.repo, mr.Number, false) | |
| 1331 | + | d, err := r.mrs.Get(r.ctx, root, r.repo, mr.Number, ports.GetMergeRequestOptions{}) | |
| 1332 | 1332 | require.NoError(t, err) | |
| 1333 | 1333 | assert.Len(t, d.Comments, usecase.MaxCommentsPerMergeRequest) | |
| 1334 | 1334 | ||