This repository has no description
1package pulls
2
3import (
4 "context"
5 "errors"
6 "html/template"
7 "io"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "tangled.org/core/appview/pages"
11 gitmirrorv1 "tangled.org/core/gitmirror/proto/gen"
12)
13
14type fileDiff struct {
15 diff *gitmirrorv1.FileDiff
16 baseLines []template.HTML
17 headLines []template.HTML
18}
19
20const (
21 numContextLines = 3
22 maxDistance = 4
23)
24
25type linePair struct {
26 lhs int // 0-based line number, -1 when empty
27 rhs int // 0-based line number, -1 when empty
28}
29
30type displayHunk struct {
31 rows []diffRow
32}
33
34type diffRow struct {
35 lhs int // 0-based line number, -1 when empty
36 rhs int // 0-based line number, -1 when empty
37 changed bool
38}
39
40func buildHunks(baseLines, headLines []template.HTML, hunks []*gitmirrorv1.Hunk) []displayHunk {
41 var flat []linePair
42 for _, h := range hunks {
43 for _, lp := range h.Lines {
44 flat = append(flat, toPair(lp))
45 }
46 }
47
48 pairs, changed := alignFile(baseLines, headLines, hunks)
49 merged := mergeAdjacent(linesToHunks(flat), pairs)
50
51 var out []displayHunk
52 prevEnd := 0
53 for _, h := range merged {
54 lo, hi := indexesForHunk(pairs, h, numContextLines)
55 if lo < prevEnd {
56 lo = prevEnd // don't re-emit rows shared with the previous hunk's slice
57 }
58 var dh displayHunk
59 for i := lo; i < hi; i++ {
60 dh.rows = append(dh.rows, diffRow{lhs: pairs[i].lhs, rhs: pairs[i].rhs, changed: changed[i]})
61 }
62 out = append(out, dh)
63 prevEnd = hi
64 }
65 return out
66}
67
68// alignFile builds the whole-file aligned list: every displayed line as a pair,
69// plus a parallel `changed` flag for pairs that came from a gitmirror hunk.
70// Unchanged lines are a 1:1 bijection, so the two cursors advance together
71// across gaps.
72func alignFile(baseLines, headLines []template.HTML, hunks []*gitmirrorv1.Hunk) (pairs []linePair, changed []bool) {
73 li, ri := 0, 0
74 emitContext := func(n int) {
75 for k := range n {
76 pairs = append(pairs, linePair{lhs: li + k, rhs: ri + k})
77 changed = append(changed, false)
78 }
79 li += n
80 ri += n
81 }
82 for _, h := range hunks {
83 lhsStart, _, ok := hunkStart(h, li, ri)
84 if !ok {
85 continue
86 }
87 emitContext(lhsStart - li) // unchanged gap before this change (== rhsStart-ri)
88 for _, lp := range h.Lines {
89 p := toPair(lp)
90 if p.lhs >= 0 {
91 li = p.lhs + 1
92 }
93 if p.rhs >= 0 {
94 ri = p.rhs + 1
95 }
96 pairs = append(pairs, p)
97 changed = append(changed, true)
98 }
99 }
100 for li < len(baseLines) && ri < len(headLines) {
101 pairs = append(pairs, linePair{lhs: li, rhs: ri})
102 changed = append(changed, false)
103 li++
104 ri++
105 }
106 return pairs, changed
107}
108
109// indexesForHunk returns the [start,end) slice of the aligned pairs to display
110// for a hunk: the span from its smallest to largest novel line, expanded by n
111// context lines each side and clamped.
112func indexesForHunk(pairs, hunkLines []linePair, n int) (start, end int) {
113 minLhs, minRhs, maxLhs, maxRhs := -1, -1, -1, -1
114 for _, lp := range hunkLines {
115 if lp.lhs >= 0 {
116 if minLhs < 0 {
117 minLhs = lp.lhs
118 }
119 maxLhs = lp.lhs
120 }
121 if lp.rhs >= 0 {
122 if minRhs < 0 {
123 minRhs = lp.rhs
124 }
125 maxRhs = lp.rhs
126 }
127 }
128 smallest, largest := linePair{minLhs, minRhs}, linePair{maxLhs, maxRhs}
129
130 start = 0
131 for i, p := range pairs {
132 if eitherSideEqual(p, smallest) {
133 start = i
134 break
135 }
136 }
137 end = len(pairs)
138 for i := len(pairs) - 1; i >= 0; i-- {
139 if eitherSideEqual(pairs[i], largest) {
140 end = i + 1
141 break
142 }
143 }
144
145 start = max(0, start-n)
146 end = min(len(pairs), end+n)
147 return start, end
148}
149
150// eitherSideEqual reports whether a and b share a present line number on the same side.
151func eitherSideEqual(a, b linePair) bool {
152 if a.lhs >= 0 && a.lhs == b.lhs {
153 return true
154 }
155 if a.rhs >= 0 && a.rhs == b.rhs {
156 return true
157 }
158 return false
159}
160
161func toPair(lp *gitmirrorv1.LinePair) linePair {
162 p := linePair{lhs: -1, rhs: -1}
163 if lp.Lhs != nil {
164 p.lhs = int(*lp.Lhs)
165 }
166 if lp.Rhs != nil {
167 p.rhs = int(*lp.Rhs)
168 }
169 return p
170}
171
172// hunkStart returns the first changed line number on each side, deriving the
173// empty side from the cursors (unchanged lines advance both sides equally). ok
174// is false for an empty hunk.
175func hunkStart(h *gitmirrorv1.Hunk, li, ri int) (lhsStart, rhsStart int, ok bool) {
176 lhsStart, rhsStart = -1, -1
177 for _, lp := range h.Lines {
178 if lp.Lhs != nil && lhsStart < 0 {
179 lhsStart = int(*lp.Lhs)
180 }
181 if lp.Rhs != nil && rhsStart < 0 {
182 rhsStart = int(*lp.Rhs)
183 }
184 }
185 switch {
186 case lhsStart < 0 && rhsStart < 0:
187 return 0, 0, false
188 case lhsStart < 0: // pure insertion
189 lhsStart = li + (rhsStart - ri)
190 case rhsStart < 0: // pure deletion
191 rhsStart = ri + (lhsStart - li)
192 }
193 return lhsStart, rhsStart, true
194}
195
196// enforceIncreasing drops any line number that would go backwards, keeping each
197// side monotonically increasing.
198func enforceIncreasing(lines []linePair) []linePair {
199 var out []linePair
200 maxLhs, maxRhs := -1, -1
201 for _, lp := range lines {
202 l, r := lp.lhs, lp.rhs
203 if maxLhs < 0 {
204 maxLhs = l
205 } else if l >= 0 && l > maxLhs {
206 maxLhs = l
207 } else {
208 l = -1
209 }
210 if maxRhs < 0 {
211 maxRhs = r
212 } else if r >= 0 && r > maxRhs {
213 maxRhs = r
214 } else {
215 r = -1
216 }
217 if l >= 0 || r >= 0 {
218 out = append(out, linePair{lhs: l, rhs: r})
219 }
220 }
221 return out
222}
223
224// linesAreClose reports whether a line is within maxDistance of the last seen
225// line on either side.
226func linesAreClose(maxLhs, maxRhs int, lp linePair) bool {
227 if maxLhs >= 0 && lp.lhs >= 0 && lp.lhs <= maxLhs+maxDistance {
228 return true
229 }
230 if maxRhs >= 0 && lp.rhs >= 0 && lp.rhs <= maxRhs+maxDistance {
231 return true
232 }
233 return false
234}
235
236// linesToHunks splits changed line pairs into hunks by per-side proximity.
237func linesToHunks(flat []linePair) [][]linePair {
238 var hunks [][]linePair
239 var cur []linePair
240 maxLhs, maxRhs := -1, -1
241 for _, lp := range enforceIncreasing(flat) {
242 if len(cur) == 0 || linesAreClose(maxLhs, maxRhs, lp) {
243 cur = append(cur, lp)
244 } else {
245 hunks = append(hunks, cur)
246 cur = []linePair{lp}
247 }
248 if lp.lhs >= 0 {
249 maxLhs = lp.lhs
250 }
251 if lp.rhs >= 0 {
252 maxRhs = lp.rhs
253 }
254 }
255 if len(cur) > 0 {
256 hunks = append(hunks, cur)
257 }
258 return hunks
259}
260
261// mergeAdjacent folds consecutive hunks whose context windows overlap in the
262// aligned pair list into one group. It pads by numContextLines+1 (one more than
263// the displayed context) so hunks separated only by shared context merge.
264func mergeAdjacent(hunks [][]linePair, pairs []linePair) [][]linePair {
265 var merged [][]linePair
266 prevHi := -1
267 for _, h := range hunks {
268 lo, hi := indexesForHunk(pairs, h, numContextLines+1)
269 if len(merged) > 0 && lo < prevHi {
270 last := len(merged) - 1
271 merged[last] = append(merged[last], h...)
272 if hi > prevHi {
273 prevHi = hi
274 }
275 continue
276 }
277 merged = append(merged, h)
278 prevHi = hi
279 }
280 return merged
281}
282
283func buildSplitRows(h displayHunk, baseLines, headLines []template.HTML) []pages.DiffRow {
284 rows := make([]pages.DiffRow, 0, len(h.rows))
285 for _, r := range h.rows {
286 var row pages.DiffRow
287 if !r.changed {
288 row.Left = pages.DiffCell{Kind: "ctx", Num: r.lhs + 1, Content: lineAt(baseLines, r.lhs)}
289 row.Right = pages.DiffCell{Kind: "ctx", Num: r.rhs + 1, Content: lineAt(headLines, r.rhs)}
290 } else {
291 if r.lhs >= 0 {
292 row.Left = pages.DiffCell{Kind: "del", Num: r.lhs + 1, Content: lineAt(baseLines, r.lhs)}
293 } else {
294 row.Left = pages.DiffCell{Kind: "empty", Num: 0}
295 }
296 if r.rhs >= 0 {
297 row.Right = pages.DiffCell{Kind: "add", Num: r.rhs + 1, Content: lineAt(headLines, r.rhs)}
298 } else {
299 row.Right = pages.DiffCell{Kind: "empty", Num: 0}
300 }
301 }
302 rows = append(rows, row)
303 }
304 return rows
305}
306
307func buildUnifiedLines(h displayHunk, baseLines, headLines []template.HTML) []pages.DiffLine {
308 var out []pages.DiffLine
309 for i := 0; i < len(h.rows); {
310 r := h.rows[i]
311 if !r.changed {
312 if r.lhs >= 0 {
313 out = append(out, pages.DiffLine{Op: " ", Old: r.lhs + 1, New: r.rhs + 1, Content: lineAt(baseLines, r.lhs)})
314 }
315 i++
316 continue
317 }
318 j := i
319 for j < len(h.rows) && h.rows[j].changed {
320 j++
321 }
322 for _, cr := range h.rows[i:j] {
323 if cr.lhs >= 0 {
324 out = append(out, pages.DiffLine{Op: "-", Old: cr.lhs + 1, New: 0, Content: lineAt(baseLines, cr.lhs)})
325 }
326 }
327 for _, cr := range h.rows[i:j] {
328 if cr.rhs >= 0 {
329 out = append(out, pages.DiffLine{Op: "+", Old: 0, New: cr.rhs + 1, Content: lineAt(headLines, cr.rhs)})
330 }
331 }
332 i = j
333 }
334 return out
335}
336
337func lineAt(lines []template.HTML, n int) template.HTML {
338 if n < 0 || n >= len(lines) {
339 return ""
340 }
341 return lines[n]
342}
343
344func (s *Pulls) getBlob(ctx context.Context, repo syntax.DID, oid string) ([]byte, error) {
345 if isNullOid(oid) {
346 return nil, nil
347 }
348 stream, err := s.gitmirror.GetBlob(ctx, &gitmirrorv1.GetBlobRequest{Repo: repo.String(), Oid: oid})
349 if err != nil {
350 return nil, err
351 }
352 var buf []byte
353 for {
354 chunk, err := stream.Recv()
355 if errors.Is(err, io.EOF) {
356 break
357 }
358 if err != nil {
359 return nil, err
360 }
361 buf = append(buf, chunk.GetData()...)
362 }
363 return buf, nil
364}
365
366func isBinaryOrSubmodule(fc *gitmirrorv1.FileContent) bool {
367 return fc != nil && (fc.GetIsBinary() || fc.GetIsSubmodule())
368}
369
370func isNullOid(oid string) bool {
371 if oid == "" {
372 return true
373 }
374 for _, c := range oid {
375 if c != '0' {
376 return false
377 }
378 }
379 return true
380}