feat(web): show the lines a diff hides (e / E) (#54)
- commit
- 032b9efee6dcade40d71e6fd7bf5c696373fad80
- author
- brice · · 2026-10-17 23:52 UTC
- committer
- bnema · · 2026-10-17 23:52 UTC
- parent
- 7ecd377
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown - refs
- tag: v0.20.0
48 files · +1136 -96
M internal/adapters/web/commit.go +1 -0
| @@ -122,6 +122,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||
| 122 | 122 | Dels: res.Stats.Deletions, | |
| 123 | 123 | Files: diffFiles(res.Diff.Files), | |
| 124 | 124 | } | |
| 125 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", cid)) | |
| 125 | 126 | if cm := person(c.Committer, now); cm != p.Author || c.Committer.Email != c.Author.Email { | |
| 126 | 127 | p.Committer = &cm | |
| 127 | 128 | } | |
| @@ -122,6 +122,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 122 | Dels: res.Stats.Deletions, | 122 | Dels: res.Stats.Deletions, | ||
| 123 | Files: diffFiles(res.Diff.Files), | 123 | Files: diffFiles(res.Diff.Files), | ||
| 124 | } | 124 | } | ||
| 125 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", cid)) | |||
| 125 | if cm := person(c.Committer, now); cm != p.Author || c.Committer.Email != c.Author.Email { | 126 | if cm := person(c.Committer, now); cm != p.Author || c.Committer.Email != c.Author.Email { | ||
| 126 | p.Committer = &cm | 127 | p.Committer = &cm | ||
| 127 | } | 128 | } | ||
M internal/adapters/web/compare.go +1 -0
| @@ -87,6 +87,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||
| 87 | 87 | } | |
| 88 | 88 | p.HasDiff = true | |
| 89 | 89 | p.Files = diffFiles(res.Diff.Files) | |
| 90 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", res.Commit.ID.String())) | |
| 90 | 91 | p.NFiles, p.Adds, p.Dels = res.Stats.FilesChanged, res.Stats.Additions, res.Stats.Deletions | |
| 91 | 92 | if n := diffNote(res.Diff, len(p.Files), p.NFiles); n != "" { | |
| 92 | 93 | p.Note = n | |
| @@ -87,6 +87,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 87 | } | 87 | } | ||
| 88 | p.HasDiff = true | 88 | p.HasDiff = true | ||
| 89 | p.Files = diffFiles(res.Diff.Files) | 89 | p.Files = diffFiles(res.Diff.Files) | ||
| 90 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", res.Commit.ID.String())) | |||
| 90 | p.NFiles, p.Adds, p.Dels = res.Stats.FilesChanged, res.Stats.Additions, res.Stats.Deletions | 91 | p.NFiles, p.Adds, p.Dels = res.Stats.FilesChanged, res.Stats.Additions, res.Stats.Deletions | ||
| 91 | if n := diffNote(res.Diff, len(p.Files), p.NFiles); n != "" { | 92 | if n := diffNote(res.Diff, len(p.Files), p.NFiles); n != "" { | ||
| 92 | p.Note = n | 93 | p.Note = n | ||
A internal/adapters/web/context.go +111 -0
| @@ -0,0 +1,111 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "net/http" | |
| 8 | + | "strconv" | |
| 9 | + | "strings" | |
| 10 | + | ||
| 11 | + | "github.com/a-h/templ" | |
| 12 | + | ||
| 13 | + | "bnema.dev/ermite/internal/domain" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // maxContextLines bounds the lines one gap loads: the rest stays behind a | |
| 17 | + | // smaller gap that loads the next lines. | |
| 18 | + | const maxContextLines = 2000 | |
| 19 | + | ||
| 20 | + | // ContextRows is the hidden lines of a gap, for both layouts: they replace the | |
| 21 | + | // rows of the gap. More is the gap that is left, when the file goes on. | |
| 22 | + | type ContextRows struct { | |
| 23 | + | Lines []DiffLine | |
| 24 | + | More *DiffGap | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // diffContext is GET /<repo>/context/<commit>?path=&old=&new=&n=: the lines of | |
| 28 | + | // a file of a diff that its hunks hide, as rows of the diff tables. commit is | |
| 29 | + | // the full id of the commit of the new side; path the file there; old and new | |
| 30 | + | // the numbers of the first hidden line on each side, which move together (the | |
| 31 | + | // lines are unchanged); n the number of lines, 0 for the rest of the file. | |
| 32 | + | // | |
| 33 | + | //hexcheck:ignore suspicious-business-logic-in-adapter cuts a file the use case opened into rows of the diff; no rule is decided here | |
| 34 | + | func (s *Server) diffContext(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 35 | + | sha := r.PathValue("sha") | |
| 36 | + | if !isFullID(sha) { | |
| 37 | + | return nil, 0, errNotFound | |
| 38 | + | } | |
| 39 | + | q := r.URL.Query() | |
| 40 | + | num := func(k string, least int) (int, bool) { | |
| 41 | + | v, err := strconv.Atoi(q.Get(k)) | |
| 42 | + | return v, err == nil && v >= least && strconv.Itoa(v) == q.Get(k) | |
| 43 | + | } | |
| 44 | + | oldFrom, ok1 := num("old", 1) | |
| 45 | + | newFrom, ok2 := num("new", 1) | |
| 46 | + | n, ok3 := num("n", 0) | |
| 47 | + | path := q.Get("path") | |
| 48 | + | if !ok1 || !ok2 || !ok3 || path == "" { | |
| 49 | + | return nil, 0, errNotFound | |
| 50 | + | } | |
| 51 | + | repo, err := domain.ParseRepoName(r.PathValue("repo")) | |
| 52 | + | if err != nil { | |
| 53 | + | return nil, 0, err | |
| 54 | + | } | |
| 55 | + | info, body, err := s.openBlobAt(ctx, repo, sha, path) | |
| 56 | + | if err != nil { | |
| 57 | + | return nil, 0, err | |
| 58 | + | } | |
| 59 | + | b := &blobStream{body: body, info: info} | |
| 60 | + | defer b.close() | |
| 61 | + | if info.Binary || info.Size > maxCodeBytes { | |
| 62 | + | return nil, 0, errNotFound | |
| 63 | + | } | |
| 64 | + | if err := b.fill(int(info.Size)); err != nil { | |
| 65 | + | return nil, 0, err | |
| 66 | + | } | |
| 67 | + | // Lines as Git numbers them: split on LF only (a lone CR is a character of | |
| 68 | + | // its line, shown as a marker; the CR of a CRLF is dropped by segments). | |
| 69 | + | text := strings.ToValidUTF8(string(b.buf), "\uFFFD") | |
| 70 | + | lines := strings.Split(text, "\n") | |
| 71 | + | noEOL := text != "" && !strings.HasSuffix(text, "\n") | |
| 72 | + | if !noEOL { | |
| 73 | + | lines = lines[:len(lines)-1] | |
| 74 | + | } | |
| 75 | + | if newFrom > len(lines)+1 { | |
| 76 | + | return nil, 0, errNotFound | |
| 77 | + | } | |
| 78 | + | end := len(lines) | |
| 79 | + | if n > 0 { | |
| 80 | + | end = min(end, newFrom-1+n) | |
| 81 | + | } | |
| 82 | + | cut := min(end, newFrom-1+maxContextLines) | |
| 83 | + | ||
| 84 | + | var out ContextRows | |
| 85 | + | for i := newFrom - 1; i < cut; i++ { | |
| 86 | + | o, nw := oldFrom+i-(newFrom-1), i+1 | |
| 87 | + | out.Lines = append(out.Lines, DiffLine{ | |
| 88 | + | Kind: "ctx", Mark: " ", Segs: segments(lines[i]), | |
| 89 | + | Old: itoa(o), New: itoa(nw), oldN: o, newN: nw, | |
| 90 | + | NoEOL: noEOL && i == len(lines)-1, | |
| 91 | + | }) | |
| 92 | + | } | |
| 93 | + | if cut < end { | |
| 94 | + | rest := 0 | |
| 95 | + | if n > 0 { | |
| 96 | + | rest = end - cut | |
| 97 | + | } | |
| 98 | + | next := gapHref(r, oldFrom+cut-(newFrom-1), cut+1, rest) | |
| 99 | + | out.More = &DiffGap{Key: q.Get("key"), Lines: rest, Src: next} | |
| 100 | + | } | |
| 101 | + | return DiffContext(out), http.StatusOK, nil | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // gapHref is the address of the gap that starts at the lines old and new. | |
| 105 | + | func gapHref(r *http.Request, old, new, n int) string { | |
| 106 | + | q := r.URL.Query() | |
| 107 | + | q.Set("old", itoa(old)) | |
| 108 | + | q.Set("new", itoa(new)) | |
| 109 | + | q.Set("n", itoa(n)) | |
| 110 | + | return r.URL.Path + "?" + q.Encode() | |
| 111 | + | } | |
| @@ -0,0 +1,111 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "net/http" | |||
| 8 | + | "strconv" | |||
| 9 | + | "strings" | |||
| 10 | + | ||||
| 11 | + | "github.com/a-h/templ" | |||
| 12 | + | ||||
| 13 | + | "bnema.dev/ermite/internal/domain" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // maxContextLines bounds the lines one gap loads: the rest stays behind a | |||
| 17 | + | // smaller gap that loads the next lines. | |||
| 18 | + | const maxContextLines = 2000 | |||
| 19 | + | ||||
| 20 | + | // ContextRows is the hidden lines of a gap, for both layouts: they replace the | |||
| 21 | + | // rows of the gap. More is the gap that is left, when the file goes on. | |||
| 22 | + | type ContextRows struct { | |||
| 23 | + | Lines []DiffLine | |||
| 24 | + | More *DiffGap | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // diffContext is GET /<repo>/context/<commit>?path=&old=&new=&n=: the lines of | |||
| 28 | + | // a file of a diff that its hunks hide, as rows of the diff tables. commit is | |||
| 29 | + | // the full id of the commit of the new side; path the file there; old and new | |||
| 30 | + | // the numbers of the first hidden line on each side, which move together (the | |||
| 31 | + | // lines are unchanged); n the number of lines, 0 for the rest of the file. | |||
| 32 | + | // | |||
| 33 | + | //hexcheck:ignore suspicious-business-logic-in-adapter cuts a file the use case opened into rows of the diff; no rule is decided here | |||
| 34 | + | func (s *Server) diffContext(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |||
| 35 | + | sha := r.PathValue("sha") | |||
| 36 | + | if !isFullID(sha) { | |||
| 37 | + | return nil, 0, errNotFound | |||
| 38 | + | } | |||
| 39 | + | q := r.URL.Query() | |||
| 40 | + | num := func(k string, least int) (int, bool) { | |||
| 41 | + | v, err := strconv.Atoi(q.Get(k)) | |||
| 42 | + | return v, err == nil && v >= least && strconv.Itoa(v) == q.Get(k) | |||
| 43 | + | } | |||
| 44 | + | oldFrom, ok1 := num("old", 1) | |||
| 45 | + | newFrom, ok2 := num("new", 1) | |||
| 46 | + | n, ok3 := num("n", 0) | |||
| 47 | + | path := q.Get("path") | |||
| 48 | + | if !ok1 || !ok2 || !ok3 || path == "" { | |||
| 49 | + | return nil, 0, errNotFound | |||
| 50 | + | } | |||
| 51 | + | repo, err := domain.ParseRepoName(r.PathValue("repo")) | |||
| 52 | + | if err != nil { | |||
| 53 | + | return nil, 0, err | |||
| 54 | + | } | |||
| 55 | + | info, body, err := s.openBlobAt(ctx, repo, sha, path) | |||
| 56 | + | if err != nil { | |||
| 57 | + | return nil, 0, err | |||
| 58 | + | } | |||
| 59 | + | b := &blobStream{body: body, info: info} | |||
| 60 | + | defer b.close() | |||
| 61 | + | if info.Binary || info.Size > maxCodeBytes { | |||
| 62 | + | return nil, 0, errNotFound | |||
| 63 | + | } | |||
| 64 | + | if err := b.fill(int(info.Size)); err != nil { | |||
| 65 | + | return nil, 0, err | |||
| 66 | + | } | |||
| 67 | + | // Lines as Git numbers them: split on LF only (a lone CR is a character of | |||
| 68 | + | // its line, shown as a marker; the CR of a CRLF is dropped by segments). | |||
| 69 | + | text := strings.ToValidUTF8(string(b.buf), "\uFFFD") | |||
| 70 | + | lines := strings.Split(text, "\n") | |||
| 71 | + | noEOL := text != "" && !strings.HasSuffix(text, "\n") | |||
| 72 | + | if !noEOL { | |||
| 73 | + | lines = lines[:len(lines)-1] | |||
| 74 | + | } | |||
| 75 | + | if newFrom > len(lines)+1 { | |||
| 76 | + | return nil, 0, errNotFound | |||
| 77 | + | } | |||
| 78 | + | end := len(lines) | |||
| 79 | + | if n > 0 { | |||
| 80 | + | end = min(end, newFrom-1+n) | |||
| 81 | + | } | |||
| 82 | + | cut := min(end, newFrom-1+maxContextLines) | |||
| 83 | + | ||||
| 84 | + | var out ContextRows | |||
| 85 | + | for i := newFrom - 1; i < cut; i++ { | |||
| 86 | + | o, nw := oldFrom+i-(newFrom-1), i+1 | |||
| 87 | + | out.Lines = append(out.Lines, DiffLine{ | |||
| 88 | + | Kind: "ctx", Mark: " ", Segs: segments(lines[i]), | |||
| 89 | + | Old: itoa(o), New: itoa(nw), oldN: o, newN: nw, | |||
| 90 | + | NoEOL: noEOL && i == len(lines)-1, | |||
| 91 | + | }) | |||
| 92 | + | } | |||
| 93 | + | if cut < end { | |||
| 94 | + | rest := 0 | |||
| 95 | + | if n > 0 { | |||
| 96 | + | rest = end - cut | |||
| 97 | + | } | |||
| 98 | + | next := gapHref(r, oldFrom+cut-(newFrom-1), cut+1, rest) | |||
| 99 | + | out.More = &DiffGap{Key: q.Get("key"), Lines: rest, Src: next} | |||
| 100 | + | } | |||
| 101 | + | return DiffContext(out), http.StatusOK, nil | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // gapHref is the address of the gap that starts at the lines old and new. | |||
| 105 | + | func gapHref(r *http.Request, old, new, n int) string { | |||
| 106 | + | q := r.URL.Query() | |||
| 107 | + | q.Set("old", itoa(old)) | |||
| 108 | + | q.Set("new", itoa(new)) | |||
| 109 | + | q.Set("n", itoa(n)) | |||
| 110 | + | return r.URL.Path + "?" + q.Encode() | |||
| 111 | + | } | |||
M internal/adapters/web/diff.go +83 -0
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "fmt" | |
| 8 | 8 | "html" | |
| 9 | 9 | "io" | |
| 10 | + | "net/url" | |
| 10 | 11 | "slices" | |
| 11 | 12 | "strings" | |
| 12 | 13 | "unicode/utf8" | |
| @@ -78,6 +79,28 @@ type DiffHunk struct { | |||
| 78 | 79 | Header string | |
| 79 | 80 | Lines []DiffLine | |
| 80 | 81 | Split []SplitRow | |
| 82 | + | // Gap is the unchanged lines before the hunk that the diff does not show, | |
| 83 | + | // nil when there are none or they cannot be loaded. | |
| 84 | + | Gap *DiffGap | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // DiffGap is a run of unchanged lines a diff hides: a row of the diff that | |
| 88 | + | // loads them (diffContext) and takes their place. | |
| 89 | + | type DiffGap struct { | |
| 90 | + | // Key tells the gap apart within its file: the unified and the split | |
| 91 | + | // tables hold one row each for it. | |
| 92 | + | Key string | |
| 93 | + | // Lines is the number of hidden lines, 0 for the rest of the file. | |
| 94 | + | Lines int | |
| 95 | + | Src string | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Label is the text of the row of the gap. | |
| 99 | + | func (g DiffGap) Label() string { | |
| 100 | + | if g.Lines == 0 { | |
| 101 | + | return "show the rest of the file" | |
| 102 | + | } | |
| 103 | + | return plural(g.Lines, "hidden line", "hidden lines") | |
| 81 | 104 | } | |
| 82 | 105 | ||
| 83 | 106 | // DiffFile is one changed file. | |
| @@ -121,6 +144,66 @@ type DiffFile struct { | |||
| 121 | 144 | // Loose are the comments of a file loaded with its window that are on a | |
| 122 | 145 | // line its diff does not show: listed at the end of the file. | |
| 123 | 146 | Loose []CommentView | |
| 147 | + | // Tail is the rest of the file after its last hunk, nil when it cannot be | |
| 148 | + | // loaded. Its length is not known until it is. | |
| 149 | + | Tail *DiffGap | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // diffContextLines is the number of unchanged lines Git shows around a change | |
| 153 | + | // (the -U3 of the diffs of the Git adapter, Git's default). | |
| 154 | + | const diffContextLines = 3 | |
| 155 | + | ||
| 156 | + | // contextGaps gives the files of a diff the gaps around their hunks, which | |
| 157 | + | // load the hidden lines of the new side from base (/<repo>/context/<commit>, | |
| 158 | + | // the commit of the new side). Only a file whose two sides hold text has | |
| 159 | + | // them: an added or a deleted file shows every line already. | |
| 160 | + | func contextGaps(out []DiffFile, files []domain.FileDiff, base string) { | |
| 161 | + | for i, f := range files { | |
| 162 | + | if i >= len(out) || f.Binary || f.Truncated || len(f.Hunks) == 0 || len(out[i].Hunks) != len(f.Hunks) { | |
| 163 | + | continue | |
| 164 | + | } | |
| 165 | + | switch f.Status { | |
| 166 | + | case domain.FileModified, domain.FileRenamed, domain.FileCopied: | |
| 167 | + | default: | |
| 168 | + | continue | |
| 169 | + | } | |
| 170 | + | gap := func(key string, oldFrom, newFrom, n int) *DiffGap { | |
| 171 | + | q := url.Values{} | |
| 172 | + | q.Set("path", f.NewPath) | |
| 173 | + | q.Set("old", itoa(oldFrom)) | |
| 174 | + | q.Set("new", itoa(newFrom)) | |
| 175 | + | q.Set("n", itoa(n)) | |
| 176 | + | q.Set("key", key) | |
| 177 | + | return &DiffGap{Key: key, Lines: n, Src: base + "?" + q.Encode()} | |
| 178 | + | } | |
| 179 | + | // first is the first line of a side of a hunk: a side without lines | |
| 180 | + | // names the line before the hunk ("-5,0"). | |
| 181 | + | first := func(start, lines int) int { | |
| 182 | + | if lines == 0 { | |
| 183 | + | return start + 1 | |
| 184 | + | } | |
| 185 | + | return start | |
| 186 | + | } | |
| 187 | + | oldNext, newNext := 1, 1 | |
| 188 | + | for j, h := range f.Hunks { | |
| 189 | + | oldFirst, newFirst := first(h.OldStart, h.OldLines), first(h.NewStart, h.NewLines) | |
| 190 | + | if n := newFirst - newNext; n > 0 && n == oldFirst-oldNext { | |
| 191 | + | out[i].Hunks[j].Gap = gap("g"+itoa(j), oldNext, newNext, n) | |
| 192 | + | } | |
| 193 | + | oldNext, newNext = oldFirst+h.OldLines, newFirst+h.NewLines | |
| 194 | + | } | |
| 195 | + | // Git ends a hunk diffContextLines lines after its last change, or at | |
| 196 | + | // the end of the file: fewer lines after the last change mean the | |
| 197 | + | // file ends there. | |
| 198 | + | last := f.Hunks[len(f.Hunks)-1].Lines | |
| 199 | + | trailing := 0 | |
| 200 | + | for k := len(last) - 1; k >= 0 && last[k].Kind == domain.LineContext && !last[k].NoNewlineAtEOF; k-- { | |
| 201 | + | trailing++ | |
| 202 | + | } | |
| 203 | + | if trailing >= diffContextLines { | |
| 204 | + | out[i].Tail = gap("tail", oldNext, newNext, 0) | |
| 205 | + | } | |
| 206 | + | } | |
| 124 | 207 | } | |
| 125 | 208 | ||
| 126 | 209 | // gutterDigits is the number of digits of the largest line number of the | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | "html" | 8 | "html" | ||
| 9 | "io" | 9 | "io" | ||
| 10 | + | "net/url" | |||
| 10 | "slices" | 11 | "slices" | ||
| 11 | "strings" | 12 | "strings" | ||
| 12 | "unicode/utf8" | 13 | "unicode/utf8" | ||
| @@ -78,6 +79,28 @@ type DiffHunk struct { | |||||
| 78 | Header string | 79 | Header string | ||
| 79 | Lines []DiffLine | 80 | Lines []DiffLine | ||
| 80 | Split []SplitRow | 81 | Split []SplitRow | ||
| 82 | + | // Gap is the unchanged lines before the hunk that the diff does not show, | |||
| 83 | + | // nil when there are none or they cannot be loaded. | |||
| 84 | + | Gap *DiffGap | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // DiffGap is a run of unchanged lines a diff hides: a row of the diff that | |||
| 88 | + | // loads them (diffContext) and takes their place. | |||
| 89 | + | type DiffGap struct { | |||
| 90 | + | // Key tells the gap apart within its file: the unified and the split | |||
| 91 | + | // tables hold one row each for it. | |||
| 92 | + | Key string | |||
| 93 | + | // Lines is the number of hidden lines, 0 for the rest of the file. | |||
| 94 | + | Lines int | |||
| 95 | + | Src string | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // Label is the text of the row of the gap. | |||
| 99 | + | func (g DiffGap) Label() string { | |||
| 100 | + | if g.Lines == 0 { | |||
| 101 | + | return "show the rest of the file" | |||
| 102 | + | } | |||
| 103 | + | return plural(g.Lines, "hidden line", "hidden lines") | |||
| 81 | } | 104 | } | ||
| 82 | 105 | ||||
| 83 | // DiffFile is one changed file. | 106 | // DiffFile is one changed file. | ||
| @@ -121,6 +144,66 @@ type DiffFile struct { | |||||
| 121 | // Loose are the comments of a file loaded with its window that are on a | 144 | // 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. | 145 | // line its diff does not show: listed at the end of the file. | ||
| 123 | Loose []CommentView | 146 | Loose []CommentView | ||
| 147 | + | // Tail is the rest of the file after its last hunk, nil when it cannot be | |||
| 148 | + | // loaded. Its length is not known until it is. | |||
| 149 | + | Tail *DiffGap | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // diffContextLines is the number of unchanged lines Git shows around a change | |||
| 153 | + | // (the -U3 of the diffs of the Git adapter, Git's default). | |||
| 154 | + | const diffContextLines = 3 | |||
| 155 | + | ||||
| 156 | + | // contextGaps gives the files of a diff the gaps around their hunks, which | |||
| 157 | + | // load the hidden lines of the new side from base (/<repo>/context/<commit>, | |||
| 158 | + | // the commit of the new side). Only a file whose two sides hold text has | |||
| 159 | + | // them: an added or a deleted file shows every line already. | |||
| 160 | + | func contextGaps(out []DiffFile, files []domain.FileDiff, base string) { | |||
| 161 | + | for i, f := range files { | |||
| 162 | + | if i >= len(out) || f.Binary || f.Truncated || len(f.Hunks) == 0 || len(out[i].Hunks) != len(f.Hunks) { | |||
| 163 | + | continue | |||
| 164 | + | } | |||
| 165 | + | switch f.Status { | |||
| 166 | + | case domain.FileModified, domain.FileRenamed, domain.FileCopied: | |||
| 167 | + | default: | |||
| 168 | + | continue | |||
| 169 | + | } | |||
| 170 | + | gap := func(key string, oldFrom, newFrom, n int) *DiffGap { | |||
| 171 | + | q := url.Values{} | |||
| 172 | + | q.Set("path", f.NewPath) | |||
| 173 | + | q.Set("old", itoa(oldFrom)) | |||
| 174 | + | q.Set("new", itoa(newFrom)) | |||
| 175 | + | q.Set("n", itoa(n)) | |||
| 176 | + | q.Set("key", key) | |||
| 177 | + | return &DiffGap{Key: key, Lines: n, Src: base + "?" + q.Encode()} | |||
| 178 | + | } | |||
| 179 | + | // first is the first line of a side of a hunk: a side without lines | |||
| 180 | + | // names the line before the hunk ("-5,0"). | |||
| 181 | + | first := func(start, lines int) int { | |||
| 182 | + | if lines == 0 { | |||
| 183 | + | return start + 1 | |||
| 184 | + | } | |||
| 185 | + | return start | |||
| 186 | + | } | |||
| 187 | + | oldNext, newNext := 1, 1 | |||
| 188 | + | for j, h := range f.Hunks { | |||
| 189 | + | oldFirst, newFirst := first(h.OldStart, h.OldLines), first(h.NewStart, h.NewLines) | |||
| 190 | + | if n := newFirst - newNext; n > 0 && n == oldFirst-oldNext { | |||
| 191 | + | out[i].Hunks[j].Gap = gap("g"+itoa(j), oldNext, newNext, n) | |||
| 192 | + | } | |||
| 193 | + | oldNext, newNext = oldFirst+h.OldLines, newFirst+h.NewLines | |||
| 194 | + | } | |||
| 195 | + | // Git ends a hunk diffContextLines lines after its last change, or at | |||
| 196 | + | // the end of the file: fewer lines after the last change mean the | |||
| 197 | + | // file ends there. | |||
| 198 | + | last := f.Hunks[len(f.Hunks)-1].Lines | |||
| 199 | + | trailing := 0 | |||
| 200 | + | for k := len(last) - 1; k >= 0 && last[k].Kind == domain.LineContext && !last[k].NoNewlineAtEOF; k-- { | |||
| 201 | + | trailing++ | |||
| 202 | + | } | |||
| 203 | + | if trailing >= diffContextLines { | |||
| 204 | + | out[i].Tail = gap("tail", oldNext, newNext, 0) | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 124 | } | 207 | } | ||
| 125 | 208 | ||||
| 126 | // gutterDigits is the number of digits of the largest line number of the | 209 | // gutterDigits is the number of digits of the largest line number of the | ||
M internal/adapters/web/diff.templ +60 -0
| @@ -212,6 +212,9 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 212 | 212 | <table class="dt uni" aria-label={ f.Path + ", unified" }> | |
| 213 | 213 | for _, h := range f.Hunks { | |
| 214 | 214 | <tbody> | |
| 215 | + | if h.Gap != nil { | |
| 216 | + | @gapRow(*h.Gap, 4) | |
| 217 | + | } | |
| 215 | 218 | <tr class="hunk"><td colspan="4">{ h.Header }</td></tr> | |
| 216 | 219 | for _, ln := range h.Lines { | |
| 217 | 220 | <tr class={ ln.Kind }> | |
| @@ -229,6 +232,11 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 229 | 232 | } | |
| 230 | 233 | </tbody> | |
| 231 | 234 | } | |
| 235 | + | if f.Tail != nil { | |
| 236 | + | <tbody> | |
| 237 | + | @gapRow(*f.Tail, 4) | |
| 238 | + | </tbody> | |
| 239 | + | } | |
| 232 | 240 | </table> | |
| 233 | 241 | } | |
| 234 | 242 | if p.ShowSplit { | |
| @@ -236,6 +244,9 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 236 | 244 | <colgroup><col class="c-ln"/><col class="c-m"/><col/><col class="c-ln"/><col class="c-m"/><col/></colgroup> | |
| 237 | 245 | for _, h := range f.Hunks { | |
| 238 | 246 | <tbody> | |
| 247 | + | if h.Gap != nil { | |
| 248 | + | @gapRow(*h.Gap, 6) | |
| 249 | + | } | |
| 239 | 250 | <tr class="hunk"><td colspan="6">{ h.Header }</td></tr> | |
| 240 | 251 | for _, row := range h.Split { | |
| 241 | 252 | <tr> | |
| @@ -250,6 +261,11 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 250 | 261 | } | |
| 251 | 262 | </tbody> | |
| 252 | 263 | } | |
| 264 | + | if f.Tail != nil { | |
| 265 | + | <tbody> | |
| 266 | + | @gapRow(*f.Tail, 6) | |
| 267 | + | </tbody> | |
| 268 | + | } | |
| 253 | 269 | </table> | |
| 254 | 270 | } | |
| 255 | 271 | </div> | |
| @@ -286,6 +302,50 @@ templ MRFilesMoved() { | |||
| 286 | 302 | <p class="notice" data-moved>this merge request changed since the page was loaded — <a href="">reload</a></p> | |
| 287 | 303 | } | |
| 288 | 304 | ||
| 305 | + | // gapRow is the row of the hidden lines before a hunk, or of the rest of the | |
| 306 | + | // file: a button that loads them in their place (ermite.js, ermite.expand). | |
| 307 | + | // Without scripts nothing loads them: hidden. | |
| 308 | + | templ gapRow(g DiffGap, cols int) { | |
| 309 | + | <tr class="gap" data-js data-gap={ g.Key }> | |
| 310 | + | <td colspan={ itoa(cols) }> | |
| 311 | + | <button type="button" data-src={ g.Src }> | |
| 312 | + | <span aria-hidden="true">⋯</span> { g.Label() } | |
| 313 | + | </button> | |
| 314 | + | </td> | |
| 315 | + | </tr> | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | // DiffContext is the hidden lines of a gap, for /context: one body a layout, | |
| 319 | + | // whose rows replace the rows of the gap in the table of that layout. | |
| 320 | + | templ DiffContext(c ContextRows) { | |
| 321 | + | <table> | |
| 322 | + | <tbody data-for="uni"> | |
| 323 | + | for _, ln := range c.Lines { | |
| 324 | + | <tr class="ctx"> | |
| 325 | + | <td class="ln">{ ln.Old }</td> | |
| 326 | + | <td class="ln">{ ln.New }</td> | |
| 327 | + | <td class="m">{ ln.Mark }</td> | |
| 328 | + | <td class="tx">@codeLine(ln)</td> | |
| 329 | + | </tr> | |
| 330 | + | } | |
| 331 | + | if c.More != nil { | |
| 332 | + | @gapRow(*c.More, 4) | |
| 333 | + | } | |
| 334 | + | </tbody> | |
| 335 | + | <tbody data-for="spl"> | |
| 336 | + | for i := range c.Lines { | |
| 337 | + | <tr> | |
| 338 | + | @splitSide(&c.Lines[i], true) | |
| 339 | + | @splitSide(&c.Lines[i], false) | |
| 340 | + | </tr> | |
| 341 | + | } | |
| 342 | + | if c.More != nil { | |
| 343 | + | @gapRow(*c.More, 6) | |
| 344 | + | } | |
| 345 | + | </tbody> | |
| 346 | + | </table> | |
| 347 | + | } | |
| 348 | + | ||
| 289 | 349 | // splitSide is one half of a row of the split view; a side without a line is | |
| 290 | 350 | // an empty cell. | |
| 291 | 351 | templ splitSide(ln *DiffLine, old bool) { | |
| @@ -212,6 +212,9 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 212 | <table class="dt uni" aria-label={ f.Path + ", unified" }> | 212 | <table class="dt uni" aria-label={ f.Path + ", unified" }> | ||
| 213 | for _, h := range f.Hunks { | 213 | for _, h := range f.Hunks { | ||
| 214 | <tbody> | 214 | <tbody> | ||
| 215 | + | if h.Gap != nil { | |||
| 216 | + | @gapRow(*h.Gap, 4) | |||
| 217 | + | } | |||
| 215 | <tr class="hunk"><td colspan="4">{ h.Header }</td></tr> | 218 | <tr class="hunk"><td colspan="4">{ h.Header }</td></tr> | ||
| 216 | for _, ln := range h.Lines { | 219 | for _, ln := range h.Lines { | ||
| 217 | <tr class={ ln.Kind }> | 220 | <tr class={ ln.Kind }> | ||
| @@ -229,6 +232,11 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 229 | } | 232 | } | ||
| 230 | </tbody> | 233 | </tbody> | ||
| 231 | } | 234 | } | ||
| 235 | + | if f.Tail != nil { | |||
| 236 | + | <tbody> | |||
| 237 | + | @gapRow(*f.Tail, 4) | |||
| 238 | + | </tbody> | |||
| 239 | + | } | |||
| 232 | </table> | 240 | </table> | ||
| 233 | } | 241 | } | ||
| 234 | if p.ShowSplit { | 242 | if p.ShowSplit { | ||
| @@ -236,6 +244,9 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 236 | <colgroup><col class="c-ln"/><col class="c-m"/><col/><col class="c-ln"/><col class="c-m"/><col/></colgroup> | 244 | <colgroup><col class="c-ln"/><col class="c-m"/><col/><col class="c-ln"/><col class="c-m"/><col/></colgroup> | ||
| 237 | for _, h := range f.Hunks { | 245 | for _, h := range f.Hunks { | ||
| 238 | <tbody> | 246 | <tbody> | ||
| 247 | + | if h.Gap != nil { | |||
| 248 | + | @gapRow(*h.Gap, 6) | |||
| 249 | + | } | |||
| 239 | <tr class="hunk"><td colspan="6">{ h.Header }</td></tr> | 250 | <tr class="hunk"><td colspan="6">{ h.Header }</td></tr> | ||
| 240 | for _, row := range h.Split { | 251 | for _, row := range h.Split { | ||
| 241 | <tr> | 252 | <tr> | ||
| @@ -250,6 +261,11 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 250 | } | 261 | } | ||
| 251 | </tbody> | 262 | </tbody> | ||
| 252 | } | 263 | } | ||
| 264 | + | if f.Tail != nil { | |||
| 265 | + | <tbody> | |||
| 266 | + | @gapRow(*f.Tail, 6) | |||
| 267 | + | </tbody> | |||
| 268 | + | } | |||
| 253 | </table> | 269 | </table> | ||
| 254 | } | 270 | } | ||
| 255 | </div> | 271 | </div> | ||
| @@ -286,6 +302,50 @@ templ MRFilesMoved() { | |||||
| 286 | <p class="notice" data-moved>this merge request changed since the page was loaded — <a href="">reload</a></p> | 302 | <p class="notice" data-moved>this merge request changed since the page was loaded — <a href="">reload</a></p> | ||
| 287 | } | 303 | } | ||
| 288 | 304 | ||||
| 305 | + | // gapRow is the row of the hidden lines before a hunk, or of the rest of the | |||
| 306 | + | // file: a button that loads them in their place (ermite.js, ermite.expand). | |||
| 307 | + | // Without scripts nothing loads them: hidden. | |||
| 308 | + | templ gapRow(g DiffGap, cols int) { | |||
| 309 | + | <tr class="gap" data-js data-gap={ g.Key }> | |||
| 310 | + | <td colspan={ itoa(cols) }> | |||
| 311 | + | <button type="button" data-src={ g.Src }> | |||
| 312 | + | <span aria-hidden="true">⋯</span> { g.Label() } | |||
| 313 | + | </button> | |||
| 314 | + | </td> | |||
| 315 | + | </tr> | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // DiffContext is the hidden lines of a gap, for /context: one body a layout, | |||
| 319 | + | // whose rows replace the rows of the gap in the table of that layout. | |||
| 320 | + | templ DiffContext(c ContextRows) { | |||
| 321 | + | <table> | |||
| 322 | + | <tbody data-for="uni"> | |||
| 323 | + | for _, ln := range c.Lines { | |||
| 324 | + | <tr class="ctx"> | |||
| 325 | + | <td class="ln">{ ln.Old }</td> | |||
| 326 | + | <td class="ln">{ ln.New }</td> | |||
| 327 | + | <td class="m">{ ln.Mark }</td> | |||
| 328 | + | <td class="tx">@codeLine(ln)</td> | |||
| 329 | + | </tr> | |||
| 330 | + | } | |||
| 331 | + | if c.More != nil { | |||
| 332 | + | @gapRow(*c.More, 4) | |||
| 333 | + | } | |||
| 334 | + | </tbody> | |||
| 335 | + | <tbody data-for="spl"> | |||
| 336 | + | for i := range c.Lines { | |||
| 337 | + | <tr> | |||
| 338 | + | @splitSide(&c.Lines[i], true) | |||
| 339 | + | @splitSide(&c.Lines[i], false) | |||
| 340 | + | </tr> | |||
| 341 | + | } | |||
| 342 | + | if c.More != nil { | |||
| 343 | + | @gapRow(*c.More, 6) | |||
| 344 | + | } | |||
| 345 | + | </tbody> | |||
| 346 | + | </table> | |||
| 347 | + | } | |||
| 348 | + | ||||
| 289 | // splitSide is one half of a row of the split view; a side without a line is | 349 | // splitSide is one half of a row of the split view; a side without a line is | ||
| 290 | // an empty cell. | 350 | // an empty cell. | ||
| 291 | templ splitSide(ln *DiffLine, old bool) { | 351 | templ splitSide(ln *DiffLine, old bool) { | ||
M internal/adapters/web/diff_templ.go +339 -85 generated file: collapsed
M internal/adapters/web/diff_test.go +50 -0
| @@ -141,3 +141,53 @@ func TestCodeLine(t *testing.T) { | |||
| 141 | 141 | assert.Equal(t, `a <b> <span class="inv" title="invisible character"><U+202E></span> c d`+ | |
| 142 | 142 | `<span class="eol" title="no newline at end of file">`+noEOL+`</span>`, b.String()) | |
| 143 | 143 | } | |
| 144 | + | ||
| 145 | + | // The gaps of a file: before a hunk the unchanged lines it does not show | |
| 146 | + | // (the same number on each side), after the last hunk the rest of the file; | |
| 147 | + | // none for a file whose lines are all in the diff. | |
| 148 | + | func TestContextGaps(t *testing.T) { | |
| 149 | + | t.Parallel() | |
| 150 | + | ctx := func(o, n int) domain.Line { return line(domain.LineContext, o, n, "x") } | |
| 151 | + | files := []domain.FileDiff{ | |
| 152 | + | {Status: domain.FileModified, NewPath: "a b.go", Hunks: []domain.Hunk{ | |
| 153 | + | {OldStart: 1, OldLines: 2, NewStart: 1, NewLines: 2, Lines: []domain.Line{ctx(1, 1), ctx(2, 2)}}, | |
| 154 | + | {OldStart: 10, OldLines: 3, NewStart: 10, NewLines: 3, Lines: []domain.Line{ctx(10, 10), ctx(11, 11), ctx(12, 12)}}, | |
| 155 | + | }}, | |
| 156 | + | {Status: domain.FileModified, NewPath: "b.go", Hunks: []domain.Hunk{ | |
| 157 | + | // a pure deletion: the new side has no line, "+4,0" names line 4 | |
| 158 | + | {OldStart: 5, OldLines: 1, NewStart: 4, NewLines: 0, Lines: []domain.Line{line(domain.LineDeletion, 5, 0, "x")}}, | |
| 159 | + | }}, | |
| 160 | + | {Status: domain.FileAdded, NewPath: "c.go", Hunks: []domain.Hunk{{OldStart: 0, NewStart: 1, NewLines: 1, Lines: []domain.Line{line(domain.LineAddition, 0, 1, "x")}}}}, | |
| 161 | + | {Status: domain.FileModified, NewPath: "d.go", Hunks: []domain.Hunk{ | |
| 162 | + | {OldStart: 3, OldLines: 1, NewStart: 3, NewLines: 1, Lines: []domain.Line{{Kind: domain.LineAddition, NewNumber: 3, Text: "end", NoNewlineAtEOF: true}}}, | |
| 163 | + | }}, | |
| 164 | + | } | |
| 165 | + | out := diffFiles(files) | |
| 166 | + | contextGaps(out, files, "/r/context/c") | |
| 167 | + | ||
| 168 | + | a := out[0] | |
| 169 | + | assert.Nil(t, a.Hunks[0].Gap, "the first hunk starts at line 1") | |
| 170 | + | require.NotNil(t, a.Hunks[1].Gap) | |
| 171 | + | assert.Equal(t, 7, a.Hunks[1].Gap.Lines, "lines 3 to 9 on both sides") | |
| 172 | + | assert.Equal(t, "/r/context/c?key=g1&n=7&new=3&old=3&path=a+b.go", a.Hunks[1].Gap.Src) | |
| 173 | + | require.NotNil(t, a.Tail) | |
| 174 | + | assert.Equal(t, "/r/context/c?key=tail&n=0&new=13&old=13&path=a+b.go", a.Tail.Src) | |
| 175 | + | ||
| 176 | + | b := out[1] | |
| 177 | + | require.NotNil(t, b.Hunks[0].Gap) | |
| 178 | + | assert.Equal(t, 4, b.Hunks[0].Gap.Lines, "lines 1 to 4 on both sides before the deleted line 5") | |
| 179 | + | assert.Nil(t, b.Tail, "a hunk that ends on a change ends the file") | |
| 180 | + | ||
| 181 | + | assert.Nil(t, out[2].Tail, "an added file shows every line") | |
| 182 | + | assert.Nil(t, out[3].Tail, "the last hunk holds the end of the file") | |
| 183 | + | assert.NotNil(t, a.Tail, "three unchanged lines after the change: the file may go on") | |
| 184 | + | assert.NotNil(t, out[3].Hunks[0].Gap) | |
| 185 | + | ||
| 186 | + | // Sides that do not move together (a parse the page cannot trust): no gap. | |
| 187 | + | odd := []domain.FileDiff{{Status: domain.FileModified, NewPath: "e.go", Hunks: []domain.Hunk{ | |
| 188 | + | {OldStart: 10, OldLines: 1, NewStart: 12, NewLines: 1, Lines: []domain.Line{ctx(10, 12)}}, | |
| 189 | + | }}} | |
| 190 | + | o := diffFiles(odd) | |
| 191 | + | contextGaps(o, odd, "/r/context/c") | |
| 192 | + | assert.Nil(t, o[0].Hunks[0].Gap) | |
| 193 | + | } | |
| @@ -141,3 +141,53 @@ func TestCodeLine(t *testing.T) { | |||||
| 141 | assert.Equal(t, `a <b> <span class="inv" title="invisible character"><U+202E></span> c d`+ | 141 | assert.Equal(t, `a <b> <span class="inv" title="invisible character"><U+202E></span> c d`+ | ||
| 142 | `<span class="eol" title="no newline at end of file">`+noEOL+`</span>`, b.String()) | 142 | `<span class="eol" title="no newline at end of file">`+noEOL+`</span>`, b.String()) | ||
| 143 | } | 143 | } | ||
| 144 | + | ||||
| 145 | + | // The gaps of a file: before a hunk the unchanged lines it does not show | |||
| 146 | + | // (the same number on each side), after the last hunk the rest of the file; | |||
| 147 | + | // none for a file whose lines are all in the diff. | |||
| 148 | + | func TestContextGaps(t *testing.T) { | |||
| 149 | + | t.Parallel() | |||
| 150 | + | ctx := func(o, n int) domain.Line { return line(domain.LineContext, o, n, "x") } | |||
| 151 | + | files := []domain.FileDiff{ | |||
| 152 | + | {Status: domain.FileModified, NewPath: "a b.go", Hunks: []domain.Hunk{ | |||
| 153 | + | {OldStart: 1, OldLines: 2, NewStart: 1, NewLines: 2, Lines: []domain.Line{ctx(1, 1), ctx(2, 2)}}, | |||
| 154 | + | {OldStart: 10, OldLines: 3, NewStart: 10, NewLines: 3, Lines: []domain.Line{ctx(10, 10), ctx(11, 11), ctx(12, 12)}}, | |||
| 155 | + | }}, | |||
| 156 | + | {Status: domain.FileModified, NewPath: "b.go", Hunks: []domain.Hunk{ | |||
| 157 | + | // a pure deletion: the new side has no line, "+4,0" names line 4 | |||
| 158 | + | {OldStart: 5, OldLines: 1, NewStart: 4, NewLines: 0, Lines: []domain.Line{line(domain.LineDeletion, 5, 0, "x")}}, | |||
| 159 | + | }}, | |||
| 160 | + | {Status: domain.FileAdded, NewPath: "c.go", Hunks: []domain.Hunk{{OldStart: 0, NewStart: 1, NewLines: 1, Lines: []domain.Line{line(domain.LineAddition, 0, 1, "x")}}}}, | |||
| 161 | + | {Status: domain.FileModified, NewPath: "d.go", Hunks: []domain.Hunk{ | |||
| 162 | + | {OldStart: 3, OldLines: 1, NewStart: 3, NewLines: 1, Lines: []domain.Line{{Kind: domain.LineAddition, NewNumber: 3, Text: "end", NoNewlineAtEOF: true}}}, | |||
| 163 | + | }}, | |||
| 164 | + | } | |||
| 165 | + | out := diffFiles(files) | |||
| 166 | + | contextGaps(out, files, "/r/context/c") | |||
| 167 | + | ||||
| 168 | + | a := out[0] | |||
| 169 | + | assert.Nil(t, a.Hunks[0].Gap, "the first hunk starts at line 1") | |||
| 170 | + | require.NotNil(t, a.Hunks[1].Gap) | |||
| 171 | + | assert.Equal(t, 7, a.Hunks[1].Gap.Lines, "lines 3 to 9 on both sides") | |||
| 172 | + | assert.Equal(t, "/r/context/c?key=g1&n=7&new=3&old=3&path=a+b.go", a.Hunks[1].Gap.Src) | |||
| 173 | + | require.NotNil(t, a.Tail) | |||
| 174 | + | assert.Equal(t, "/r/context/c?key=tail&n=0&new=13&old=13&path=a+b.go", a.Tail.Src) | |||
| 175 | + | ||||
| 176 | + | b := out[1] | |||
| 177 | + | require.NotNil(t, b.Hunks[0].Gap) | |||
| 178 | + | assert.Equal(t, 4, b.Hunks[0].Gap.Lines, "lines 1 to 4 on both sides before the deleted line 5") | |||
| 179 | + | assert.Nil(t, b.Tail, "a hunk that ends on a change ends the file") | |||
| 180 | + | ||||
| 181 | + | assert.Nil(t, out[2].Tail, "an added file shows every line") | |||
| 182 | + | assert.Nil(t, out[3].Tail, "the last hunk holds the end of the file") | |||
| 183 | + | assert.NotNil(t, a.Tail, "three unchanged lines after the change: the file may go on") | |||
| 184 | + | assert.NotNil(t, out[3].Hunks[0].Gap) | |||
| 185 | + | ||||
| 186 | + | // Sides that do not move together (a parse the page cannot trust): no gap. | |||
| 187 | + | odd := []domain.FileDiff{{Status: domain.FileModified, NewPath: "e.go", Hunks: []domain.Hunk{ | |||
| 188 | + | {OldStart: 10, OldLines: 1, NewStart: 12, NewLines: 1, Lines: []domain.Line{ctx(10, 12)}}, | |||
| 189 | + | }}} | |||
| 190 | + | o := diffFiles(odd) | |||
| 191 | + | contextGaps(o, odd, "/r/context/c") | |||
| 192 | + | assert.Nil(t, o[0].Hunks[0].Gap) | |||
| 193 | + | } | |||
M internal/adapters/web/keys.templ +2 -0
| @@ -31,6 +31,8 @@ templ Keys() { | |||
| 31 | 31 | k-split from:body -> ermite.layout('split'); | |
| 32 | 32 | k-unified from:body -> ermite.layout('unified'); | |
| 33 | 33 | k-collapse from:body -> ermite.collapse(); | |
| 34 | + | k-expand from:body -> ermite.expandFile(); | |
| 35 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 34 | 36 | k-permalink from:body -> ermite.permalink(); | |
| 35 | 37 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 36 | 38 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -31,6 +31,8 @@ templ Keys() { | |||||
| 31 | k-split from:body -> ermite.layout('split'); | 31 | k-split from:body -> ermite.layout('split'); | ||
| 32 | k-unified from:body -> ermite.layout('unified'); | 32 | k-unified from:body -> ermite.layout('unified'); | ||
| 33 | k-collapse from:body -> ermite.collapse(); | 33 | k-collapse from:body -> ermite.collapse(); | ||
| 34 | + | k-expand from:body -> ermite.expandFile(); | |||
| 35 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 34 | k-permalink from:body -> ermite.permalink(); | 36 | k-permalink from:body -> ermite.permalink(); | ||
| 35 | k-merge from:body -> window.ermiteMR?.openMerge(); | 37 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 36 | k-read from:body -> window.ermiteMR?.markRead(); | 38 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/adapters/web/keys_templ.go +1 -1
| @@ -34,7 +34,7 @@ func Keys() templ.Component { | |||
| 34 | 34 | templ_7745c5c3_Var1 = templ.NopComponent | |
| 35 | 35 | } | |
| 36 | 36 | ctx = templ.ClearChildren(ctx) | |
| 37 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"keys\" hidden hx-on=\"k-j from:body -> ermite.move(1);\n k-k from:body -> ermite.move(-1);\n k-top from:body -> ermite.edge(false);\n k-bottom from:body -> ermite.edge(true);\n k-open from:body -> ermite.open();\n k-right from:body -> ermite.rail(true);\n k-left from:body -> ermite.rail(false);\n k-esc from:body -> ermite.closeDialogs();\n k-help from:body -> ermite.dialog('panel-help');\n k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');\n k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); ermiteFinder.load();\n k-filter from:body -> ermite.focusFilter();\n k-gh from:body -> ermite.go('.brand');\n k-s from:body -> ermite.tab('s');\n k-t from:body -> ermite.tab('t');\n k-c from:body -> ermite.tab('c');\n k-r from:body -> ermite.tab('r');\n k-m from:body -> ermite.tab('m');\n k-next from:body -> ermite.nextNote();\n k-split from:body -> ermite.layout('split');\n k-unified from:body -> ermite.layout('unified');\n k-collapse from:body -> ermite.collapse();\n k-permalink from:body -> ermite.permalink();\n k-merge from:body -> window.ermiteMR?.openMerge();\n k-read from:body -> window.ermiteMR?.markRead();\n k-gf from:body -> window.ermiteMR?.focusFiles()\"></div>") | |
| 37 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"keys\" hidden hx-on=\"k-j from:body -> ermite.move(1);\n k-k from:body -> ermite.move(-1);\n k-top from:body -> ermite.edge(false);\n k-bottom from:body -> ermite.edge(true);\n k-open from:body -> ermite.open();\n k-right from:body -> ermite.rail(true);\n k-left from:body -> ermite.rail(false);\n k-esc from:body -> ermite.closeDialogs();\n k-help from:body -> ermite.dialog('panel-help');\n k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');\n k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); ermiteFinder.load();\n k-filter from:body -> ermite.focusFilter();\n k-gh from:body -> ermite.go('.brand');\n k-s from:body -> ermite.tab('s');\n k-t from:body -> ermite.tab('t');\n k-c from:body -> ermite.tab('c');\n k-r from:body -> ermite.tab('r');\n k-m from:body -> ermite.tab('m');\n k-next from:body -> ermite.nextNote();\n k-split from:body -> ermite.layout('split');\n k-unified from:body -> ermite.layout('unified');\n k-collapse from:body -> ermite.collapse();\n k-expand from:body -> ermite.expandFile();\n k-expandAll from:body -> ermite.expandAll();\n k-permalink from:body -> ermite.permalink();\n k-merge from:body -> window.ermiteMR?.openMerge();\n k-read from:body -> window.ermiteMR?.markRead();\n k-gf from:body -> window.ermiteMR?.focusFiles()\"></div>") | |
| 38 | 38 | if templ_7745c5c3_Err != nil { | |
| 39 | 39 | return templ_7745c5c3_Err | |
| 40 | 40 | } | |
| @@ -34,7 +34,7 @@ func Keys() templ.Component { | |||||
| 34 | templ_7745c5c3_Var1 = templ.NopComponent | 34 | templ_7745c5c3_Var1 = templ.NopComponent | ||
| 35 | } | 35 | } | ||
| 36 | ctx = templ.ClearChildren(ctx) | 36 | ctx = templ.ClearChildren(ctx) | ||
| 37 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"keys\" hidden hx-on=\"k-j from:body -> ermite.move(1);\n k-k from:body -> ermite.move(-1);\n k-top from:body -> ermite.edge(false);\n k-bottom from:body -> ermite.edge(true);\n k-open from:body -> ermite.open();\n k-right from:body -> ermite.rail(true);\n k-left from:body -> ermite.rail(false);\n k-esc from:body -> ermite.closeDialogs();\n k-help from:body -> ermite.dialog('panel-help');\n k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');\n k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); ermiteFinder.load();\n k-filter from:body -> ermite.focusFilter();\n k-gh from:body -> ermite.go('.brand');\n k-s from:body -> ermite.tab('s');\n k-t from:body -> ermite.tab('t');\n k-c from:body -> ermite.tab('c');\n k-r from:body -> ermite.tab('r');\n k-m from:body -> ermite.tab('m');\n k-next from:body -> ermite.nextNote();\n k-split from:body -> ermite.layout('split');\n k-unified from:body -> ermite.layout('unified');\n k-collapse from:body -> ermite.collapse();\n k-permalink from:body -> ermite.permalink();\n k-merge from:body -> window.ermiteMR?.openMerge();\n k-read from:body -> window.ermiteMR?.markRead();\n k-gf from:body -> window.ermiteMR?.focusFiles()\"></div>") | 37 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"keys\" hidden hx-on=\"k-j from:body -> ermite.move(1);\n k-k from:body -> ermite.move(-1);\n k-top from:body -> ermite.edge(false);\n k-bottom from:body -> ermite.edge(true);\n k-open from:body -> ermite.open();\n k-right from:body -> ermite.rail(true);\n k-left from:body -> ermite.rail(false);\n k-esc from:body -> ermite.closeDialogs();\n k-help from:body -> ermite.dialog('panel-help');\n k-refs from:body -> ermite.dialog('panel-refs', 'refs-filter');\n k-cmd from:body -> ermite.dialog('panel-cmd', 'cmd-q'); ermiteFinder.load();\n k-filter from:body -> ermite.focusFilter();\n k-gh from:body -> ermite.go('.brand');\n k-s from:body -> ermite.tab('s');\n k-t from:body -> ermite.tab('t');\n k-c from:body -> ermite.tab('c');\n k-r from:body -> ermite.tab('r');\n k-m from:body -> ermite.tab('m');\n k-next from:body -> ermite.nextNote();\n k-split from:body -> ermite.layout('split');\n k-unified from:body -> ermite.layout('unified');\n k-collapse from:body -> ermite.collapse();\n k-expand from:body -> ermite.expandFile();\n k-expandAll from:body -> ermite.expandAll();\n k-permalink from:body -> ermite.permalink();\n k-merge from:body -> window.ermiteMR?.openMerge();\n k-read from:body -> window.ermiteMR?.markRead();\n k-gf from:body -> window.ermiteMR?.focusFiles()\"></div>") |
| 38 | if templ_7745c5c3_Err != nil { | 38 | if templ_7745c5c3_Err != nil { | ||
| 39 | return templ_7745c5c3_Err | 39 | return templ_7745c5c3_Err | ||
| 40 | } | 40 | } | ||
M internal/adapters/web/mr.go +1 -0
| @@ -476,6 +476,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 476 | 476 | Dels: res.Stats.Deletions, | |
| 477 | 477 | Files: diffFiles(res.Diff.Files), | |
| 478 | 478 | } | |
| 479 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | |
| 479 | 480 | p.Opened.Name = p.Author | |
| 480 | 481 | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt) | |
| 481 | 482 | if !m.CreatedAt.IsZero() { | |
| @@ -476,6 +476,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 476 | Dels: res.Stats.Deletions, | 476 | Dels: res.Stats.Deletions, | ||
| 477 | Files: diffFiles(res.Diff.Files), | 477 | Files: diffFiles(res.Diff.Files), | ||
| 478 | } | 478 | } | ||
| 479 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | |||
| 479 | p.Opened.Name = p.Author | 480 | p.Opened.Name = p.Author | ||
| 480 | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt) | 481 | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt) | ||
| 481 | if !m.CreatedAt.IsZero() { | 482 | if !m.CreatedAt.IsZero() { | ||
M internal/adapters/web/mr_files.go +1 -0
| @@ -197,6 +197,7 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||
| 197 | 197 | signed := SignedIn(ctx) | |
| 198 | 198 | canWrite := signed && m.State == domain.MRStateOpen && !since | |
| 199 | 199 | p := MRPage{Files: diffFiles(res.Diff.Files), LooseInFiles: true} | |
| 200 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String())) | |
| 200 | 201 | s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil) | |
| 201 | 202 | window := p.Files[from:min(from+mrWindow, len(p.Files))] | |
| 202 | 203 | // The layouts follow the page: it sends ?view= (the layout shown) and | |
| @@ -197,6 +197,7 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 197 | signed := SignedIn(ctx) | 197 | signed := SignedIn(ctx) | ||
| 198 | canWrite := signed && m.State == domain.MRStateOpen && !since | 198 | canWrite := signed && m.State == domain.MRStateOpen && !since | ||
| 199 | p := MRPage{Files: diffFiles(res.Diff.Files), LooseInFiles: true} | 199 | p := MRPage{Files: diffFiles(res.Diff.Files), LooseInFiles: true} | ||
| 200 | + | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String())) | |||
| 200 | s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil) | 201 | 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 | window := p.Files[from:min(from+mrWindow, len(p.Files))] | ||
| 202 | // The layouts follow the page: it sends ?view= (the layout shown) and | 203 | // The layouts follow the page: it sends ?view= (the layout shown) and | ||
M internal/adapters/web/overlays.templ +1 -0
| @@ -22,6 +22,7 @@ templ OverlayHelp(mr bool) { | |||
| 22 | 22 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | |
| 23 | 23 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | |
| 24 | 24 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | |
| 25 | + | <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div> | |
| 25 | 26 | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | |
| 26 | 27 | if mr { | |
| 27 | 28 | <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div> | |
| @@ -22,6 +22,7 @@ templ OverlayHelp(mr bool) { | |||||
| 22 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | 22 | <div><dt><kbd>g h</kbd></dt><dd>home</dd></div> | ||
| 23 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | 23 | <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div> | ||
| 24 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | 24 | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse or expand all</dd></div> | ||
| 25 | + | <div><dt><kbd>e</kbd> <kbd>E</kbd></dt><dd>diff: the whole file in view / every file, with the lines the hunks hide</dd></div> | |||
| 25 | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | 26 | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | ||
| 26 | if mr { | 27 | if mr { | ||
| 27 | <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div> | 28 | <div><dt><kbd>g f</kbd></dt><dd>merge request: the list of files (j k move, enter opens, esc back)</dd></div> | ||
M internal/adapters/web/overlays_templ.go +9 -9 generated file: collapsed
M internal/adapters/web/static/css/50-code.css +9 -0
| @@ -80,6 +80,15 @@ | |||
| 80 | 80 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | |
| 81 | 81 | & .tx { white-space: pre; } | |
| 82 | 82 | & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; } | |
| 83 | + | /* a run of hidden lines: the whole row loads them */ | |
| 84 | + | & .gap td { padding: 0; background: var(--paper-alt); } | |
| 85 | + | & .gap button { | |
| 86 | + | display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer; | |
| 87 | + | font: inherit; color: var(--ink-dim); background: none; border: 0; | |
| 88 | + | border-block: 1px dashed var(--border); | |
| 89 | + | } | |
| 90 | + | & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); } | |
| 91 | + | & .gap button:disabled { cursor: progress; opacity: .6; } | |
| 83 | 92 | & .add, & tr.add td { background: var(--add); } | |
| 84 | 93 | & .del, & tr.del td { background: var(--del); } | |
| 85 | 94 | & .add.m, & tr.add .m { color: var(--teal); font-weight: bold; } | |
| @@ -80,6 +80,15 @@ | |||||
| 80 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | 80 | & .m { width: 2ch; text-align: center; user-select: none; color: var(--ink-dim); } | ||
| 81 | & .tx { white-space: pre; } | 81 | & .tx { white-space: pre; } | ||
| 82 | & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; } | 82 | & .hunk td { color: var(--ink-mute); background: var(--paper-alt); padding: 0 1ch; } | ||
| 83 | + | /* a run of hidden lines: the whole row loads them */ | |||
| 84 | + | & .gap td { padding: 0; background: var(--paper-alt); } | |||
| 85 | + | & .gap button { | |||
| 86 | + | display: block; width: 100%; padding: 2px 1ch; text-align: left; cursor: pointer; | |||
| 87 | + | font: inherit; color: var(--ink-dim); background: none; border: 0; | |||
| 88 | + | border-block: 1px dashed var(--border); | |||
| 89 | + | } | |||
| 90 | + | & .gap button:hover, & .gap button:focus-visible { color: var(--ink-hi); background: var(--bg-strong); } | |||
| 91 | + | & .gap button:disabled { cursor: progress; opacity: .6; } | |||
| 83 | & .add, & tr.add td { background: var(--add); } | 92 | & .add, & tr.add td { background: var(--add); } | ||
| 84 | & .del, & tr.del td { background: var(--del); } | 93 | & .del, & tr.del td { background: var(--del); } | ||
| 85 | & .add.m, & tr.add .m { color: var(--teal); font-weight: bold; } | 94 | & .add.m, & tr.add .m { color: var(--teal); font-weight: bold; } | ||
M internal/adapters/web/static/js/ermite.js +109 -1
| @@ -10,6 +10,7 @@ | |||
| 10 | 10 | // 3. ermiteLive applies the fragments of the event stream (see events.go). | |
| 11 | 11 | // 4. The file of the diff in view is marked in the table of contents. | |
| 12 | 12 | // 5. A handler that fails is shown in the hint badge. | |
| 13 | + | // 6. The lines a diff hides load in place (ermiteGaps; e and E). | |
| 13 | 14 | (() => { | |
| 14 | 15 | // Marks the page as scripted: what does nothing without scripts (the filters, | |
| 15 | 16 | // the "go to" and help buttons) is hidden until this class is set (see the | |
| @@ -179,6 +180,26 @@ | |||
| 179 | 180 | const open = !files.some((d) => d.open); | |
| 180 | 181 | for (const d of files) d.open = open; | |
| 181 | 182 | }, | |
| 183 | + | // e / E: the whole file in view, or every file of the diff, with every | |
| 184 | + | // line its hunks hide. | |
| 185 | + | expandFile() { | |
| 186 | + | const f = window.ermiteGaps.current(); | |
| 187 | + | if (!f) return flash('no file in view'); | |
| 188 | + | f.open = true; | |
| 189 | + | window.ermiteGaps.file(f).then((r) => { | |
| 190 | + | if (!r.done) return; // load said what failed | |
| 191 | + | flash(r.lines ? 'whole file' : 'nothing hidden in this file'); | |
| 192 | + | }); | |
| 193 | + | }, | |
| 194 | + | async expandAll() { | |
| 195 | + | const files = $$('[data-file]').filter((f) => f.querySelector('tr.gap')); | |
| 196 | + | if (!files.length) return flash('nothing hidden in the loaded files'); | |
| 197 | + | flash('loading every line…', 10000); | |
| 198 | + | for (const f of files) { | |
| 199 | + | if (!(await window.ermiteGaps.file(f)).done) return; // load said what failed | |
| 200 | + | } | |
| 201 | + | flash('every line of the loaded files'); | |
| 202 | + | }, | |
| 182 | 203 | nextNote() { | |
| 183 | 204 | const notes = $$('#diffs .cmt'); | |
| 184 | 205 | if (!notes.length) return; | |
| @@ -280,9 +301,96 @@ | |||
| 280 | 301 | }; | |
| 281 | 302 | })(); | |
| 282 | 303 | ||
| 304 | + | // The hidden lines of a diff: a row .gap holds a button that loads them | |
| 305 | + | // (/context); the rows that come back replace the gap in the table of each | |
| 306 | + | // layout (the unified and the split tables hold one row each for it). A | |
| 307 | + | // file whose lines are all loaded has no gap left. | |
| 308 | + | window.ermiteGaps = (() => { | |
| 309 | + | const pending = new Map(); | |
| 310 | + | // load fills the gap the button is in, in both tables of its file. | |
| 311 | + | function load(btn) { | |
| 312 | + | const src = btn.dataset.src; | |
| 313 | + | const row = btn.closest('tr.gap'); | |
| 314 | + | const file = row?.closest('[data-file]'); | |
| 315 | + | if (!src || !file) return Promise.resolve(0); | |
| 316 | + | if (pending.has(src)) return pending.get(src); | |
| 317 | + | const key = row.dataset.gap; | |
| 318 | + | const table = btn.closest('table'); | |
| 319 | + | const focused = document.activeElement === btn; | |
| 320 | + | btn.disabled = true; | |
| 321 | + | const p = fetch(src, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |
| 322 | + | .then(async (res) => { | |
| 323 | + | if (!res.ok) throw new Error('loading the hidden lines: ' + res.status); | |
| 324 | + | const t = document.createElement('template'); | |
| 325 | + | t.innerHTML = await res.text(); | |
| 326 | + | let n = 0; | |
| 327 | + | for (const table of file.querySelectorAll('table.dt')) { | |
| 328 | + | const kind = table.classList.contains('spl') ? 'spl' : 'uni'; | |
| 329 | + | const from = t.content.querySelector('tbody[data-for="' + kind + '"]'); | |
| 330 | + | const at = [...table.querySelectorAll('tr.gap')].find((r) => r.dataset.gap === key); | |
| 331 | + | if (!from || !at) continue; | |
| 332 | + | const rows = [...from.children].map((r) => r.cloneNode(true)); | |
| 333 | + | n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length); | |
| 334 | + | at.replaceWith(...rows); | |
| 335 | + | } | |
| 336 | + | // The focus was on the button, which is gone: on the gap that is | |
| 337 | + | // left in the same table, else on the content. | |
| 338 | + | if (focused && (document.activeElement === btn || document.activeElement === document.body)) { | |
| 339 | + | const next = [...(table?.querySelectorAll('tr.gap') || [])].find((r) => r.dataset.gap === key)?.querySelector('button'); | |
| 340 | + | if (next) next.focus({ preventScroll: true }); | |
| 341 | + | else document.getElementById('main')?.focus({ preventScroll: true }); | |
| 342 | + | } | |
| 343 | + | return n; | |
| 344 | + | }) | |
| 345 | + | .catch((err) => { | |
| 346 | + | btn.disabled = false; | |
| 347 | + | window.ermite?.flash?.(err.message, 4000); | |
| 348 | + | return -1; | |
| 349 | + | }) | |
| 350 | + | .finally(() => pending.delete(src)); | |
| 351 | + | pending.set(src, p); | |
| 352 | + | return p; | |
| 353 | + | } | |
| 354 | + | // file loads every gap of a file, until none is left (a long gap comes | |
| 355 | + | // back with a gap for the rest). A gap already loading is waited for. | |
| 356 | + | // done is false when a load failed (it said why) or the file never ran | |
| 357 | + | // out of gaps. | |
| 358 | + | async function file(f) { | |
| 359 | + | let lines = 0; | |
| 360 | + | for (let guard = 0; guard < 1000; guard++) { | |
| 361 | + | const btn = f.querySelector('tr.gap button[data-src]'); | |
| 362 | + | if (!btn) return { done: true, lines }; | |
| 363 | + | const n = await load(btn); | |
| 364 | + | if (n < 0) return { done: false, lines }; | |
| 365 | + | if (btn.isConnected) { | |
| 366 | + | // an answer that did not take the gap's place: stop, not loop | |
| 367 | + | window.ermite?.flash?.('some hidden lines could not be shown', 4000); | |
| 368 | + | return { done: false, lines }; | |
| 369 | + | } | |
| 370 | + | lines += n; | |
| 371 | + | } | |
| 372 | + | window.ermite?.flash?.('the file is too long to show every line at once', 4000); | |
| 373 | + | return { done: false, lines }; | |
| 374 | + | } | |
| 375 | + | // current is the file in view: the one the table of contents marks, else | |
| 376 | + | // the last whose top is above the top third of the window. | |
| 377 | + | function current() { | |
| 378 | + | const a = document.querySelector('.files a[aria-current="true"]'); | |
| 379 | + | const marked = a && document.getElementById(a.hash.slice(1)); | |
| 380 | + | if (marked?.matches('[data-file]')) return marked; | |
| 381 | + | const files = [...document.querySelectorAll('[data-file]')]; | |
| 382 | + | return files.findLast((f) => f.getBoundingClientRect().top < innerHeight / 3) ?? files[0] ?? null; | |
| 383 | + | } | |
| 384 | + | addEventListener('click', (e) => { | |
| 385 | + | const btn = e.target.closest?.('tr.gap button[data-src]'); | |
| 386 | + | if (btn) load(btn); | |
| 387 | + | }); | |
| 388 | + | return { load, file, current }; | |
| 389 | + | })(); | |
| 390 | + | ||
| 283 | 391 | // 1. Keys. | |
| 284 | 392 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | |
| 285 | - | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc', | |
| 393 | + | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc', | |
| 286 | 394 | // the merge request page (mr.js): the merge menu, mark read | |
| 287 | 395 | M: 'merge', R: 'read' }; | |
| 288 | 396 | // "g t" is the key "t": the tabs have one event each. | |
| @@ -10,6 +10,7 @@ | |||||
| 10 | // 3. ermiteLive applies the fragments of the event stream (see events.go). | 10 | // 3. ermiteLive applies the fragments of the event stream (see events.go). | ||
| 11 | // 4. The file of the diff in view is marked in the table of contents. | 11 | // 4. The file of the diff in view is marked in the table of contents. | ||
| 12 | // 5. A handler that fails is shown in the hint badge. | 12 | // 5. A handler that fails is shown in the hint badge. | ||
| 13 | + | // 6. The lines a diff hides load in place (ermiteGaps; e and E). | |||
| 13 | (() => { | 14 | (() => { | ||
| 14 | // Marks the page as scripted: what does nothing without scripts (the filters, | 15 | // Marks the page as scripted: what does nothing without scripts (the filters, | ||
| 15 | // the "go to" and help buttons) is hidden until this class is set (see the | 16 | // the "go to" and help buttons) is hidden until this class is set (see the | ||
| @@ -179,6 +180,26 @@ | |||||
| 179 | const open = !files.some((d) => d.open); | 180 | const open = !files.some((d) => d.open); | ||
| 180 | for (const d of files) d.open = open; | 181 | for (const d of files) d.open = open; | ||
| 181 | }, | 182 | }, | ||
| 183 | + | // e / E: the whole file in view, or every file of the diff, with every | |||
| 184 | + | // line its hunks hide. | |||
| 185 | + | expandFile() { | |||
| 186 | + | const f = window.ermiteGaps.current(); | |||
| 187 | + | if (!f) return flash('no file in view'); | |||
| 188 | + | f.open = true; | |||
| 189 | + | window.ermiteGaps.file(f).then((r) => { | |||
| 190 | + | if (!r.done) return; // load said what failed | |||
| 191 | + | flash(r.lines ? 'whole file' : 'nothing hidden in this file'); | |||
| 192 | + | }); | |||
| 193 | + | }, | |||
| 194 | + | async expandAll() { | |||
| 195 | + | const files = $$('[data-file]').filter((f) => f.querySelector('tr.gap')); | |||
| 196 | + | if (!files.length) return flash('nothing hidden in the loaded files'); | |||
| 197 | + | flash('loading every line…', 10000); | |||
| 198 | + | for (const f of files) { | |||
| 199 | + | if (!(await window.ermiteGaps.file(f)).done) return; // load said what failed | |||
| 200 | + | } | |||
| 201 | + | flash('every line of the loaded files'); | |||
| 202 | + | }, | |||
| 182 | nextNote() { | 203 | nextNote() { | ||
| 183 | const notes = $$('#diffs .cmt'); | 204 | const notes = $$('#diffs .cmt'); | ||
| 184 | if (!notes.length) return; | 205 | if (!notes.length) return; | ||
| @@ -280,9 +301,96 @@ | |||||
| 280 | }; | 301 | }; | ||
| 281 | })(); | 302 | })(); | ||
| 282 | 303 | ||||
| 304 | + | // The hidden lines of a diff: a row .gap holds a button that loads them | |||
| 305 | + | // (/context); the rows that come back replace the gap in the table of each | |||
| 306 | + | // layout (the unified and the split tables hold one row each for it). A | |||
| 307 | + | // file whose lines are all loaded has no gap left. | |||
| 308 | + | window.ermiteGaps = (() => { | |||
| 309 | + | const pending = new Map(); | |||
| 310 | + | // load fills the gap the button is in, in both tables of its file. | |||
| 311 | + | function load(btn) { | |||
| 312 | + | const src = btn.dataset.src; | |||
| 313 | + | const row = btn.closest('tr.gap'); | |||
| 314 | + | const file = row?.closest('[data-file]'); | |||
| 315 | + | if (!src || !file) return Promise.resolve(0); | |||
| 316 | + | if (pending.has(src)) return pending.get(src); | |||
| 317 | + | const key = row.dataset.gap; | |||
| 318 | + | const table = btn.closest('table'); | |||
| 319 | + | const focused = document.activeElement === btn; | |||
| 320 | + | btn.disabled = true; | |||
| 321 | + | const p = fetch(src, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |||
| 322 | + | .then(async (res) => { | |||
| 323 | + | if (!res.ok) throw new Error('loading the hidden lines: ' + res.status); | |||
| 324 | + | const t = document.createElement('template'); | |||
| 325 | + | t.innerHTML = await res.text(); | |||
| 326 | + | let n = 0; | |||
| 327 | + | for (const table of file.querySelectorAll('table.dt')) { | |||
| 328 | + | const kind = table.classList.contains('spl') ? 'spl' : 'uni'; | |||
| 329 | + | const from = t.content.querySelector('tbody[data-for="' + kind + '"]'); | |||
| 330 | + | const at = [...table.querySelectorAll('tr.gap')].find((r) => r.dataset.gap === key); | |||
| 331 | + | if (!from || !at) continue; | |||
| 332 | + | const rows = [...from.children].map((r) => r.cloneNode(true)); | |||
| 333 | + | n = Math.max(n, rows.filter((r) => !r.classList.contains('gap')).length); | |||
| 334 | + | at.replaceWith(...rows); | |||
| 335 | + | } | |||
| 336 | + | // The focus was on the button, which is gone: on the gap that is | |||
| 337 | + | // left in the same table, else on the content. | |||
| 338 | + | if (focused && (document.activeElement === btn || document.activeElement === document.body)) { | |||
| 339 | + | const next = [...(table?.querySelectorAll('tr.gap') || [])].find((r) => r.dataset.gap === key)?.querySelector('button'); | |||
| 340 | + | if (next) next.focus({ preventScroll: true }); | |||
| 341 | + | else document.getElementById('main')?.focus({ preventScroll: true }); | |||
| 342 | + | } | |||
| 343 | + | return n; | |||
| 344 | + | }) | |||
| 345 | + | .catch((err) => { | |||
| 346 | + | btn.disabled = false; | |||
| 347 | + | window.ermite?.flash?.(err.message, 4000); | |||
| 348 | + | return -1; | |||
| 349 | + | }) | |||
| 350 | + | .finally(() => pending.delete(src)); | |||
| 351 | + | pending.set(src, p); | |||
| 352 | + | return p; | |||
| 353 | + | } | |||
| 354 | + | // file loads every gap of a file, until none is left (a long gap comes | |||
| 355 | + | // back with a gap for the rest). A gap already loading is waited for. | |||
| 356 | + | // done is false when a load failed (it said why) or the file never ran | |||
| 357 | + | // out of gaps. | |||
| 358 | + | async function file(f) { | |||
| 359 | + | let lines = 0; | |||
| 360 | + | for (let guard = 0; guard < 1000; guard++) { | |||
| 361 | + | const btn = f.querySelector('tr.gap button[data-src]'); | |||
| 362 | + | if (!btn) return { done: true, lines }; | |||
| 363 | + | const n = await load(btn); | |||
| 364 | + | if (n < 0) return { done: false, lines }; | |||
| 365 | + | if (btn.isConnected) { | |||
| 366 | + | // an answer that did not take the gap's place: stop, not loop | |||
| 367 | + | window.ermite?.flash?.('some hidden lines could not be shown', 4000); | |||
| 368 | + | return { done: false, lines }; | |||
| 369 | + | } | |||
| 370 | + | lines += n; | |||
| 371 | + | } | |||
| 372 | + | window.ermite?.flash?.('the file is too long to show every line at once', 4000); | |||
| 373 | + | return { done: false, lines }; | |||
| 374 | + | } | |||
| 375 | + | // current is the file in view: the one the table of contents marks, else | |||
| 376 | + | // the last whose top is above the top third of the window. | |||
| 377 | + | function current() { | |||
| 378 | + | const a = document.querySelector('.files a[aria-current="true"]'); | |||
| 379 | + | const marked = a && document.getElementById(a.hash.slice(1)); | |||
| 380 | + | if (marked?.matches('[data-file]')) return marked; | |||
| 381 | + | const files = [...document.querySelectorAll('[data-file]')]; | |||
| 382 | + | return files.findLast((f) => f.getBoundingClientRect().top < innerHeight / 3) ?? files[0] ?? null; | |||
| 383 | + | } | |||
| 384 | + | addEventListener('click', (e) => { | |||
| 385 | + | const btn = e.target.closest?.('tr.gap button[data-src]'); | |||
| 386 | + | if (btn) load(btn); | |||
| 387 | + | }); | |||
| 388 | + | return { load, file, current }; | |||
| 389 | + | })(); | |||
| 390 | + | ||||
| 283 | // 1. Keys. | 391 | // 1. Keys. | ||
| 284 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | 392 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | ||
| 285 | - | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc', | 393 | + | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc', |
| 286 | // the merge request page (mr.js): the merge menu, mark read | 394 | // the merge request page (mr.js): the merge menu, mark read | ||
| 287 | M: 'merge', R: 'read' }; | 395 | M: 'merge', R: 'read' }; | ||
| 288 | // "g t" is the key "t": the tabs have one event each. | 396 | // "g t" is the key "t": the tabs have one event each. | ||
M internal/adapters/web/web.go +1 -0
| @@ -172,6 +172,7 @@ func (s *Server) routes() *http.ServeMux { | |||
| 172 | 172 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | |
| 173 | 173 | mux.HandleFunc("GET /{repo}/raw/{rest...}", s.raw) | |
| 174 | 174 | mux.HandleFunc("GET /{repo}/paths/{sha}", s.paths) | |
| 175 | + | mux.HandleFunc("GET /{repo}/context/{sha}", s.page(s.diffContext)) | |
| 175 | 176 | if s.cfg.MergeRequests != nil { | |
| 176 | 177 | mux.HandleFunc("GET /{repo}/merge-requests", s.page(s.mrList)) | |
| 177 | 178 | mux.HandleFunc("GET /{repo}/merge-requests/{$}", s.page(s.mrList)) | |
| @@ -172,6 +172,7 @@ func (s *Server) routes() *http.ServeMux { | |||||
| 172 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | 172 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | ||
| 173 | mux.HandleFunc("GET /{repo}/raw/{rest...}", s.raw) | 173 | mux.HandleFunc("GET /{repo}/raw/{rest...}", s.raw) | ||
| 174 | mux.HandleFunc("GET /{repo}/paths/{sha}", s.paths) | 174 | mux.HandleFunc("GET /{repo}/paths/{sha}", s.paths) | ||
| 175 | + | mux.HandleFunc("GET /{repo}/context/{sha}", s.page(s.diffContext)) | |||
| 175 | if s.cfg.MergeRequests != nil { | 176 | if s.cfg.MergeRequests != nil { | ||
| 176 | mux.HandleFunc("GET /{repo}/merge-requests", s.page(s.mrList)) | 177 | mux.HandleFunc("GET /{repo}/merge-requests", s.page(s.mrList)) | ||
| 177 | mux.HandleFunc("GET /{repo}/merge-requests/{$}", s.page(s.mrList)) | 178 | mux.HandleFunc("GET /{repo}/merge-requests/{$}", s.page(s.mrList)) | ||
M internal/app/testdata/web/blob-binary.html +9 -0
| @@ -234,6 +234,13 @@ | |||
| 234 | 234 | </div> | |
| 235 | 235 | <div> | |
| 236 | 236 | <dt> | |
| 237 | + | <kbd>e</kbd> | |
| 238 | + | <kbd>E</kbd> | |
| 239 | + | </dt> | |
| 240 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 241 | + | </div> | |
| 242 | + | <div> | |
| 243 | + | <dt> | |
| 237 | 244 | <kbd>n</kbd> | |
| 238 | 245 | </dt> | |
| 239 | 246 | <dd>merge request: next note in the diff</dd> | |
| @@ -383,6 +390,8 @@ | |||
| 383 | 390 | k-split from:body -> ermite.layout('split'); | |
| 384 | 391 | k-unified from:body -> ermite.layout('unified'); | |
| 385 | 392 | k-collapse from:body -> ermite.collapse(); | |
| 393 | + | k-expand from:body -> ermite.expandFile(); | |
| 394 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 386 | 395 | k-permalink from:body -> ermite.permalink(); | |
| 387 | 396 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 388 | 397 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -234,6 +234,13 @@ | |||||
| 234 | </div> | 234 | </div> | ||
| 235 | <div> | 235 | <div> | ||
| 236 | <dt> | 236 | <dt> | ||
| 237 | + | <kbd>e</kbd> | |||
| 238 | + | <kbd>E</kbd> | |||
| 239 | + | </dt> | |||
| 240 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 241 | + | </div> | |||
| 242 | + | <div> | |||
| 243 | + | <dt> | |||
| 237 | <kbd>n</kbd> | 244 | <kbd>n</kbd> | ||
| 238 | </dt> | 245 | </dt> | ||
| 239 | <dd>merge request: next note in the diff</dd> | 246 | <dd>merge request: next note in the diff</dd> | ||
| @@ -383,6 +390,8 @@ | |||||
| 383 | k-split from:body -> ermite.layout('split'); | 390 | k-split from:body -> ermite.layout('split'); | ||
| 384 | k-unified from:body -> ermite.layout('unified'); | 391 | k-unified from:body -> ermite.layout('unified'); | ||
| 385 | k-collapse from:body -> ermite.collapse(); | 392 | k-collapse from:body -> ermite.collapse(); | ||
| 393 | + | k-expand from:body -> ermite.expandFile(); | |||
| 394 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 386 | k-permalink from:body -> ermite.permalink(); | 395 | k-permalink from:body -> ermite.permalink(); | ||
| 387 | k-merge from:body -> window.ermiteMR?.openMerge(); | 396 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 388 | k-read from:body -> window.ermiteMR?.markRead(); | 397 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-code.html +9 -0
| @@ -304,6 +304,13 @@ | |||
| 304 | 304 | </div> | |
| 305 | 305 | <div> | |
| 306 | 306 | <dt> | |
| 307 | + | <kbd>e</kbd> | |
| 308 | + | <kbd>E</kbd> | |
| 309 | + | </dt> | |
| 310 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 311 | + | </div> | |
| 312 | + | <div> | |
| 313 | + | <dt> | |
| 307 | 314 | <kbd>n</kbd> | |
| 308 | 315 | </dt> | |
| 309 | 316 | <dd>merge request: next note in the diff</dd> | |
| @@ -453,6 +460,8 @@ | |||
| 453 | 460 | k-split from:body -> ermite.layout('split'); | |
| 454 | 461 | k-unified from:body -> ermite.layout('unified'); | |
| 455 | 462 | k-collapse from:body -> ermite.collapse(); | |
| 463 | + | k-expand from:body -> ermite.expandFile(); | |
| 464 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 456 | 465 | k-permalink from:body -> ermite.permalink(); | |
| 457 | 466 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 458 | 467 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -304,6 +304,13 @@ | |||||
| 304 | </div> | 304 | </div> | ||
| 305 | <div> | 305 | <div> | ||
| 306 | <dt> | 306 | <dt> | ||
| 307 | + | <kbd>e</kbd> | |||
| 308 | + | <kbd>E</kbd> | |||
| 309 | + | </dt> | |||
| 310 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 311 | + | </div> | |||
| 312 | + | <div> | |||
| 313 | + | <dt> | |||
| 307 | <kbd>n</kbd> | 314 | <kbd>n</kbd> | ||
| 308 | </dt> | 315 | </dt> | ||
| 309 | <dd>merge request: next note in the diff</dd> | 316 | <dd>merge request: next note in the diff</dd> | ||
| @@ -453,6 +460,8 @@ | |||||
| 453 | k-split from:body -> ermite.layout('split'); | 460 | k-split from:body -> ermite.layout('split'); | ||
| 454 | k-unified from:body -> ermite.layout('unified'); | 461 | k-unified from:body -> ermite.layout('unified'); | ||
| 455 | k-collapse from:body -> ermite.collapse(); | 462 | k-collapse from:body -> ermite.collapse(); | ||
| 463 | + | k-expand from:body -> ermite.expandFile(); | |||
| 464 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 456 | k-permalink from:body -> ermite.permalink(); | 465 | k-permalink from:body -> ermite.permalink(); | ||
| 457 | k-merge from:body -> window.ermiteMR?.openMerge(); | 466 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 458 | k-read from:body -> window.ermiteMR?.markRead(); | 467 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-empty.html +9 -0
| @@ -234,6 +234,13 @@ | |||
| 234 | 234 | </div> | |
| 235 | 235 | <div> | |
| 236 | 236 | <dt> | |
| 237 | + | <kbd>e</kbd> | |
| 238 | + | <kbd>E</kbd> | |
| 239 | + | </dt> | |
| 240 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 241 | + | </div> | |
| 242 | + | <div> | |
| 243 | + | <dt> | |
| 237 | 244 | <kbd>n</kbd> | |
| 238 | 245 | </dt> | |
| 239 | 246 | <dd>merge request: next note in the diff</dd> | |
| @@ -383,6 +390,8 @@ | |||
| 383 | 390 | k-split from:body -> ermite.layout('split'); | |
| 384 | 391 | k-unified from:body -> ermite.layout('unified'); | |
| 385 | 392 | k-collapse from:body -> ermite.collapse(); | |
| 393 | + | k-expand from:body -> ermite.expandFile(); | |
| 394 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 386 | 395 | k-permalink from:body -> ermite.permalink(); | |
| 387 | 396 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 388 | 397 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -234,6 +234,13 @@ | |||||
| 234 | </div> | 234 | </div> | ||
| 235 | <div> | 235 | <div> | ||
| 236 | <dt> | 236 | <dt> | ||
| 237 | + | <kbd>e</kbd> | |||
| 238 | + | <kbd>E</kbd> | |||
| 239 | + | </dt> | |||
| 240 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 241 | + | </div> | |||
| 242 | + | <div> | |||
| 243 | + | <dt> | |||
| 237 | <kbd>n</kbd> | 244 | <kbd>n</kbd> | ||
| 238 | </dt> | 245 | </dt> | ||
| 239 | <dd>merge request: next note in the diff</dd> | 246 | <dd>merge request: next note in the diff</dd> | ||
| @@ -383,6 +390,8 @@ | |||||
| 383 | k-split from:body -> ermite.layout('split'); | 390 | k-split from:body -> ermite.layout('split'); | ||
| 384 | k-unified from:body -> ermite.layout('unified'); | 391 | k-unified from:body -> ermite.layout('unified'); | ||
| 385 | k-collapse from:body -> ermite.collapse(); | 392 | k-collapse from:body -> ermite.collapse(); | ||
| 393 | + | k-expand from:body -> ermite.expandFile(); | |||
| 394 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 386 | k-permalink from:body -> ermite.permalink(); | 395 | k-permalink from:body -> ermite.permalink(); | ||
| 387 | k-merge from:body -> window.ermiteMR?.openMerge(); | 396 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 388 | k-read from:body -> window.ermiteMR?.markRead(); | 397 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-image.html +9 -0
| @@ -236,6 +236,13 @@ | |||
| 236 | 236 | </div> | |
| 237 | 237 | <div> | |
| 238 | 238 | <dt> | |
| 239 | + | <kbd>e</kbd> | |
| 240 | + | <kbd>E</kbd> | |
| 241 | + | </dt> | |
| 242 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 243 | + | </div> | |
| 244 | + | <div> | |
| 245 | + | <dt> | |
| 239 | 246 | <kbd>n</kbd> | |
| 240 | 247 | </dt> | |
| 241 | 248 | <dd>merge request: next note in the diff</dd> | |
| @@ -385,6 +392,8 @@ | |||
| 385 | 392 | k-split from:body -> ermite.layout('split'); | |
| 386 | 393 | k-unified from:body -> ermite.layout('unified'); | |
| 387 | 394 | k-collapse from:body -> ermite.collapse(); | |
| 395 | + | k-expand from:body -> ermite.expandFile(); | |
| 396 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 388 | 397 | k-permalink from:body -> ermite.permalink(); | |
| 389 | 398 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 390 | 399 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -236,6 +236,13 @@ | |||||
| 236 | </div> | 236 | </div> | ||
| 237 | <div> | 237 | <div> | ||
| 238 | <dt> | 238 | <dt> | ||
| 239 | + | <kbd>e</kbd> | |||
| 240 | + | <kbd>E</kbd> | |||
| 241 | + | </dt> | |||
| 242 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 243 | + | </div> | |||
| 244 | + | <div> | |||
| 245 | + | <dt> | |||
| 239 | <kbd>n</kbd> | 246 | <kbd>n</kbd> | ||
| 240 | </dt> | 247 | </dt> | ||
| 241 | <dd>merge request: next note in the diff</dd> | 248 | <dd>merge request: next note in the diff</dd> | ||
| @@ -385,6 +392,8 @@ | |||||
| 385 | k-split from:body -> ermite.layout('split'); | 392 | k-split from:body -> ermite.layout('split'); | ||
| 386 | k-unified from:body -> ermite.layout('unified'); | 393 | k-unified from:body -> ermite.layout('unified'); | ||
| 387 | k-collapse from:body -> ermite.collapse(); | 394 | k-collapse from:body -> ermite.collapse(); | ||
| 395 | + | k-expand from:body -> ermite.expandFile(); | |||
| 396 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 388 | k-permalink from:body -> ermite.permalink(); | 397 | k-permalink from:body -> ermite.permalink(); | ||
| 389 | k-merge from:body -> window.ermiteMR?.openMerge(); | 398 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 390 | k-read from:body -> window.ermiteMR?.markRead(); | 399 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-markdown.html +9 -0
| @@ -301,6 +301,13 @@ | |||
| 301 | 301 | </div> | |
| 302 | 302 | <div> | |
| 303 | 303 | <dt> | |
| 304 | + | <kbd>e</kbd> | |
| 305 | + | <kbd>E</kbd> | |
| 306 | + | </dt> | |
| 307 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 308 | + | </div> | |
| 309 | + | <div> | |
| 310 | + | <dt> | |
| 304 | 311 | <kbd>n</kbd> | |
| 305 | 312 | </dt> | |
| 306 | 313 | <dd>merge request: next note in the diff</dd> | |
| @@ -450,6 +457,8 @@ | |||
| 450 | 457 | k-split from:body -> ermite.layout('split'); | |
| 451 | 458 | k-unified from:body -> ermite.layout('unified'); | |
| 452 | 459 | k-collapse from:body -> ermite.collapse(); | |
| 460 | + | k-expand from:body -> ermite.expandFile(); | |
| 461 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 453 | 462 | k-permalink from:body -> ermite.permalink(); | |
| 454 | 463 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 455 | 464 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -301,6 +301,13 @@ | |||||
| 301 | </div> | 301 | </div> | ||
| 302 | <div> | 302 | <div> | ||
| 303 | <dt> | 303 | <dt> | ||
| 304 | + | <kbd>e</kbd> | |||
| 305 | + | <kbd>E</kbd> | |||
| 306 | + | </dt> | |||
| 307 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 308 | + | </div> | |||
| 309 | + | <div> | |||
| 310 | + | <dt> | |||
| 304 | <kbd>n</kbd> | 311 | <kbd>n</kbd> | ||
| 305 | </dt> | 312 | </dt> | ||
| 306 | <dd>merge request: next note in the diff</dd> | 313 | <dd>merge request: next note in the diff</dd> | ||
| @@ -450,6 +457,8 @@ | |||||
| 450 | k-split from:body -> ermite.layout('split'); | 457 | k-split from:body -> ermite.layout('split'); | ||
| 451 | k-unified from:body -> ermite.layout('unified'); | 458 | k-unified from:body -> ermite.layout('unified'); | ||
| 452 | k-collapse from:body -> ermite.collapse(); | 459 | k-collapse from:body -> ermite.collapse(); | ||
| 460 | + | k-expand from:body -> ermite.expandFile(); | |||
| 461 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 453 | k-permalink from:body -> ermite.permalink(); | 462 | k-permalink from:body -> ermite.permalink(); | ||
| 454 | k-merge from:body -> window.ermiteMR?.openMerge(); | 463 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 455 | k-read from:body -> window.ermiteMR?.markRead(); | 464 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-svg.html +9 -0
| @@ -259,6 +259,13 @@ | |||
| 259 | 259 | </div> | |
| 260 | 260 | <div> | |
| 261 | 261 | <dt> | |
| 262 | + | <kbd>e</kbd> | |
| 263 | + | <kbd>E</kbd> | |
| 264 | + | </dt> | |
| 265 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 266 | + | </div> | |
| 267 | + | <div> | |
| 268 | + | <dt> | |
| 262 | 269 | <kbd>n</kbd> | |
| 263 | 270 | </dt> | |
| 264 | 271 | <dd>merge request: next note in the diff</dd> | |
| @@ -408,6 +415,8 @@ | |||
| 408 | 415 | k-split from:body -> ermite.layout('split'); | |
| 409 | 416 | k-unified from:body -> ermite.layout('unified'); | |
| 410 | 417 | k-collapse from:body -> ermite.collapse(); | |
| 418 | + | k-expand from:body -> ermite.expandFile(); | |
| 419 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 411 | 420 | k-permalink from:body -> ermite.permalink(); | |
| 412 | 421 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 413 | 422 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -259,6 +259,13 @@ | |||||
| 259 | </div> | 259 | </div> | ||
| 260 | <div> | 260 | <div> | ||
| 261 | <dt> | 261 | <dt> | ||
| 262 | + | <kbd>e</kbd> | |||
| 263 | + | <kbd>E</kbd> | |||
| 264 | + | </dt> | |||
| 265 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 266 | + | </div> | |||
| 267 | + | <div> | |||
| 268 | + | <dt> | |||
| 262 | <kbd>n</kbd> | 269 | <kbd>n</kbd> | ||
| 263 | </dt> | 270 | </dt> | ||
| 264 | <dd>merge request: next note in the diff</dd> | 271 | <dd>merge request: next note in the diff</dd> | ||
| @@ -408,6 +415,8 @@ | |||||
| 408 | k-split from:body -> ermite.layout('split'); | 415 | k-split from:body -> ermite.layout('split'); | ||
| 409 | k-unified from:body -> ermite.layout('unified'); | 416 | k-unified from:body -> ermite.layout('unified'); | ||
| 410 | k-collapse from:body -> ermite.collapse(); | 417 | k-collapse from:body -> ermite.collapse(); | ||
| 418 | + | k-expand from:body -> ermite.expandFile(); | |||
| 419 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 411 | k-permalink from:body -> ermite.permalink(); | 420 | k-permalink from:body -> ermite.permalink(); | ||
| 412 | k-merge from:body -> window.ermiteMR?.openMerge(); | 421 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 413 | k-read from:body -> window.ermiteMR?.markRead(); | 422 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-text.html +9 -0
| @@ -254,6 +254,13 @@ | |||
| 254 | 254 | </div> | |
| 255 | 255 | <div> | |
| 256 | 256 | <dt> | |
| 257 | + | <kbd>e</kbd> | |
| 258 | + | <kbd>E</kbd> | |
| 259 | + | </dt> | |
| 260 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 261 | + | </div> | |
| 262 | + | <div> | |
| 263 | + | <dt> | |
| 257 | 264 | <kbd>n</kbd> | |
| 258 | 265 | </dt> | |
| 259 | 266 | <dd>merge request: next note in the diff</dd> | |
| @@ -403,6 +410,8 @@ | |||
| 403 | 410 | k-split from:body -> ermite.layout('split'); | |
| 404 | 411 | k-unified from:body -> ermite.layout('unified'); | |
| 405 | 412 | k-collapse from:body -> ermite.collapse(); | |
| 413 | + | k-expand from:body -> ermite.expandFile(); | |
| 414 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 406 | 415 | k-permalink from:body -> ermite.permalink(); | |
| 407 | 416 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 408 | 417 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -254,6 +254,13 @@ | |||||
| 254 | </div> | 254 | </div> | ||
| 255 | <div> | 255 | <div> | ||
| 256 | <dt> | 256 | <dt> | ||
| 257 | + | <kbd>e</kbd> | |||
| 258 | + | <kbd>E</kbd> | |||
| 259 | + | </dt> | |||
| 260 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 261 | + | </div> | |||
| 262 | + | <div> | |||
| 263 | + | <dt> | |||
| 257 | <kbd>n</kbd> | 264 | <kbd>n</kbd> | ||
| 258 | </dt> | 265 | </dt> | ||
| 259 | <dd>merge request: next note in the diff</dd> | 266 | <dd>merge request: next note in the diff</dd> | ||
| @@ -403,6 +410,8 @@ | |||||
| 403 | k-split from:body -> ermite.layout('split'); | 410 | k-split from:body -> ermite.layout('split'); | ||
| 404 | k-unified from:body -> ermite.layout('unified'); | 411 | k-unified from:body -> ermite.layout('unified'); | ||
| 405 | k-collapse from:body -> ermite.collapse(); | 412 | k-collapse from:body -> ermite.collapse(); | ||
| 413 | + | k-expand from:body -> ermite.expandFile(); | |||
| 414 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 406 | k-permalink from:body -> ermite.permalink(); | 415 | k-permalink from:body -> ermite.permalink(); | ||
| 407 | k-merge from:body -> window.ermiteMR?.openMerge(); | 416 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 408 | k-read from:body -> window.ermiteMR?.markRead(); | 417 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-too-large.html +9 -0
| @@ -234,6 +234,13 @@ | |||
| 234 | 234 | </div> | |
| 235 | 235 | <div> | |
| 236 | 236 | <dt> | |
| 237 | + | <kbd>e</kbd> | |
| 238 | + | <kbd>E</kbd> | |
| 239 | + | </dt> | |
| 240 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 241 | + | </div> | |
| 242 | + | <div> | |
| 243 | + | <dt> | |
| 237 | 244 | <kbd>n</kbd> | |
| 238 | 245 | </dt> | |
| 239 | 246 | <dd>merge request: next note in the diff</dd> | |
| @@ -383,6 +390,8 @@ | |||
| 383 | 390 | k-split from:body -> ermite.layout('split'); | |
| 384 | 391 | k-unified from:body -> ermite.layout('unified'); | |
| 385 | 392 | k-collapse from:body -> ermite.collapse(); | |
| 393 | + | k-expand from:body -> ermite.expandFile(); | |
| 394 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 386 | 395 | k-permalink from:body -> ermite.permalink(); | |
| 387 | 396 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 388 | 397 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -234,6 +234,13 @@ | |||||
| 234 | </div> | 234 | </div> | ||
| 235 | <div> | 235 | <div> | ||
| 236 | <dt> | 236 | <dt> | ||
| 237 | + | <kbd>e</kbd> | |||
| 238 | + | <kbd>E</kbd> | |||
| 239 | + | </dt> | |||
| 240 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 241 | + | </div> | |||
| 242 | + | <div> | |||
| 243 | + | <dt> | |||
| 237 | <kbd>n</kbd> | 244 | <kbd>n</kbd> | ||
| 238 | </dt> | 245 | </dt> | ||
| 239 | <dd>merge request: next note in the diff</dd> | 246 | <dd>merge request: next note in the diff</dd> | ||
| @@ -383,6 +390,8 @@ | |||||
| 383 | k-split from:body -> ermite.layout('split'); | 390 | k-split from:body -> ermite.layout('split'); | ||
| 384 | k-unified from:body -> ermite.layout('unified'); | 391 | k-unified from:body -> ermite.layout('unified'); | ||
| 385 | k-collapse from:body -> ermite.collapse(); | 392 | k-collapse from:body -> ermite.collapse(); | ||
| 393 | + | k-expand from:body -> ermite.expandFile(); | |||
| 394 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 386 | k-permalink from:body -> ermite.permalink(); | 395 | k-permalink from:body -> ermite.permalink(); | ||
| 387 | k-merge from:body -> window.ermiteMR?.openMerge(); | 396 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 388 | k-read from:body -> window.ermiteMR?.markRead(); | 397 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/blob-trojan.html +9 -0
| @@ -301,6 +301,13 @@ | |||
| 301 | 301 | </div> | |
| 302 | 302 | <div> | |
| 303 | 303 | <dt> | |
| 304 | + | <kbd>e</kbd> | |
| 305 | + | <kbd>E</kbd> | |
| 306 | + | </dt> | |
| 307 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 308 | + | </div> | |
| 309 | + | <div> | |
| 310 | + | <dt> | |
| 304 | 311 | <kbd>n</kbd> | |
| 305 | 312 | </dt> | |
| 306 | 313 | <dd>merge request: next note in the diff</dd> | |
| @@ -450,6 +457,8 @@ | |||
| 450 | 457 | k-split from:body -> ermite.layout('split'); | |
| 451 | 458 | k-unified from:body -> ermite.layout('unified'); | |
| 452 | 459 | k-collapse from:body -> ermite.collapse(); | |
| 460 | + | k-expand from:body -> ermite.expandFile(); | |
| 461 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 453 | 462 | k-permalink from:body -> ermite.permalink(); | |
| 454 | 463 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 455 | 464 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -301,6 +301,13 @@ | |||||
| 301 | </div> | 301 | </div> | ||
| 302 | <div> | 302 | <div> | ||
| 303 | <dt> | 303 | <dt> | ||
| 304 | + | <kbd>e</kbd> | |||
| 305 | + | <kbd>E</kbd> | |||
| 306 | + | </dt> | |||
| 307 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 308 | + | </div> | |||
| 309 | + | <div> | |||
| 310 | + | <dt> | |||
| 304 | <kbd>n</kbd> | 311 | <kbd>n</kbd> | ||
| 305 | </dt> | 312 | </dt> | ||
| 306 | <dd>merge request: next note in the diff</dd> | 313 | <dd>merge request: next note in the diff</dd> | ||
| @@ -450,6 +457,8 @@ | |||||
| 450 | k-split from:body -> ermite.layout('split'); | 457 | k-split from:body -> ermite.layout('split'); | ||
| 451 | k-unified from:body -> ermite.layout('unified'); | 458 | k-unified from:body -> ermite.layout('unified'); | ||
| 452 | k-collapse from:body -> ermite.collapse(); | 459 | k-collapse from:body -> ermite.collapse(); | ||
| 460 | + | k-expand from:body -> ermite.expandFile(); | |||
| 461 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 453 | k-permalink from:body -> ermite.permalink(); | 462 | k-permalink from:body -> ermite.permalink(); | ||
| 454 | k-merge from:body -> window.ermiteMR?.openMerge(); | 463 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 455 | k-read from:body -> window.ermiteMR?.markRead(); | 464 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-big.html +9 -0
| @@ -272,6 +272,13 @@ | |||
| 272 | 272 | </div> | |
| 273 | 273 | <div> | |
| 274 | 274 | <dt> | |
| 275 | + | <kbd>e</kbd> | |
| 276 | + | <kbd>E</kbd> | |
| 277 | + | </dt> | |
| 278 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 279 | + | </div> | |
| 280 | + | <div> | |
| 281 | + | <dt> | |
| 275 | 282 | <kbd>n</kbd> | |
| 276 | 283 | </dt> | |
| 277 | 284 | <dd>merge request: next note in the diff</dd> | |
| @@ -421,6 +428,8 @@ | |||
| 421 | 428 | k-split from:body -> ermite.layout('split'); | |
| 422 | 429 | k-unified from:body -> ermite.layout('unified'); | |
| 423 | 430 | k-collapse from:body -> ermite.collapse(); | |
| 431 | + | k-expand from:body -> ermite.expandFile(); | |
| 432 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 424 | 433 | k-permalink from:body -> ermite.permalink(); | |
| 425 | 434 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 426 | 435 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -272,6 +272,13 @@ | |||||
| 272 | </div> | 272 | </div> | ||
| 273 | <div> | 273 | <div> | ||
| 274 | <dt> | 274 | <dt> | ||
| 275 | + | <kbd>e</kbd> | |||
| 276 | + | <kbd>E</kbd> | |||
| 277 | + | </dt> | |||
| 278 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 279 | + | </div> | |||
| 280 | + | <div> | |||
| 281 | + | <dt> | |||
| 275 | <kbd>n</kbd> | 282 | <kbd>n</kbd> | ||
| 276 | </dt> | 283 | </dt> | ||
| 277 | <dd>merge request: next note in the diff</dd> | 284 | <dd>merge request: next note in the diff</dd> | ||
| @@ -421,6 +428,8 @@ | |||||
| 421 | k-split from:body -> ermite.layout('split'); | 428 | k-split from:body -> ermite.layout('split'); | ||
| 422 | k-unified from:body -> ermite.layout('unified'); | 429 | k-unified from:body -> ermite.layout('unified'); | ||
| 423 | k-collapse from:body -> ermite.collapse(); | 430 | k-collapse from:body -> ermite.collapse(); | ||
| 431 | + | k-expand from:body -> ermite.expandFile(); | |||
| 432 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 424 | k-permalink from:body -> ermite.permalink(); | 433 | k-permalink from:body -> ermite.permalink(); | ||
| 425 | k-merge from:body -> window.ermiteMR?.openMerge(); | 434 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 426 | k-read from:body -> window.ermiteMR?.markRead(); | 435 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-binary.html +9 -0
| @@ -264,6 +264,13 @@ | |||
| 264 | 264 | </div> | |
| 265 | 265 | <div> | |
| 266 | 266 | <dt> | |
| 267 | + | <kbd>e</kbd> | |
| 268 | + | <kbd>E</kbd> | |
| 269 | + | </dt> | |
| 270 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 271 | + | </div> | |
| 272 | + | <div> | |
| 273 | + | <dt> | |
| 267 | 274 | <kbd>n</kbd> | |
| 268 | 275 | </dt> | |
| 269 | 276 | <dd>merge request: next note in the diff</dd> | |
| @@ -413,6 +420,8 @@ | |||
| 413 | 420 | k-split from:body -> ermite.layout('split'); | |
| 414 | 421 | k-unified from:body -> ermite.layout('unified'); | |
| 415 | 422 | k-collapse from:body -> ermite.collapse(); | |
| 423 | + | k-expand from:body -> ermite.expandFile(); | |
| 424 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 416 | 425 | k-permalink from:body -> ermite.permalink(); | |
| 417 | 426 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 418 | 427 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -264,6 +264,13 @@ | |||||
| 264 | </div> | 264 | </div> | ||
| 265 | <div> | 265 | <div> | ||
| 266 | <dt> | 266 | <dt> | ||
| 267 | + | <kbd>e</kbd> | |||
| 268 | + | <kbd>E</kbd> | |||
| 269 | + | </dt> | |||
| 270 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 271 | + | </div> | |||
| 272 | + | <div> | |||
| 273 | + | <dt> | |||
| 267 | <kbd>n</kbd> | 274 | <kbd>n</kbd> | ||
| 268 | </dt> | 275 | </dt> | ||
| 269 | <dd>merge request: next note in the diff</dd> | 276 | <dd>merge request: next note in the diff</dd> | ||
| @@ -413,6 +420,8 @@ | |||||
| 413 | k-split from:body -> ermite.layout('split'); | 420 | k-split from:body -> ermite.layout('split'); | ||
| 414 | k-unified from:body -> ermite.layout('unified'); | 421 | k-unified from:body -> ermite.layout('unified'); | ||
| 415 | k-collapse from:body -> ermite.collapse(); | 422 | k-collapse from:body -> ermite.collapse(); | ||
| 423 | + | k-expand from:body -> ermite.expandFile(); | |||
| 424 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 416 | k-permalink from:body -> ermite.permalink(); | 425 | k-permalink from:body -> ermite.permalink(); | ||
| 417 | k-merge from:body -> window.ermiteMR?.openMerge(); | 426 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 418 | k-read from:body -> window.ermiteMR?.markRead(); | 427 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-merge.html +9 -0
| @@ -306,6 +306,13 @@ | |||
| 306 | 306 | </div> | |
| 307 | 307 | <div> | |
| 308 | 308 | <dt> | |
| 309 | + | <kbd>e</kbd> | |
| 310 | + | <kbd>E</kbd> | |
| 311 | + | </dt> | |
| 312 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 313 | + | </div> | |
| 314 | + | <div> | |
| 315 | + | <dt> | |
| 309 | 316 | <kbd>n</kbd> | |
| 310 | 317 | </dt> | |
| 311 | 318 | <dd>merge request: next note in the diff</dd> | |
| @@ -455,6 +462,8 @@ | |||
| 455 | 462 | k-split from:body -> ermite.layout('split'); | |
| 456 | 463 | k-unified from:body -> ermite.layout('unified'); | |
| 457 | 464 | k-collapse from:body -> ermite.collapse(); | |
| 465 | + | k-expand from:body -> ermite.expandFile(); | |
| 466 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 458 | 467 | k-permalink from:body -> ermite.permalink(); | |
| 459 | 468 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 460 | 469 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -306,6 +306,13 @@ | |||||
| 306 | </div> | 306 | </div> | ||
| 307 | <div> | 307 | <div> | ||
| 308 | <dt> | 308 | <dt> | ||
| 309 | + | <kbd>e</kbd> | |||
| 310 | + | <kbd>E</kbd> | |||
| 311 | + | </dt> | |||
| 312 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 313 | + | </div> | |||
| 314 | + | <div> | |||
| 315 | + | <dt> | |||
| 309 | <kbd>n</kbd> | 316 | <kbd>n</kbd> | ||
| 310 | </dt> | 317 | </dt> | ||
| 311 | <dd>merge request: next note in the diff</dd> | 318 | <dd>merge request: next note in the diff</dd> | ||
| @@ -455,6 +462,8 @@ | |||||
| 455 | k-split from:body -> ermite.layout('split'); | 462 | k-split from:body -> ermite.layout('split'); | ||
| 456 | k-unified from:body -> ermite.layout('unified'); | 463 | k-unified from:body -> ermite.layout('unified'); | ||
| 457 | k-collapse from:body -> ermite.collapse(); | 464 | k-collapse from:body -> ermite.collapse(); | ||
| 465 | + | k-expand from:body -> ermite.expandFile(); | |||
| 466 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 458 | k-permalink from:body -> ermite.permalink(); | 467 | k-permalink from:body -> ermite.permalink(); | ||
| 459 | k-merge from:body -> window.ermiteMR?.openMerge(); | 468 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 460 | k-read from:body -> window.ermiteMR?.markRead(); | 469 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-modify-split.html +9 -0
| @@ -429,6 +429,13 @@ And <b>html</b> & "quotes".</pre> | |||
| 429 | 429 | </div> | |
| 430 | 430 | <div> | |
| 431 | 431 | <dt> | |
| 432 | + | <kbd>e</kbd> | |
| 433 | + | <kbd>E</kbd> | |
| 434 | + | </dt> | |
| 435 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 436 | + | </div> | |
| 437 | + | <div> | |
| 438 | + | <dt> | |
| 432 | 439 | <kbd>n</kbd> | |
| 433 | 440 | </dt> | |
| 434 | 441 | <dd>merge request: next note in the diff</dd> | |
| @@ -578,6 +585,8 @@ And <b>html</b> & "quotes".</pre> | |||
| 578 | 585 | k-split from:body -> ermite.layout('split'); | |
| 579 | 586 | k-unified from:body -> ermite.layout('unified'); | |
| 580 | 587 | k-collapse from:body -> ermite.collapse(); | |
| 588 | + | k-expand from:body -> ermite.expandFile(); | |
| 589 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 581 | 590 | k-permalink from:body -> ermite.permalink(); | |
| 582 | 591 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 583 | 592 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -429,6 +429,13 @@ And <b>html</b> & "quotes".</pre> | |||||
| 429 | </div> | 429 | </div> | ||
| 430 | <div> | 430 | <div> | ||
| 431 | <dt> | 431 | <dt> | ||
| 432 | + | <kbd>e</kbd> | |||
| 433 | + | <kbd>E</kbd> | |||
| 434 | + | </dt> | |||
| 435 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 436 | + | </div> | |||
| 437 | + | <div> | |||
| 438 | + | <dt> | |||
| 432 | <kbd>n</kbd> | 439 | <kbd>n</kbd> | ||
| 433 | </dt> | 440 | </dt> | ||
| 434 | <dd>merge request: next note in the diff</dd> | 441 | <dd>merge request: next note in the diff</dd> | ||
| @@ -578,6 +585,8 @@ And <b>html</b> & "quotes".</pre> | |||||
| 578 | k-split from:body -> ermite.layout('split'); | 585 | k-split from:body -> ermite.layout('split'); | ||
| 579 | k-unified from:body -> ermite.layout('unified'); | 586 | k-unified from:body -> ermite.layout('unified'); | ||
| 580 | k-collapse from:body -> ermite.collapse(); | 587 | k-collapse from:body -> ermite.collapse(); | ||
| 588 | + | k-expand from:body -> ermite.expandFile(); | |||
| 589 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 581 | k-permalink from:body -> ermite.permalink(); | 590 | k-permalink from:body -> ermite.permalink(); | ||
| 582 | k-merge from:body -> window.ermiteMR?.openMerge(); | 591 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 583 | k-read from:body -> window.ermiteMR?.markRead(); | 592 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-modify.html +9 -0
| @@ -429,6 +429,13 @@ And <b>html</b> & "quotes".</pre> | |||
| 429 | 429 | </div> | |
| 430 | 430 | <div> | |
| 431 | 431 | <dt> | |
| 432 | + | <kbd>e</kbd> | |
| 433 | + | <kbd>E</kbd> | |
| 434 | + | </dt> | |
| 435 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 436 | + | </div> | |
| 437 | + | <div> | |
| 438 | + | <dt> | |
| 432 | 439 | <kbd>n</kbd> | |
| 433 | 440 | </dt> | |
| 434 | 441 | <dd>merge request: next note in the diff</dd> | |
| @@ -578,6 +585,8 @@ And <b>html</b> & "quotes".</pre> | |||
| 578 | 585 | k-split from:body -> ermite.layout('split'); | |
| 579 | 586 | k-unified from:body -> ermite.layout('unified'); | |
| 580 | 587 | k-collapse from:body -> ermite.collapse(); | |
| 588 | + | k-expand from:body -> ermite.expandFile(); | |
| 589 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 581 | 590 | k-permalink from:body -> ermite.permalink(); | |
| 582 | 591 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 583 | 592 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -429,6 +429,13 @@ And <b>html</b> & "quotes".</pre> | |||||
| 429 | </div> | 429 | </div> | ||
| 430 | <div> | 430 | <div> | ||
| 431 | <dt> | 431 | <dt> | ||
| 432 | + | <kbd>e</kbd> | |||
| 433 | + | <kbd>E</kbd> | |||
| 434 | + | </dt> | |||
| 435 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 436 | + | </div> | |||
| 437 | + | <div> | |||
| 438 | + | <dt> | |||
| 432 | <kbd>n</kbd> | 439 | <kbd>n</kbd> | ||
| 433 | </dt> | 440 | </dt> | ||
| 434 | <dd>merge request: next note in the diff</dd> | 441 | <dd>merge request: next note in the diff</dd> | ||
| @@ -578,6 +585,8 @@ And <b>html</b> & "quotes".</pre> | |||||
| 578 | k-split from:body -> ermite.layout('split'); | 585 | k-split from:body -> ermite.layout('split'); | ||
| 579 | k-unified from:body -> ermite.layout('unified'); | 586 | k-unified from:body -> ermite.layout('unified'); | ||
| 580 | k-collapse from:body -> ermite.collapse(); | 587 | k-collapse from:body -> ermite.collapse(); | ||
| 588 | + | k-expand from:body -> ermite.expandFile(); | |||
| 589 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 581 | k-permalink from:body -> ermite.permalink(); | 590 | k-permalink from:body -> ermite.permalink(); | ||
| 582 | k-merge from:body -> window.ermiteMR?.openMerge(); | 591 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 583 | k-read from:body -> window.ermiteMR?.markRead(); | 592 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-rename.html +9 -0
| @@ -333,6 +333,13 @@ | |||
| 333 | 333 | </div> | |
| 334 | 334 | <div> | |
| 335 | 335 | <dt> | |
| 336 | + | <kbd>e</kbd> | |
| 337 | + | <kbd>E</kbd> | |
| 338 | + | </dt> | |
| 339 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 340 | + | </div> | |
| 341 | + | <div> | |
| 342 | + | <dt> | |
| 336 | 343 | <kbd>n</kbd> | |
| 337 | 344 | </dt> | |
| 338 | 345 | <dd>merge request: next note in the diff</dd> | |
| @@ -482,6 +489,8 @@ | |||
| 482 | 489 | k-split from:body -> ermite.layout('split'); | |
| 483 | 490 | k-unified from:body -> ermite.layout('unified'); | |
| 484 | 491 | k-collapse from:body -> ermite.collapse(); | |
| 492 | + | k-expand from:body -> ermite.expandFile(); | |
| 493 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 485 | 494 | k-permalink from:body -> ermite.permalink(); | |
| 486 | 495 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 487 | 496 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -333,6 +333,13 @@ | |||||
| 333 | </div> | 333 | </div> | ||
| 334 | <div> | 334 | <div> | ||
| 335 | <dt> | 335 | <dt> | ||
| 336 | + | <kbd>e</kbd> | |||
| 337 | + | <kbd>E</kbd> | |||
| 338 | + | </dt> | |||
| 339 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 340 | + | </div> | |||
| 341 | + | <div> | |||
| 342 | + | <dt> | |||
| 336 | <kbd>n</kbd> | 343 | <kbd>n</kbd> | ||
| 337 | </dt> | 344 | </dt> | ||
| 338 | <dd>merge request: next note in the diff</dd> | 345 | <dd>merge request: next note in the diff</dd> | ||
| @@ -482,6 +489,8 @@ | |||||
| 482 | k-split from:body -> ermite.layout('split'); | 489 | k-split from:body -> ermite.layout('split'); | ||
| 483 | k-unified from:body -> ermite.layout('unified'); | 490 | k-unified from:body -> ermite.layout('unified'); | ||
| 484 | k-collapse from:body -> ermite.collapse(); | 491 | k-collapse from:body -> ermite.collapse(); | ||
| 492 | + | k-expand from:body -> ermite.expandFile(); | |||
| 493 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 485 | k-permalink from:body -> ermite.permalink(); | 494 | k-permalink from:body -> ermite.permalink(); | ||
| 486 | k-merge from:body -> window.ermiteMR?.openMerge(); | 495 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 487 | k-read from:body -> window.ermiteMR?.markRead(); | 496 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-root.html +9 -0
| @@ -652,6 +652,13 @@ | |||
| 652 | 652 | </div> | |
| 653 | 653 | <div> | |
| 654 | 654 | <dt> | |
| 655 | + | <kbd>e</kbd> | |
| 656 | + | <kbd>E</kbd> | |
| 657 | + | </dt> | |
| 658 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 659 | + | </div> | |
| 660 | + | <div> | |
| 661 | + | <dt> | |
| 655 | 662 | <kbd>n</kbd> | |
| 656 | 663 | </dt> | |
| 657 | 664 | <dd>merge request: next note in the diff</dd> | |
| @@ -801,6 +808,8 @@ | |||
| 801 | 808 | k-split from:body -> ermite.layout('split'); | |
| 802 | 809 | k-unified from:body -> ermite.layout('unified'); | |
| 803 | 810 | k-collapse from:body -> ermite.collapse(); | |
| 811 | + | k-expand from:body -> ermite.expandFile(); | |
| 812 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 804 | 813 | k-permalink from:body -> ermite.permalink(); | |
| 805 | 814 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 806 | 815 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -652,6 +652,13 @@ | |||||
| 652 | </div> | 652 | </div> | ||
| 653 | <div> | 653 | <div> | ||
| 654 | <dt> | 654 | <dt> | ||
| 655 | + | <kbd>e</kbd> | |||
| 656 | + | <kbd>E</kbd> | |||
| 657 | + | </dt> | |||
| 658 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 659 | + | </div> | |||
| 660 | + | <div> | |||
| 661 | + | <dt> | |||
| 655 | <kbd>n</kbd> | 662 | <kbd>n</kbd> | ||
| 656 | </dt> | 663 | </dt> | ||
| 657 | <dd>merge request: next note in the diff</dd> | 664 | <dd>merge request: next note in the diff</dd> | ||
| @@ -801,6 +808,8 @@ | |||||
| 801 | k-split from:body -> ermite.layout('split'); | 808 | k-split from:body -> ermite.layout('split'); | ||
| 802 | k-unified from:body -> ermite.layout('unified'); | 809 | k-unified from:body -> ermite.layout('unified'); | ||
| 803 | k-collapse from:body -> ermite.collapse(); | 810 | k-collapse from:body -> ermite.collapse(); | ||
| 811 | + | k-expand from:body -> ermite.expandFile(); | |||
| 812 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 804 | k-permalink from:body -> ermite.permalink(); | 813 | k-permalink from:body -> ermite.permalink(); | ||
| 805 | k-merge from:body -> window.ermiteMR?.openMerge(); | 814 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 806 | k-read from:body -> window.ermiteMR?.markRead(); | 815 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commit-trojan.html +9 -0
| @@ -441,6 +441,13 @@ | |||
| 441 | 441 | </div> | |
| 442 | 442 | <div> | |
| 443 | 443 | <dt> | |
| 444 | + | <kbd>e</kbd> | |
| 445 | + | <kbd>E</kbd> | |
| 446 | + | </dt> | |
| 447 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 448 | + | </div> | |
| 449 | + | <div> | |
| 450 | + | <dt> | |
| 444 | 451 | <kbd>n</kbd> | |
| 445 | 452 | </dt> | |
| 446 | 453 | <dd>merge request: next note in the diff</dd> | |
| @@ -590,6 +597,8 @@ | |||
| 590 | 597 | k-split from:body -> ermite.layout('split'); | |
| 591 | 598 | k-unified from:body -> ermite.layout('unified'); | |
| 592 | 599 | k-collapse from:body -> ermite.collapse(); | |
| 600 | + | k-expand from:body -> ermite.expandFile(); | |
| 601 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 593 | 602 | k-permalink from:body -> ermite.permalink(); | |
| 594 | 603 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 595 | 604 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -441,6 +441,13 @@ | |||||
| 441 | </div> | 441 | </div> | ||
| 442 | <div> | 442 | <div> | ||
| 443 | <dt> | 443 | <dt> | ||
| 444 | + | <kbd>e</kbd> | |||
| 445 | + | <kbd>E</kbd> | |||
| 446 | + | </dt> | |||
| 447 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 448 | + | </div> | |||
| 449 | + | <div> | |||
| 450 | + | <dt> | |||
| 444 | <kbd>n</kbd> | 451 | <kbd>n</kbd> | ||
| 445 | </dt> | 452 | </dt> | ||
| 446 | <dd>merge request: next note in the diff</dd> | 453 | <dd>merge request: next note in the diff</dd> | ||
| @@ -590,6 +597,8 @@ | |||||
| 590 | k-split from:body -> ermite.layout('split'); | 597 | k-split from:body -> ermite.layout('split'); | ||
| 591 | k-unified from:body -> ermite.layout('unified'); | 598 | k-unified from:body -> ermite.layout('unified'); | ||
| 592 | k-collapse from:body -> ermite.collapse(); | 599 | k-collapse from:body -> ermite.collapse(); | ||
| 600 | + | k-expand from:body -> ermite.expandFile(); | |||
| 601 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 593 | k-permalink from:body -> ermite.permalink(); | 602 | k-permalink from:body -> ermite.permalink(); | ||
| 594 | k-merge from:body -> window.ermiteMR?.openMerge(); | 603 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 595 | k-read from:body -> window.ermiteMR?.markRead(); | 604 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commits-page2.html +9 -0
| @@ -265,6 +265,13 @@ | |||
| 265 | 265 | </div> | |
| 266 | 266 | <div> | |
| 267 | 267 | <dt> | |
| 268 | + | <kbd>e</kbd> | |
| 269 | + | <kbd>E</kbd> | |
| 270 | + | </dt> | |
| 271 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 272 | + | </div> | |
| 273 | + | <div> | |
| 274 | + | <dt> | |
| 268 | 275 | <kbd>n</kbd> | |
| 269 | 276 | </dt> | |
| 270 | 277 | <dd>merge request: next note in the diff</dd> | |
| @@ -400,6 +407,8 @@ | |||
| 400 | 407 | k-split from:body -> ermite.layout('split'); | |
| 401 | 408 | k-unified from:body -> ermite.layout('unified'); | |
| 402 | 409 | k-collapse from:body -> ermite.collapse(); | |
| 410 | + | k-expand from:body -> ermite.expandFile(); | |
| 411 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 403 | 412 | k-permalink from:body -> ermite.permalink(); | |
| 404 | 413 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 405 | 414 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -265,6 +265,13 @@ | |||||
| 265 | </div> | 265 | </div> | ||
| 266 | <div> | 266 | <div> | ||
| 267 | <dt> | 267 | <dt> | ||
| 268 | + | <kbd>e</kbd> | |||
| 269 | + | <kbd>E</kbd> | |||
| 270 | + | </dt> | |||
| 271 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 272 | + | </div> | |||
| 273 | + | <div> | |||
| 274 | + | <dt> | |||
| 268 | <kbd>n</kbd> | 275 | <kbd>n</kbd> | ||
| 269 | </dt> | 276 | </dt> | ||
| 270 | <dd>merge request: next note in the diff</dd> | 277 | <dd>merge request: next note in the diff</dd> | ||
| @@ -400,6 +407,8 @@ | |||||
| 400 | k-split from:body -> ermite.layout('split'); | 407 | k-split from:body -> ermite.layout('split'); | ||
| 401 | k-unified from:body -> ermite.layout('unified'); | 408 | k-unified from:body -> ermite.layout('unified'); | ||
| 402 | k-collapse from:body -> ermite.collapse(); | 409 | k-collapse from:body -> ermite.collapse(); | ||
| 410 | + | k-expand from:body -> ermite.expandFile(); | |||
| 411 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 403 | k-permalink from:body -> ermite.permalink(); | 412 | k-permalink from:body -> ermite.permalink(); | ||
| 404 | k-merge from:body -> window.ermiteMR?.openMerge(); | 413 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 405 | k-read from:body -> window.ermiteMR?.markRead(); | 414 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commits-path.html +9 -0
| @@ -280,6 +280,13 @@ | |||
| 280 | 280 | </div> | |
| 281 | 281 | <div> | |
| 282 | 282 | <dt> | |
| 283 | + | <kbd>e</kbd> | |
| 284 | + | <kbd>E</kbd> | |
| 285 | + | </dt> | |
| 286 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 287 | + | </div> | |
| 288 | + | <div> | |
| 289 | + | <dt> | |
| 283 | 290 | <kbd>n</kbd> | |
| 284 | 291 | </dt> | |
| 285 | 292 | <dd>merge request: next note in the diff</dd> | |
| @@ -415,6 +422,8 @@ | |||
| 415 | 422 | k-split from:body -> ermite.layout('split'); | |
| 416 | 423 | k-unified from:body -> ermite.layout('unified'); | |
| 417 | 424 | k-collapse from:body -> ermite.collapse(); | |
| 425 | + | k-expand from:body -> ermite.expandFile(); | |
| 426 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 418 | 427 | k-permalink from:body -> ermite.permalink(); | |
| 419 | 428 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 420 | 429 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -280,6 +280,13 @@ | |||||
| 280 | </div> | 280 | </div> | ||
| 281 | <div> | 281 | <div> | ||
| 282 | <dt> | 282 | <dt> | ||
| 283 | + | <kbd>e</kbd> | |||
| 284 | + | <kbd>E</kbd> | |||
| 285 | + | </dt> | |||
| 286 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 287 | + | </div> | |||
| 288 | + | <div> | |||
| 289 | + | <dt> | |||
| 283 | <kbd>n</kbd> | 290 | <kbd>n</kbd> | ||
| 284 | </dt> | 291 | </dt> | ||
| 285 | <dd>merge request: next note in the diff</dd> | 292 | <dd>merge request: next note in the diff</dd> | ||
| @@ -415,6 +422,8 @@ | |||||
| 415 | k-split from:body -> ermite.layout('split'); | 422 | k-split from:body -> ermite.layout('split'); | ||
| 416 | k-unified from:body -> ermite.layout('unified'); | 423 | k-unified from:body -> ermite.layout('unified'); | ||
| 417 | k-collapse from:body -> ermite.collapse(); | 424 | k-collapse from:body -> ermite.collapse(); | ||
| 425 | + | k-expand from:body -> ermite.expandFile(); | |||
| 426 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 418 | k-permalink from:body -> ermite.permalink(); | 427 | k-permalink from:body -> ermite.permalink(); | ||
| 419 | k-merge from:body -> window.ermiteMR?.openMerge(); | 428 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 420 | k-read from:body -> window.ermiteMR?.markRead(); | 429 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commits-root.html +9 -0
| @@ -1165,6 +1165,13 @@ | |||
| 1165 | 1165 | </div> | |
| 1166 | 1166 | <div> | |
| 1167 | 1167 | <dt> | |
| 1168 | + | <kbd>e</kbd> | |
| 1169 | + | <kbd>E</kbd> | |
| 1170 | + | </dt> | |
| 1171 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 1172 | + | </div> | |
| 1173 | + | <div> | |
| 1174 | + | <dt> | |
| 1168 | 1175 | <kbd>n</kbd> | |
| 1169 | 1176 | </dt> | |
| 1170 | 1177 | <dd>merge request: next note in the diff</dd> | |
| @@ -1300,6 +1307,8 @@ | |||
| 1300 | 1307 | k-split from:body -> ermite.layout('split'); | |
| 1301 | 1308 | k-unified from:body -> ermite.layout('unified'); | |
| 1302 | 1309 | k-collapse from:body -> ermite.collapse(); | |
| 1310 | + | k-expand from:body -> ermite.expandFile(); | |
| 1311 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 1303 | 1312 | k-permalink from:body -> ermite.permalink(); | |
| 1304 | 1313 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 1305 | 1314 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -1165,6 +1165,13 @@ | |||||
| 1165 | </div> | 1165 | </div> | ||
| 1166 | <div> | 1166 | <div> | ||
| 1167 | <dt> | 1167 | <dt> | ||
| 1168 | + | <kbd>e</kbd> | |||
| 1169 | + | <kbd>E</kbd> | |||
| 1170 | + | </dt> | |||
| 1171 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 1172 | + | </div> | |||
| 1173 | + | <div> | |||
| 1174 | + | <dt> | |||
| 1168 | <kbd>n</kbd> | 1175 | <kbd>n</kbd> | ||
| 1169 | </dt> | 1176 | </dt> | ||
| 1170 | <dd>merge request: next note in the diff</dd> | 1177 | <dd>merge request: next note in the diff</dd> | ||
| @@ -1300,6 +1307,8 @@ | |||||
| 1300 | k-split from:body -> ermite.layout('split'); | 1307 | k-split from:body -> ermite.layout('split'); | ||
| 1301 | k-unified from:body -> ermite.layout('unified'); | 1308 | k-unified from:body -> ermite.layout('unified'); | ||
| 1302 | k-collapse from:body -> ermite.collapse(); | 1309 | k-collapse from:body -> ermite.collapse(); | ||
| 1310 | + | k-expand from:body -> ermite.expandFile(); | |||
| 1311 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 1303 | k-permalink from:body -> ermite.permalink(); | 1312 | k-permalink from:body -> ermite.permalink(); | ||
| 1304 | k-merge from:body -> window.ermiteMR?.openMerge(); | 1313 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 1305 | k-read from:body -> window.ermiteMR?.markRead(); | 1314 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/commits-small.html +9 -0
| @@ -426,6 +426,13 @@ | |||
| 426 | 426 | </div> | |
| 427 | 427 | <div> | |
| 428 | 428 | <dt> | |
| 429 | + | <kbd>e</kbd> | |
| 430 | + | <kbd>E</kbd> | |
| 431 | + | </dt> | |
| 432 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 433 | + | </div> | |
| 434 | + | <div> | |
| 435 | + | <dt> | |
| 429 | 436 | <kbd>n</kbd> | |
| 430 | 437 | </dt> | |
| 431 | 438 | <dd>merge request: next note in the diff</dd> | |
| @@ -581,6 +588,8 @@ | |||
| 581 | 588 | k-split from:body -> ermite.layout('split'); | |
| 582 | 589 | k-unified from:body -> ermite.layout('unified'); | |
| 583 | 590 | k-collapse from:body -> ermite.collapse(); | |
| 591 | + | k-expand from:body -> ermite.expandFile(); | |
| 592 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 584 | 593 | k-permalink from:body -> ermite.permalink(); | |
| 585 | 594 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 586 | 595 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -426,6 +426,13 @@ | |||||
| 426 | </div> | 426 | </div> | ||
| 427 | <div> | 427 | <div> | ||
| 428 | <dt> | 428 | <dt> | ||
| 429 | + | <kbd>e</kbd> | |||
| 430 | + | <kbd>E</kbd> | |||
| 431 | + | </dt> | |||
| 432 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 433 | + | </div> | |||
| 434 | + | <div> | |||
| 435 | + | <dt> | |||
| 429 | <kbd>n</kbd> | 436 | <kbd>n</kbd> | ||
| 430 | </dt> | 437 | </dt> | ||
| 431 | <dd>merge request: next note in the diff</dd> | 438 | <dd>merge request: next note in the diff</dd> | ||
| @@ -581,6 +588,8 @@ | |||||
| 581 | k-split from:body -> ermite.layout('split'); | 588 | k-split from:body -> ermite.layout('split'); | ||
| 582 | k-unified from:body -> ermite.layout('unified'); | 589 | k-unified from:body -> ermite.layout('unified'); | ||
| 583 | k-collapse from:body -> ermite.collapse(); | 590 | k-collapse from:body -> ermite.collapse(); | ||
| 591 | + | k-expand from:body -> ermite.expandFile(); | |||
| 592 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 584 | k-permalink from:body -> ermite.permalink(); | 593 | k-permalink from:body -> ermite.permalink(); | ||
| 585 | k-merge from:body -> window.ermiteMR?.openMerge(); | 594 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 586 | k-read from:body -> window.ermiteMR?.markRead(); | 595 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/empty.html +9 -0
| @@ -183,6 +183,13 @@ | |||
| 183 | 183 | </div> | |
| 184 | 184 | <div> | |
| 185 | 185 | <dt> | |
| 186 | + | <kbd>e</kbd> | |
| 187 | + | <kbd>E</kbd> | |
| 188 | + | </dt> | |
| 189 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 190 | + | </div> | |
| 191 | + | <div> | |
| 192 | + | <dt> | |
| 186 | 193 | <kbd>n</kbd> | |
| 187 | 194 | </dt> | |
| 188 | 195 | <dd>merge request: next note in the diff</dd> | |
| @@ -249,6 +256,8 @@ | |||
| 249 | 256 | k-split from:body -> ermite.layout('split'); | |
| 250 | 257 | k-unified from:body -> ermite.layout('unified'); | |
| 251 | 258 | k-collapse from:body -> ermite.collapse(); | |
| 259 | + | k-expand from:body -> ermite.expandFile(); | |
| 260 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 252 | 261 | k-permalink from:body -> ermite.permalink(); | |
| 253 | 262 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 254 | 263 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -183,6 +183,13 @@ | |||||
| 183 | </div> | 183 | </div> | ||
| 184 | <div> | 184 | <div> | ||
| 185 | <dt> | 185 | <dt> | ||
| 186 | + | <kbd>e</kbd> | |||
| 187 | + | <kbd>E</kbd> | |||
| 188 | + | </dt> | |||
| 189 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 190 | + | </div> | |||
| 191 | + | <div> | |||
| 192 | + | <dt> | |||
| 186 | <kbd>n</kbd> | 193 | <kbd>n</kbd> | ||
| 187 | </dt> | 194 | </dt> | ||
| 188 | <dd>merge request: next note in the diff</dd> | 195 | <dd>merge request: next note in the diff</dd> | ||
| @@ -249,6 +256,8 @@ | |||||
| 249 | k-split from:body -> ermite.layout('split'); | 256 | k-split from:body -> ermite.layout('split'); | ||
| 250 | k-unified from:body -> ermite.layout('unified'); | 257 | k-unified from:body -> ermite.layout('unified'); | ||
| 251 | k-collapse from:body -> ermite.collapse(); | 258 | k-collapse from:body -> ermite.collapse(); | ||
| 259 | + | k-expand from:body -> ermite.expandFile(); | |||
| 260 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 252 | k-permalink from:body -> ermite.permalink(); | 261 | k-permalink from:body -> ermite.permalink(); | ||
| 253 | k-merge from:body -> window.ermiteMR?.openMerge(); | 262 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 254 | k-read from:body -> window.ermiteMR?.markRead(); | 263 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/home.html +9 -0
| @@ -215,6 +215,13 @@ | |||
| 215 | 215 | </div> | |
| 216 | 216 | <div> | |
| 217 | 217 | <dt> | |
| 218 | + | <kbd>e</kbd> | |
| 219 | + | <kbd>E</kbd> | |
| 220 | + | </dt> | |
| 221 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 222 | + | </div> | |
| 223 | + | <div> | |
| 224 | + | <dt> | |
| 218 | 225 | <kbd>n</kbd> | |
| 219 | 226 | </dt> | |
| 220 | 227 | <dd>merge request: next note in the diff</dd> | |
| @@ -287,6 +294,8 @@ | |||
| 287 | 294 | k-split from:body -> ermite.layout('split'); | |
| 288 | 295 | k-unified from:body -> ermite.layout('unified'); | |
| 289 | 296 | k-collapse from:body -> ermite.collapse(); | |
| 297 | + | k-expand from:body -> ermite.expandFile(); | |
| 298 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 290 | 299 | k-permalink from:body -> ermite.permalink(); | |
| 291 | 300 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 292 | 301 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -215,6 +215,13 @@ | |||||
| 215 | </div> | 215 | </div> | ||
| 216 | <div> | 216 | <div> | ||
| 217 | <dt> | 217 | <dt> | ||
| 218 | + | <kbd>e</kbd> | |||
| 219 | + | <kbd>E</kbd> | |||
| 220 | + | </dt> | |||
| 221 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 222 | + | </div> | |||
| 223 | + | <div> | |||
| 224 | + | <dt> | |||
| 218 | <kbd>n</kbd> | 225 | <kbd>n</kbd> | ||
| 219 | </dt> | 226 | </dt> | ||
| 220 | <dd>merge request: next note in the diff</dd> | 227 | <dd>merge request: next note in the diff</dd> | ||
| @@ -287,6 +294,8 @@ | |||||
| 287 | k-split from:body -> ermite.layout('split'); | 294 | k-split from:body -> ermite.layout('split'); | ||
| 288 | k-unified from:body -> ermite.layout('unified'); | 295 | k-unified from:body -> ermite.layout('unified'); | ||
| 289 | k-collapse from:body -> ermite.collapse(); | 296 | k-collapse from:body -> ermite.collapse(); | ||
| 297 | + | k-expand from:body -> ermite.expandFile(); | |||
| 298 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 290 | k-permalink from:body -> ermite.permalink(); | 299 | k-permalink from:body -> ermite.permalink(); | ||
| 291 | k-merge from:body -> window.ermiteMR?.openMerge(); | 300 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 292 | k-read from:body -> window.ermiteMR?.markRead(); | 301 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/not-found-page.html +9 -0
| @@ -172,6 +172,13 @@ | |||
| 172 | 172 | </div> | |
| 173 | 173 | <div> | |
| 174 | 174 | <dt> | |
| 175 | + | <kbd>e</kbd> | |
| 176 | + | <kbd>E</kbd> | |
| 177 | + | </dt> | |
| 178 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 179 | + | </div> | |
| 180 | + | <div> | |
| 181 | + | <dt> | |
| 175 | 182 | <kbd>n</kbd> | |
| 176 | 183 | </dt> | |
| 177 | 184 | <dd>merge request: next note in the diff</dd> | |
| @@ -232,6 +239,8 @@ | |||
| 232 | 239 | k-split from:body -> ermite.layout('split'); | |
| 233 | 240 | k-unified from:body -> ermite.layout('unified'); | |
| 234 | 241 | k-collapse from:body -> ermite.collapse(); | |
| 242 | + | k-expand from:body -> ermite.expandFile(); | |
| 243 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 235 | 244 | k-permalink from:body -> ermite.permalink(); | |
| 236 | 245 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 237 | 246 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -172,6 +172,13 @@ | |||||
| 172 | </div> | 172 | </div> | ||
| 173 | <div> | 173 | <div> | ||
| 174 | <dt> | 174 | <dt> | ||
| 175 | + | <kbd>e</kbd> | |||
| 176 | + | <kbd>E</kbd> | |||
| 177 | + | </dt> | |||
| 178 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 179 | + | </div> | |||
| 180 | + | <div> | |||
| 181 | + | <dt> | |||
| 175 | <kbd>n</kbd> | 182 | <kbd>n</kbd> | ||
| 176 | </dt> | 183 | </dt> | ||
| 177 | <dd>merge request: next note in the diff</dd> | 184 | <dd>merge request: next note in the diff</dd> | ||
| @@ -232,6 +239,8 @@ | |||||
| 232 | k-split from:body -> ermite.layout('split'); | 239 | k-split from:body -> ermite.layout('split'); | ||
| 233 | k-unified from:body -> ermite.layout('unified'); | 240 | k-unified from:body -> ermite.layout('unified'); | ||
| 234 | k-collapse from:body -> ermite.collapse(); | 241 | k-collapse from:body -> ermite.collapse(); | ||
| 242 | + | k-expand from:body -> ermite.expandFile(); | |||
| 243 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 235 | k-permalink from:body -> ermite.permalink(); | 244 | k-permalink from:body -> ermite.permalink(); | ||
| 236 | k-merge from:body -> window.ermiteMR?.openMerge(); | 245 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 237 | k-read from:body -> window.ermiteMR?.markRead(); | 246 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/not-found.html +9 -0
| @@ -172,6 +172,13 @@ | |||
| 172 | 172 | </div> | |
| 173 | 173 | <div> | |
| 174 | 174 | <dt> | |
| 175 | + | <kbd>e</kbd> | |
| 176 | + | <kbd>E</kbd> | |
| 177 | + | </dt> | |
| 178 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 179 | + | </div> | |
| 180 | + | <div> | |
| 181 | + | <dt> | |
| 175 | 182 | <kbd>n</kbd> | |
| 176 | 183 | </dt> | |
| 177 | 184 | <dd>merge request: next note in the diff</dd> | |
| @@ -232,6 +239,8 @@ | |||
| 232 | 239 | k-split from:body -> ermite.layout('split'); | |
| 233 | 240 | k-unified from:body -> ermite.layout('unified'); | |
| 234 | 241 | k-collapse from:body -> ermite.collapse(); | |
| 242 | + | k-expand from:body -> ermite.expandFile(); | |
| 243 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 235 | 244 | k-permalink from:body -> ermite.permalink(); | |
| 236 | 245 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 237 | 246 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -172,6 +172,13 @@ | |||||
| 172 | </div> | 172 | </div> | ||
| 173 | <div> | 173 | <div> | ||
| 174 | <dt> | 174 | <dt> | ||
| 175 | + | <kbd>e</kbd> | |||
| 176 | + | <kbd>E</kbd> | |||
| 177 | + | </dt> | |||
| 178 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 179 | + | </div> | |||
| 180 | + | <div> | |||
| 181 | + | <dt> | |||
| 175 | <kbd>n</kbd> | 182 | <kbd>n</kbd> | ||
| 176 | </dt> | 183 | </dt> | ||
| 177 | <dd>merge request: next note in the diff</dd> | 184 | <dd>merge request: next note in the diff</dd> | ||
| @@ -232,6 +239,8 @@ | |||||
| 232 | k-split from:body -> ermite.layout('split'); | 239 | k-split from:body -> ermite.layout('split'); | ||
| 233 | k-unified from:body -> ermite.layout('unified'); | 240 | k-unified from:body -> ermite.layout('unified'); | ||
| 234 | k-collapse from:body -> ermite.collapse(); | 241 | k-collapse from:body -> ermite.collapse(); | ||
| 242 | + | k-expand from:body -> ermite.expandFile(); | |||
| 243 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 235 | k-permalink from:body -> ermite.permalink(); | 244 | k-permalink from:body -> ermite.permalink(); | ||
| 236 | k-merge from:body -> window.ermiteMR?.openMerge(); | 245 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 237 | k-read from:body -> window.ermiteMR?.markRead(); | 246 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/refs.html +9 -0
| @@ -302,6 +302,13 @@ | |||
| 302 | 302 | </div> | |
| 303 | 303 | <div> | |
| 304 | 304 | <dt> | |
| 305 | + | <kbd>e</kbd> | |
| 306 | + | <kbd>E</kbd> | |
| 307 | + | </dt> | |
| 308 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 309 | + | </div> | |
| 310 | + | <div> | |
| 311 | + | <dt> | |
| 305 | 312 | <kbd>n</kbd> | |
| 306 | 313 | </dt> | |
| 307 | 314 | <dd>merge request: next note in the diff</dd> | |
| @@ -467,6 +474,8 @@ | |||
| 467 | 474 | k-split from:body -> ermite.layout('split'); | |
| 468 | 475 | k-unified from:body -> ermite.layout('unified'); | |
| 469 | 476 | k-collapse from:body -> ermite.collapse(); | |
| 477 | + | k-expand from:body -> ermite.expandFile(); | |
| 478 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 470 | 479 | k-permalink from:body -> ermite.permalink(); | |
| 471 | 480 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 472 | 481 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -302,6 +302,13 @@ | |||||
| 302 | </div> | 302 | </div> | ||
| 303 | <div> | 303 | <div> | ||
| 304 | <dt> | 304 | <dt> | ||
| 305 | + | <kbd>e</kbd> | |||
| 306 | + | <kbd>E</kbd> | |||
| 307 | + | </dt> | |||
| 308 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 309 | + | </div> | |||
| 310 | + | <div> | |||
| 311 | + | <dt> | |||
| 305 | <kbd>n</kbd> | 312 | <kbd>n</kbd> | ||
| 306 | </dt> | 313 | </dt> | ||
| 307 | <dd>merge request: next note in the diff</dd> | 314 | <dd>merge request: next note in the diff</dd> | ||
| @@ -467,6 +474,8 @@ | |||||
| 467 | k-split from:body -> ermite.layout('split'); | 474 | k-split from:body -> ermite.layout('split'); | ||
| 468 | k-unified from:body -> ermite.layout('unified'); | 475 | k-unified from:body -> ermite.layout('unified'); | ||
| 469 | k-collapse from:body -> ermite.collapse(); | 476 | k-collapse from:body -> ermite.collapse(); | ||
| 477 | + | k-expand from:body -> ermite.expandFile(); | |||
| 478 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 470 | k-permalink from:body -> ermite.permalink(); | 479 | k-permalink from:body -> ermite.permalink(); | ||
| 471 | k-merge from:body -> window.ermiteMR?.openMerge(); | 480 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 472 | k-read from:body -> window.ermiteMR?.markRead(); | 481 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/summary.html +9 -0
| @@ -331,6 +331,13 @@ | |||
| 331 | 331 | </div> | |
| 332 | 332 | <div> | |
| 333 | 333 | <dt> | |
| 334 | + | <kbd>e</kbd> | |
| 335 | + | <kbd>E</kbd> | |
| 336 | + | </dt> | |
| 337 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 338 | + | </div> | |
| 339 | + | <div> | |
| 340 | + | <dt> | |
| 334 | 341 | <kbd>n</kbd> | |
| 335 | 342 | </dt> | |
| 336 | 343 | <dd>merge request: next note in the diff</dd> | |
| @@ -496,6 +503,8 @@ | |||
| 496 | 503 | k-split from:body -> ermite.layout('split'); | |
| 497 | 504 | k-unified from:body -> ermite.layout('unified'); | |
| 498 | 505 | k-collapse from:body -> ermite.collapse(); | |
| 506 | + | k-expand from:body -> ermite.expandFile(); | |
| 507 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 499 | 508 | k-permalink from:body -> ermite.permalink(); | |
| 500 | 509 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 501 | 510 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -331,6 +331,13 @@ | |||||
| 331 | </div> | 331 | </div> | ||
| 332 | <div> | 332 | <div> | ||
| 333 | <dt> | 333 | <dt> | ||
| 334 | + | <kbd>e</kbd> | |||
| 335 | + | <kbd>E</kbd> | |||
| 336 | + | </dt> | |||
| 337 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 338 | + | </div> | |||
| 339 | + | <div> | |||
| 340 | + | <dt> | |||
| 334 | <kbd>n</kbd> | 341 | <kbd>n</kbd> | ||
| 335 | </dt> | 342 | </dt> | ||
| 336 | <dd>merge request: next note in the diff</dd> | 343 | <dd>merge request: next note in the diff</dd> | ||
| @@ -496,6 +503,8 @@ | |||||
| 496 | k-split from:body -> ermite.layout('split'); | 503 | k-split from:body -> ermite.layout('split'); | ||
| 497 | k-unified from:body -> ermite.layout('unified'); | 504 | k-unified from:body -> ermite.layout('unified'); | ||
| 498 | k-collapse from:body -> ermite.collapse(); | 505 | k-collapse from:body -> ermite.collapse(); | ||
| 506 | + | k-expand from:body -> ermite.expandFile(); | |||
| 507 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 499 | k-permalink from:body -> ermite.permalink(); | 508 | k-permalink from:body -> ermite.permalink(); | ||
| 500 | k-merge from:body -> window.ermiteMR?.openMerge(); | 509 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 501 | k-read from:body -> window.ermiteMR?.markRead(); | 510 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/tree-nested.html +9 -0
| @@ -254,6 +254,13 @@ | |||
| 254 | 254 | </div> | |
| 255 | 255 | <div> | |
| 256 | 256 | <dt> | |
| 257 | + | <kbd>e</kbd> | |
| 258 | + | <kbd>E</kbd> | |
| 259 | + | </dt> | |
| 260 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 261 | + | </div> | |
| 262 | + | <div> | |
| 263 | + | <dt> | |
| 257 | 264 | <kbd>n</kbd> | |
| 258 | 265 | </dt> | |
| 259 | 266 | <dd>merge request: next note in the diff</dd> | |
| @@ -403,6 +410,8 @@ | |||
| 403 | 410 | k-split from:body -> ermite.layout('split'); | |
| 404 | 411 | k-unified from:body -> ermite.layout('unified'); | |
| 405 | 412 | k-collapse from:body -> ermite.collapse(); | |
| 413 | + | k-expand from:body -> ermite.expandFile(); | |
| 414 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 406 | 415 | k-permalink from:body -> ermite.permalink(); | |
| 407 | 416 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 408 | 417 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -254,6 +254,13 @@ | |||||
| 254 | </div> | 254 | </div> | ||
| 255 | <div> | 255 | <div> | ||
| 256 | <dt> | 256 | <dt> | ||
| 257 | + | <kbd>e</kbd> | |||
| 258 | + | <kbd>E</kbd> | |||
| 259 | + | </dt> | |||
| 260 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 261 | + | </div> | |||
| 262 | + | <div> | |||
| 263 | + | <dt> | |||
| 257 | <kbd>n</kbd> | 264 | <kbd>n</kbd> | ||
| 258 | </dt> | 265 | </dt> | ||
| 259 | <dd>merge request: next note in the diff</dd> | 266 | <dd>merge request: next note in the diff</dd> | ||
| @@ -403,6 +410,8 @@ | |||||
| 403 | k-split from:body -> ermite.layout('split'); | 410 | k-split from:body -> ermite.layout('split'); | ||
| 404 | k-unified from:body -> ermite.layout('unified'); | 411 | k-unified from:body -> ermite.layout('unified'); | ||
| 405 | k-collapse from:body -> ermite.collapse(); | 412 | k-collapse from:body -> ermite.collapse(); | ||
| 413 | + | k-expand from:body -> ermite.expandFile(); | |||
| 414 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 406 | k-permalink from:body -> ermite.permalink(); | 415 | k-permalink from:body -> ermite.permalink(); | ||
| 407 | k-merge from:body -> window.ermiteMR?.openMerge(); | 416 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 408 | k-read from:body -> window.ermiteMR?.markRead(); | 417 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/tree-root.html +9 -0
| @@ -481,6 +481,13 @@ | |||
| 481 | 481 | </div> | |
| 482 | 482 | <div> | |
| 483 | 483 | <dt> | |
| 484 | + | <kbd>e</kbd> | |
| 485 | + | <kbd>E</kbd> | |
| 486 | + | </dt> | |
| 487 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 488 | + | </div> | |
| 489 | + | <div> | |
| 490 | + | <dt> | |
| 484 | 491 | <kbd>n</kbd> | |
| 485 | 492 | </dt> | |
| 486 | 493 | <dd>merge request: next note in the diff</dd> | |
| @@ -630,6 +637,8 @@ | |||
| 630 | 637 | k-split from:body -> ermite.layout('split'); | |
| 631 | 638 | k-unified from:body -> ermite.layout('unified'); | |
| 632 | 639 | k-collapse from:body -> ermite.collapse(); | |
| 640 | + | k-expand from:body -> ermite.expandFile(); | |
| 641 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 633 | 642 | k-permalink from:body -> ermite.permalink(); | |
| 634 | 643 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 635 | 644 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -481,6 +481,13 @@ | |||||
| 481 | </div> | 481 | </div> | ||
| 482 | <div> | 482 | <div> | ||
| 483 | <dt> | 483 | <dt> | ||
| 484 | + | <kbd>e</kbd> | |||
| 485 | + | <kbd>E</kbd> | |||
| 486 | + | </dt> | |||
| 487 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 488 | + | </div> | |||
| 489 | + | <div> | |||
| 490 | + | <dt> | |||
| 484 | <kbd>n</kbd> | 491 | <kbd>n</kbd> | ||
| 485 | </dt> | 492 | </dt> | ||
| 486 | <dd>merge request: next note in the diff</dd> | 493 | <dd>merge request: next note in the diff</dd> | ||
| @@ -630,6 +637,8 @@ | |||||
| 630 | k-split from:body -> ermite.layout('split'); | 637 | k-split from:body -> ermite.layout('split'); | ||
| 631 | k-unified from:body -> ermite.layout('unified'); | 638 | k-unified from:body -> ermite.layout('unified'); | ||
| 632 | k-collapse from:body -> ermite.collapse(); | 639 | k-collapse from:body -> ermite.collapse(); | ||
| 640 | + | k-expand from:body -> ermite.expandFile(); | |||
| 641 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 633 | k-permalink from:body -> ermite.permalink(); | 642 | k-permalink from:body -> ermite.permalink(); | ||
| 634 | k-merge from:body -> window.ermiteMR?.openMerge(); | 643 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 635 | k-read from:body -> window.ermiteMR?.markRead(); | 644 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/tree-slash-ref.html +9 -0
| @@ -251,6 +251,13 @@ | |||
| 251 | 251 | </div> | |
| 252 | 252 | <div> | |
| 253 | 253 | <dt> | |
| 254 | + | <kbd>e</kbd> | |
| 255 | + | <kbd>E</kbd> | |
| 256 | + | </dt> | |
| 257 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 258 | + | </div> | |
| 259 | + | <div> | |
| 260 | + | <dt> | |
| 254 | 261 | <kbd>n</kbd> | |
| 255 | 262 | </dt> | |
| 256 | 263 | <dd>merge request: next note in the diff</dd> | |
| @@ -400,6 +407,8 @@ | |||
| 400 | 407 | k-split from:body -> ermite.layout('split'); | |
| 401 | 408 | k-unified from:body -> ermite.layout('unified'); | |
| 402 | 409 | k-collapse from:body -> ermite.collapse(); | |
| 410 | + | k-expand from:body -> ermite.expandFile(); | |
| 411 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 403 | 412 | k-permalink from:body -> ermite.permalink(); | |
| 404 | 413 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 405 | 414 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -251,6 +251,13 @@ | |||||
| 251 | </div> | 251 | </div> | ||
| 252 | <div> | 252 | <div> | ||
| 253 | <dt> | 253 | <dt> | ||
| 254 | + | <kbd>e</kbd> | |||
| 255 | + | <kbd>E</kbd> | |||
| 256 | + | </dt> | |||
| 257 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 258 | + | </div> | |||
| 259 | + | <div> | |||
| 260 | + | <dt> | |||
| 254 | <kbd>n</kbd> | 261 | <kbd>n</kbd> | ||
| 255 | </dt> | 262 | </dt> | ||
| 256 | <dd>merge request: next note in the diff</dd> | 263 | <dd>merge request: next note in the diff</dd> | ||
| @@ -400,6 +407,8 @@ | |||||
| 400 | k-split from:body -> ermite.layout('split'); | 407 | k-split from:body -> ermite.layout('split'); | ||
| 401 | k-unified from:body -> ermite.layout('unified'); | 408 | k-unified from:body -> ermite.layout('unified'); | ||
| 402 | k-collapse from:body -> ermite.collapse(); | 409 | k-collapse from:body -> ermite.collapse(); | ||
| 410 | + | k-expand from:body -> ermite.expandFile(); | |||
| 411 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 403 | k-permalink from:body -> ermite.permalink(); | 412 | k-permalink from:body -> ermite.permalink(); | ||
| 404 | k-merge from:body -> window.ermiteMR?.openMerge(); | 413 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 405 | k-read from:body -> window.ermiteMR?.markRead(); | 414 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/testdata/web/tree-subdir.html +9 -0
| @@ -276,6 +276,13 @@ | |||
| 276 | 276 | </div> | |
| 277 | 277 | <div> | |
| 278 | 278 | <dt> | |
| 279 | + | <kbd>e</kbd> | |
| 280 | + | <kbd>E</kbd> | |
| 281 | + | </dt> | |
| 282 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |
| 283 | + | </div> | |
| 284 | + | <div> | |
| 285 | + | <dt> | |
| 279 | 286 | <kbd>n</kbd> | |
| 280 | 287 | </dt> | |
| 281 | 288 | <dd>merge request: next note in the diff</dd> | |
| @@ -425,6 +432,8 @@ | |||
| 425 | 432 | k-split from:body -> ermite.layout('split'); | |
| 426 | 433 | k-unified from:body -> ermite.layout('unified'); | |
| 427 | 434 | k-collapse from:body -> ermite.collapse(); | |
| 435 | + | k-expand from:body -> ermite.expandFile(); | |
| 436 | + | k-expandAll from:body -> ermite.expandAll(); | |
| 428 | 437 | k-permalink from:body -> ermite.permalink(); | |
| 429 | 438 | k-merge from:body -> window.ermiteMR?.openMerge(); | |
| 430 | 439 | k-read from:body -> window.ermiteMR?.markRead(); | |
| @@ -276,6 +276,13 @@ | |||||
| 276 | </div> | 276 | </div> | ||
| 277 | <div> | 277 | <div> | ||
| 278 | <dt> | 278 | <dt> | ||
| 279 | + | <kbd>e</kbd> | |||
| 280 | + | <kbd>E</kbd> | |||
| 281 | + | </dt> | |||
| 282 | + | <dd>diff: the whole file in view / every file, with the lines the hunks hide</dd> | |||
| 283 | + | </div> | |||
| 284 | + | <div> | |||
| 285 | + | <dt> | |||
| 279 | <kbd>n</kbd> | 286 | <kbd>n</kbd> | ||
| 280 | </dt> | 287 | </dt> | ||
| 281 | <dd>merge request: next note in the diff</dd> | 288 | <dd>merge request: next note in the diff</dd> | ||
| @@ -425,6 +432,8 @@ | |||||
| 425 | k-split from:body -> ermite.layout('split'); | 432 | k-split from:body -> ermite.layout('split'); | ||
| 426 | k-unified from:body -> ermite.layout('unified'); | 433 | k-unified from:body -> ermite.layout('unified'); | ||
| 427 | k-collapse from:body -> ermite.collapse(); | 434 | k-collapse from:body -> ermite.collapse(); | ||
| 435 | + | k-expand from:body -> ermite.expandFile(); | |||
| 436 | + | k-expandAll from:body -> ermite.expandAll(); | |||
| 428 | k-permalink from:body -> ermite.permalink(); | 437 | k-permalink from:body -> ermite.permalink(); | ||
| 429 | k-merge from:body -> window.ermiteMR?.openMerge(); | 438 | k-merge from:body -> window.ermiteMR?.openMerge(); | ||
| 430 | k-read from:body -> window.ermiteMR?.markRead(); | 439 | k-read from:body -> window.ermiteMR?.markRead(); | ||
M internal/app/web_commit_test.go +78 -0
| @@ -185,3 +185,81 @@ func TestWebCommitPrivate(t *testing.T) { | |||
| 185 | 185 | assert.Equal(t, normalise(missing), normalise(body), p) | |
| 186 | 186 | } | |
| 187 | 187 | } | |
| 188 | + | ||
| 189 | + | // The hidden lines of a diff: the page has a gap before a hunk and after the | |
| 190 | + | // last one, and /context serves their rows for both layouts, numbered on both | |
| 191 | + | // sides; a private repository's are the 404 page. | |
| 192 | + | func TestWebDiffContext(t *testing.T) { | |
| 193 | + | h := newHarness(t, harnessOpts{}) | |
| 194 | + | wt := t.TempDir() | |
| 195 | + | h.mustGit(wt, "init", "-q", "--initial-branch=main") | |
| 196 | + | var src strings.Builder | |
| 197 | + | for i := 1; i <= 40; i++ { | |
| 198 | + | fmt.Fprintf(&src, "line %d <b>\n", i) | |
| 199 | + | } | |
| 200 | + | h.put(wt, "f.txt", []byte(src.String()), 0o644) | |
| 201 | + | h.commitAt(wt, "2026-02-01T10:00:00Z", "forty lines") | |
| 202 | + | h.put(wt, "f.txt", []byte(strings.Replace(src.String(), "line 20 <b>", "line twenty", 1)), 0o644) | |
| 203 | + | sha := h.commitAt(wt, "2026-02-02T10:00:00Z", "change line 20") | |
| 204 | + | h.createRepo("ctx", domain.VisibilityPublic) | |
| 205 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx.git"), "main") | |
| 206 | + | ||
| 207 | + | _, _, page := h.get("/ctx/commit/" + sha) | |
| 208 | + | gap := "/ctx/context/" + sha + "?key=g0&n=16&new=1&old=1&path=f.txt" | |
| 209 | + | assert.Contains(t, page, gap, "lines 1 to 16 before the hunk (17 to 23)") | |
| 210 | + | assert.Contains(t, page, "16 hidden lines") | |
| 211 | + | assert.Contains(t, page, "/ctx/context/"+sha+"?key=tail&n=0&new=24&old=24&path=f.txt") | |
| 212 | + | ||
| 213 | + | st, _, body := h.get("/ctx/context/" + sha + "?key=g0&n=16&new=1&old=1&path=f.txt") | |
| 214 | + | require.Equal(t, 200, st) | |
| 215 | + | assert.Equal(t, 3, strings.Count(body, "line 16 <b>"), "unified once, split on both sides, escaped") | |
| 216 | + | assert.NotContains(t, body, "line 17") | |
| 217 | + | assert.NotContains(t, body, `class="gap"`, "nothing left") | |
| 218 | + | ||
| 219 | + | st, _, body = h.get("/ctx/context/" + sha + "?key=tail&n=0&new=24&old=24&path=f.txt") | |
| 220 | + | require.Equal(t, 200, st) | |
| 221 | + | assert.Contains(t, body, "line 40 <b>") | |
| 222 | + | assert.NotContains(t, body, "line 23 ") | |
| 223 | + | ||
| 224 | + | for _, bad := range []string{ | |
| 225 | + | "/ctx/context/" + sha + "?n=1&new=1&old=1", // no path | |
| 226 | + | "/ctx/context/" + sha + "?n=1&new=0&old=1&path=f.txt", // line 0 | |
| 227 | + | "/ctx/context/" + sha + "?n=-1&new=1&old=1&path=f.txt", // negative | |
| 228 | + | "/ctx/context/" + sha + "?n=1&new=99&old=99&path=f.txt", // past the end | |
| 229 | + | "/ctx/context/" + sha + "?n=1&new=1&old=1&path=nope.txt", // no such file | |
| 230 | + | "/ctx/context/" + sha[:7] + "?n=1&new=1&old=1&path=f.txt", // not a full id | |
| 231 | + | "/ctx/context/main?n=1&new=1&old=1&path=f.txt", // a ref | |
| 232 | + | } { | |
| 233 | + | st, _, _ := h.get(bad) | |
| 234 | + | assert.Equal(t, 404, st, bad) | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // Lines as Git numbers them: a lone CR is not a line break; the last line | |
| 238 | + | // of a file without a final newline keeps its marker. | |
| 239 | + | var cr strings.Builder | |
| 240 | + | cr.WriteString("a\rb\n") | |
| 241 | + | for i := 2; i <= 30; i++ { | |
| 242 | + | fmt.Fprintf(&cr, "row %d\n", i) | |
| 243 | + | } | |
| 244 | + | h.put(wt, "cr.txt", []byte(cr.String()+"last"), 0o644) | |
| 245 | + | h.commitAt(wt, "2026-02-03T10:00:00Z", "cr") | |
| 246 | + | h.put(wt, "cr.txt", []byte(strings.Replace(cr.String(), "row 15\n", "row fifteen\n", 1)+"last"), 0o644) | |
| 247 | + | crSHA := h.commitAt(wt, "2026-02-04T10:00:00Z", "change row 15") | |
| 248 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx.git"), "main") | |
| 249 | + | _, _, crPage := h.get("/ctx/commit/" + crSHA) | |
| 250 | + | assert.Contains(t, crPage, "11 hidden lines", "lines 1 to 11 before the hunk at 12") | |
| 251 | + | st, _, body = h.get("/ctx/context/" + crSHA + "?n=11&new=1&old=1&path=cr.txt") | |
| 252 | + | require.Equal(t, 200, st) | |
| 253 | + | assert.Contains(t, body, "row 11") | |
| 254 | + | assert.NotContains(t, body, "row 12", "a lone CR does not shift the numbers") | |
| 255 | + | st, _, body = h.get("/ctx/context/" + crSHA + "?n=0&new=19&old=19&path=cr.txt") | |
| 256 | + | require.Equal(t, 200, st) | |
| 257 | + | assert.Contains(t, body, "last<span class=\"eol\"", "no newline at the end of the file") | |
| 258 | + | ||
| 259 | + | // A private repository: the 404 page, nothing of the file. | |
| 260 | + | h.createRepo("ctx-hidden", domain.VisibilityPrivate) | |
| 261 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx-hidden.git"), "main") | |
| 262 | + | st, _, body = h.get("/ctx-hidden/context/" + sha + "?n=0&new=1&old=1&path=f.txt") | |
| 263 | + | assert.Equal(t, 404, st) | |
| 264 | + | assert.NotContains(t, body, "line 1 ") | |
| 265 | + | } | |
| @@ -185,3 +185,81 @@ func TestWebCommitPrivate(t *testing.T) { | |||||
| 185 | assert.Equal(t, normalise(missing), normalise(body), p) | 185 | assert.Equal(t, normalise(missing), normalise(body), p) | ||
| 186 | } | 186 | } | ||
| 187 | } | 187 | } | ||
| 188 | + | ||||
| 189 | + | // The hidden lines of a diff: the page has a gap before a hunk and after the | |||
| 190 | + | // last one, and /context serves their rows for both layouts, numbered on both | |||
| 191 | + | // sides; a private repository's are the 404 page. | |||
| 192 | + | func TestWebDiffContext(t *testing.T) { | |||
| 193 | + | h := newHarness(t, harnessOpts{}) | |||
| 194 | + | wt := t.TempDir() | |||
| 195 | + | h.mustGit(wt, "init", "-q", "--initial-branch=main") | |||
| 196 | + | var src strings.Builder | |||
| 197 | + | for i := 1; i <= 40; i++ { | |||
| 198 | + | fmt.Fprintf(&src, "line %d <b>\n", i) | |||
| 199 | + | } | |||
| 200 | + | h.put(wt, "f.txt", []byte(src.String()), 0o644) | |||
| 201 | + | h.commitAt(wt, "2026-02-01T10:00:00Z", "forty lines") | |||
| 202 | + | h.put(wt, "f.txt", []byte(strings.Replace(src.String(), "line 20 <b>", "line twenty", 1)), 0o644) | |||
| 203 | + | sha := h.commitAt(wt, "2026-02-02T10:00:00Z", "change line 20") | |||
| 204 | + | h.createRepo("ctx", domain.VisibilityPublic) | |||
| 205 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx.git"), "main") | |||
| 206 | + | ||||
| 207 | + | _, _, page := h.get("/ctx/commit/" + sha) | |||
| 208 | + | gap := "/ctx/context/" + sha + "?key=g0&n=16&new=1&old=1&path=f.txt" | |||
| 209 | + | assert.Contains(t, page, gap, "lines 1 to 16 before the hunk (17 to 23)") | |||
| 210 | + | assert.Contains(t, page, "16 hidden lines") | |||
| 211 | + | assert.Contains(t, page, "/ctx/context/"+sha+"?key=tail&n=0&new=24&old=24&path=f.txt") | |||
| 212 | + | ||||
| 213 | + | st, _, body := h.get("/ctx/context/" + sha + "?key=g0&n=16&new=1&old=1&path=f.txt") | |||
| 214 | + | require.Equal(t, 200, st) | |||
| 215 | + | assert.Equal(t, 3, strings.Count(body, "line 16 <b>"), "unified once, split on both sides, escaped") | |||
| 216 | + | assert.NotContains(t, body, "line 17") | |||
| 217 | + | assert.NotContains(t, body, `class="gap"`, "nothing left") | |||
| 218 | + | ||||
| 219 | + | st, _, body = h.get("/ctx/context/" + sha + "?key=tail&n=0&new=24&old=24&path=f.txt") | |||
| 220 | + | require.Equal(t, 200, st) | |||
| 221 | + | assert.Contains(t, body, "line 40 <b>") | |||
| 222 | + | assert.NotContains(t, body, "line 23 ") | |||
| 223 | + | ||||
| 224 | + | for _, bad := range []string{ | |||
| 225 | + | "/ctx/context/" + sha + "?n=1&new=1&old=1", // no path | |||
| 226 | + | "/ctx/context/" + sha + "?n=1&new=0&old=1&path=f.txt", // line 0 | |||
| 227 | + | "/ctx/context/" + sha + "?n=-1&new=1&old=1&path=f.txt", // negative | |||
| 228 | + | "/ctx/context/" + sha + "?n=1&new=99&old=99&path=f.txt", // past the end | |||
| 229 | + | "/ctx/context/" + sha + "?n=1&new=1&old=1&path=nope.txt", // no such file | |||
| 230 | + | "/ctx/context/" + sha[:7] + "?n=1&new=1&old=1&path=f.txt", // not a full id | |||
| 231 | + | "/ctx/context/main?n=1&new=1&old=1&path=f.txt", // a ref | |||
| 232 | + | } { | |||
| 233 | + | st, _, _ := h.get(bad) | |||
| 234 | + | assert.Equal(t, 404, st, bad) | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // Lines as Git numbers them: a lone CR is not a line break; the last line | |||
| 238 | + | // of a file without a final newline keeps its marker. | |||
| 239 | + | var cr strings.Builder | |||
| 240 | + | cr.WriteString("a\rb\n") | |||
| 241 | + | for i := 2; i <= 30; i++ { | |||
| 242 | + | fmt.Fprintf(&cr, "row %d\n", i) | |||
| 243 | + | } | |||
| 244 | + | h.put(wt, "cr.txt", []byte(cr.String()+"last"), 0o644) | |||
| 245 | + | h.commitAt(wt, "2026-02-03T10:00:00Z", "cr") | |||
| 246 | + | h.put(wt, "cr.txt", []byte(strings.Replace(cr.String(), "row 15\n", "row fifteen\n", 1)+"last"), 0o644) | |||
| 247 | + | crSHA := h.commitAt(wt, "2026-02-04T10:00:00Z", "change row 15") | |||
| 248 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx.git"), "main") | |||
| 249 | + | _, _, crPage := h.get("/ctx/commit/" + crSHA) | |||
| 250 | + | assert.Contains(t, crPage, "11 hidden lines", "lines 1 to 11 before the hunk at 12") | |||
| 251 | + | st, _, body = h.get("/ctx/context/" + crSHA + "?n=11&new=1&old=1&path=cr.txt") | |||
| 252 | + | require.Equal(t, 200, st) | |||
| 253 | + | assert.Contains(t, body, "row 11") | |||
| 254 | + | assert.NotContains(t, body, "row 12", "a lone CR does not shift the numbers") | |||
| 255 | + | st, _, body = h.get("/ctx/context/" + crSHA + "?n=0&new=19&old=19&path=cr.txt") | |||
| 256 | + | require.Equal(t, 200, st) | |||
| 257 | + | assert.Contains(t, body, "last<span class=\"eol\"", "no newline at the end of the file") | |||
| 258 | + | ||||
| 259 | + | // A private repository: the 404 page, nothing of the file. | |||
| 260 | + | h.createRepo("ctx-hidden", domain.VisibilityPrivate) | |||
| 261 | + | h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx-hidden.git"), "main") | |||
| 262 | + | st, _, body = h.get("/ctx-hidden/context/" + sha + "?n=0&new=1&old=1&path=f.txt") | |||
| 263 | + | assert.Equal(t, 404, st) | |||
| 264 | + | assert.NotContains(t, body, "line 1 ") | |||
| 265 | + | } | |||