Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 7 slice 1: review on the web, so approving does not start with a clone

78912785b00227cffe3f8fb26b057cd60159a6b4 · Keenan Nemetz · 17d ago · parent 9bea7f11

Phase 7 slice 1: review on the web, so approving does not start with a clone

Phase 7 is re-scoped: signing is deferred (the SSH handshake already establishes
who pushed, and there is no third-party verifier here), and the phase is now about
the experience of using the forge.

- GET /r/<repo>/commit/<sha>: a commit's metadata and the patch it introduced,
  per-file hunks with line numbers; the commit list links to it
- change requests show the same diff, computed three-dot: a review is what the
  branch adds since it diverged, not every commit main gained meanwhile
- diffs are capped at 512 KiB and labelled when cut, binary files are named
  rather than faked, and every line is escaped (a <script> line is a test)
- the repository README renders on its home page and markdown blobs render as
  markdown, through the sanitiser issues already use

internal/web/diff.go +143

@@ -0,0 +1,143 @@
1 package web
2
3 // Unified diff parsing for the web views. Git is the source of truth for what
4 // changed, so nothing here computes a diff: it reads the patch git prints and
5 // shapes it for a template. Two rules matter: every line is escaped by
6 // html/template on the way out, and a patch that is cut off (the cap) is shown
7 // as cut off rather than as a smaller change.
8
9 import (
10 "regexp"
11 "strconv"
12 "strings"
13 )
14
15 type diffFile struct {
16 OldPath string
17 NewPath string
18 Binary bool
19 Hunks []diffHunk
20 Added int
21 Removed int
22 }
23
24 // Label is what to show as the file's name: the new path, or the old one for a
25 // deletion.
26 func (f diffFile) Label() string {
27 if f.NewPath != "" {
28 return f.NewPath
29 }
30 return f.OldPath
31 }
32
33 type diffHunk struct {
34 Header string
35 Lines []diffLine
36 }
37
38 type diffLine struct {
39 Kind string // "add", "del" or "ctx"
40 OldNo int
41 NewNo int
42 Text string
43 }
44
45 var (
46 hunkRE = regexp.MustCompile(`^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@`)
47 metaRE = regexp.MustCompile(`^(index |old mode|new mode|similarity index|rename (from|to)|copy (from|to)|new file mode|deleted file mode)`)
48 quoteRE = regexp.MustCompile(`^"(.*)"$`)
49 )
50
51 // parseDiff turns `git diff`/`git show` output into files, hunks and numbered
52 // lines. Unrecognised metadata is dropped: a viewer that shows "index 3f2a.." is
53 // showing the plumbing, not the change.
54 func parseDiff(patch string) []diffFile {
55 var files []diffFile
56 var cur *diffFile
57 var hunk *diffHunk
58 oldNo, newNo := 0, 0
59
60 flushHunk := func() {
61 if cur != nil && hunk != nil {
62 cur.Hunks = append(cur.Hunks, *hunk)
63 hunk = nil
64 }
65 }
66 flushFile := func() {
67 flushHunk()
68 if cur != nil {
69 files = append(files, *cur)
70 cur = nil
71 }
72 }
73
74 for _, line := range strings.Split(patch, "\n") {
75 switch {
76 case strings.HasPrefix(line, "diff --git "):
77 flushFile()
78 cur = &diffFile{}
79 case cur == nil:
80 continue
81 case strings.HasPrefix(line, "--- "):
82 cur.OldPath = diffPath(strings.TrimPrefix(line, "--- "))
83 case strings.HasPrefix(line, "+++ "):
84 cur.NewPath = diffPath(strings.TrimPrefix(line, "+++ "))
85 case strings.HasPrefix(line, "Binary files ") || strings.HasPrefix(line, "GIT binary patch"):
86 cur.Binary = true
87 case metaRE.MatchString(line):
88 // plumbing: shown nowhere
89 case hunkRE.MatchString(line):
90 flushHunk()
91 m := hunkRE.FindStringSubmatch(line)
92 oldNo, _ = strconv.Atoi(m[1])
93 newNo, _ = strconv.Atoi(m[2])
94 hunk = &diffHunk{Header: line}
95 case hunk != nil && strings.HasPrefix(line, "+"):
96 hunk.Lines = append(hunk.Lines, diffLine{Kind: "add", NewNo: newNo, Text: line[1:]})
97 cur.Added++
98 newNo++
99 case hunk != nil && strings.HasPrefix(line, "-"):
100 hunk.Lines = append(hunk.Lines, diffLine{Kind: "del", OldNo: oldNo, Text: line[1:]})
101 cur.Removed++
102 oldNo++
103 case hunk != nil && strings.HasPrefix(line, " "):
104 hunk.Lines = append(hunk.Lines, diffLine{Kind: "ctx", OldNo: oldNo, NewNo: newNo, Text: line[1:]})
105 oldNo++
106 newNo++
107 case hunk != nil && strings.HasPrefix(line, `\`):
108 // "\ No newline at end of file" belongs to the previous line.
109 }
110 }
111 flushFile()
112 return files
113 }
114
115 // diffPath normalises a ---/+++ path: strips the a/ or b/ prefix and the tab
116 // git appends when a path contains spaces.
117 func diffPath(p string) string {
118 if i := strings.IndexByte(p, '\t'); i >= 0 {
119 p = p[:i]
120 }
121 p = strings.TrimSpace(p)
122 if m := quoteRE.FindStringSubmatch(p); m != nil {
123 if q, err := strconv.Unquote(`"` + m[1] + `"`); err == nil {
124 p = q
125 }
126 }
127 if p == "/dev/null" {
128 return ""
129 }
130 if len(p) > 2 && (strings.HasPrefix(p, "a/") || strings.HasPrefix(p, "b/")) {
131 return p[2:]
132 }
133 return p
134 }
135
136 // diffSummary is the "3 files changed, +12 −4" line under a heading.
137 func diffSummary(files []diffFile) (added, removed int) {
138 for _, f := range files {
139 added += f.Added
140 removed += f.Removed
141 }
142 return added, removed
143 }