Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381package pulls
import ( "context" "errors" "html/template" "io"
"github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/appview/pages" gitmirrorv1 "tangled.org/core/gitmirror/proto/gen")
type fileDiff struct { diff *gitmirrorv1.FileDiff baseLines []template.HTML headLines []template.HTML}
const ( numContextLines = 3 maxDistance = 4)
type linePair struct { lhs int // 0-based line number, -1 when empty rhs int // 0-based line number, -1 when empty}
type displayHunk struct { rows []diffRow}
type diffRow struct { lhs int // 0-based line number, -1 when empty rhs int // 0-based line number, -1 when empty changed bool}
func buildHunks(baseLines, headLines []template.HTML, hunks []*gitmirrorv1.Hunk) []displayHunk { var flat []linePair for _, h := range hunks { for _, lp := range h.Lines { flat = append(flat, toPair(lp)) } }
pairs, changed := alignFile(baseLines, headLines, hunks) merged := mergeAdjacent(linesToHunks(flat), pairs)
var out []displayHunk prevEnd := 0 for _, h := range merged { lo, hi := indexesForHunk(pairs, h, numContextLines) if lo < prevEnd { lo = prevEnd // don't re-emit rows shared with the previous hunk's slice } var dh displayHunk for i := lo; i < hi; i++ { dh.rows = append(dh.rows, diffRow{lhs: pairs[i].lhs, rhs: pairs[i].rhs, changed: changed[i]}) } out = append(out, dh) prevEnd = hi } return out}
// alignFile builds the whole-file aligned list: every displayed line as a pair,// plus a parallel `changed` flag for pairs that came from a gitmirror hunk.// Unchanged lines are a 1:1 bijection, so the two cursors advance together// across gaps.func alignFile(baseLines, headLines []template.HTML, hunks []*gitmirrorv1.Hunk) (pairs []linePair, changed []bool) { li, ri := 0, 0 emitContext := func(n int) { for k := range n { pairs = append(pairs, linePair{lhs: li + k, rhs: ri + k}) changed = append(changed, false) } li += n ri += n } for _, h := range hunks { lhsStart, _, ok := hunkStart(h, li, ri) if !ok { continue } emitContext(lhsStart - li) // unchanged gap before this change (== rhsStart-ri) for _, lp := range h.Lines { p := toPair(lp) if p.lhs >= 0 { li = p.lhs + 1 } if p.rhs >= 0 { ri = p.rhs + 1 } pairs = append(pairs, p) changed = append(changed, true) } } for li < len(baseLines) && ri < len(headLines) { pairs = append(pairs, linePair{lhs: li, rhs: ri}) changed = append(changed, false) li++ ri++ } return pairs, changed}
// indexesForHunk returns the [start,end) slice of the aligned pairs to display// for a hunk: the span from its smallest to largest novel line, expanded by n// context lines each side and clamped.func indexesForHunk(pairs, hunkLines []linePair, n int) (start, end int) { minLhs, minRhs, maxLhs, maxRhs := -1, -1, -1, -1 for _, lp := range hunkLines { if lp.lhs >= 0 { if minLhs < 0 { minLhs = lp.lhs } maxLhs = lp.lhs } if lp.rhs >= 0 { if minRhs < 0 { minRhs = lp.rhs } maxRhs = lp.rhs } } smallest, largest := linePair{minLhs, minRhs}, linePair{maxLhs, maxRhs}
start = 0 for i, p := range pairs { if eitherSideEqual(p, smallest) { start = i break } } end = len(pairs) for i := len(pairs) - 1; i >= 0; i-- { if eitherSideEqual(pairs[i], largest) { end = i + 1 break } }
start = max(0, start-n) end = min(len(pairs), end+n) return start, end}
// eitherSideEqual reports whether a and b share a present line number on the same side.func eitherSideEqual(a, b linePair) bool { if a.lhs >= 0 && a.lhs == b.lhs { return true } if a.rhs >= 0 && a.rhs == b.rhs { return true } return false}
func toPair(lp *gitmirrorv1.LinePair) linePair { p := linePair{lhs: -1, rhs: -1} if lp.Lhs != nil { p.lhs = int(*lp.Lhs) } if lp.Rhs != nil { p.rhs = int(*lp.Rhs) } return p}
// hunkStart returns the first changed line number on each side, deriving the// empty side from the cursors (unchanged lines advance both sides equally). ok// is false for an empty hunk.func hunkStart(h *gitmirrorv1.Hunk, li, ri int) (lhsStart, rhsStart int, ok bool) { lhsStart, rhsStart = -1, -1 for _, lp := range h.Lines { if lp.Lhs != nil && lhsStart < 0 { lhsStart = int(*lp.Lhs) } if lp.Rhs != nil && rhsStart < 0 { rhsStart = int(*lp.Rhs) } } switch { case lhsStart < 0 && rhsStart < 0: return 0, 0, false case lhsStart < 0: // pure insertion lhsStart = li + (rhsStart - ri) case rhsStart < 0: // pure deletion rhsStart = ri + (lhsStart - li) } return lhsStart, rhsStart, true}
// enforceIncreasing drops any line number that would go backwards, keeping each// side monotonically increasing.func enforceIncreasing(lines []linePair) []linePair { var out []linePair maxLhs, maxRhs := -1, -1 for _, lp := range lines { l, r := lp.lhs, lp.rhs if maxLhs < 0 { maxLhs = l } else if l >= 0 && l > maxLhs { maxLhs = l } else { l = -1 } if maxRhs < 0 { maxRhs = r } else if r >= 0 && r > maxRhs { maxRhs = r } else { r = -1 } if l >= 0 || r >= 0 { out = append(out, linePair{lhs: l, rhs: r}) } } return out}
// linesAreClose reports whether a line is within maxDistance of the last seen// line on either side.func linesAreClose(maxLhs, maxRhs int, lp linePair) bool { if maxLhs >= 0 && lp.lhs >= 0 && lp.lhs <= maxLhs+maxDistance { return true } if maxRhs >= 0 && lp.rhs >= 0 && lp.rhs <= maxRhs+maxDistance { return true } return false}
// linesToHunks splits changed line pairs into hunks by per-side proximity.func linesToHunks(flat []linePair) [][]linePair { var hunks [][]linePair var cur []linePair maxLhs, maxRhs := -1, -1 for _, lp := range enforceIncreasing(flat) { if len(cur) == 0 || linesAreClose(maxLhs, maxRhs, lp) { cur = append(cur, lp) } else { hunks = append(hunks, cur) cur = []linePair{lp} } if lp.lhs >= 0 { maxLhs = lp.lhs } if lp.rhs >= 0 { maxRhs = lp.rhs } } if len(cur) > 0 { hunks = append(hunks, cur) } return hunks}
// mergeAdjacent folds consecutive hunks whose context windows overlap in the// aligned pair list into one group. It pads by numContextLines+1 (one more than// the displayed context) so hunks separated only by shared context merge.func mergeAdjacent(hunks [][]linePair, pairs []linePair) [][]linePair { var merged [][]linePair prevHi := -1 for _, h := range hunks { lo, hi := indexesForHunk(pairs, h, numContextLines+1) if len(merged) > 0 && lo < prevHi { last := len(merged) - 1 merged[last] = append(merged[last], h...) if hi > prevHi { prevHi = hi } continue } merged = append(merged, h) prevHi = hi } return merged}
func buildSplitRows(h displayHunk, baseLines, headLines []template.HTML) []pages.DiffRow { rows := make([]pages.DiffRow, 0, len(h.rows)) for _, r := range h.rows { var row pages.DiffRow if !r.changed { row.Left = pages.DiffCell{Kind: "ctx", Num: r.lhs + 1, Content: lineAt(baseLines, r.lhs)} row.Right = pages.DiffCell{Kind: "ctx", Num: r.rhs + 1, Content: lineAt(headLines, r.rhs)} } else { if r.lhs >= 0 { row.Left = pages.DiffCell{Kind: "del", Num: r.lhs + 1, Content: lineAt(baseLines, r.lhs)} } else { row.Left = pages.DiffCell{Kind: "empty", Num: 0} } if r.rhs >= 0 { row.Right = pages.DiffCell{Kind: "add", Num: r.rhs + 1, Content: lineAt(headLines, r.rhs)} } else { row.Right = pages.DiffCell{Kind: "empty", Num: 0} } } rows = append(rows, row) } return rows}
func buildUnifiedLines(h displayHunk, baseLines, headLines []template.HTML) []pages.DiffLine { var out []pages.DiffLine for i := 0; i < len(h.rows); { r := h.rows[i] if !r.changed { if r.lhs >= 0 { out = append(out, pages.DiffLine{Op: " ", Old: r.lhs + 1, New: r.rhs + 1, Content: lineAt(baseLines, r.lhs)}) } i++ continue } j := i for j < len(h.rows) && h.rows[j].changed { j++ } for _, cr := range h.rows[i:j] { if cr.lhs >= 0 { out = append(out, pages.DiffLine{Op: "-", Old: cr.lhs + 1, New: 0, Content: lineAt(baseLines, cr.lhs)}) } } for _, cr := range h.rows[i:j] { if cr.rhs >= 0 { out = append(out, pages.DiffLine{Op: "+", Old: 0, New: cr.rhs + 1, Content: lineAt(headLines, cr.rhs)}) } } i = j } return out}
func lineAt(lines []template.HTML, n int) template.HTML { if n < 0 || n >= len(lines) { return "" } return lines[n]}
func (s *Pulls) getBlob(ctx context.Context, repo syntax.DID, oid string) ([]byte, error) { if isNullOid(oid) { return nil, nil } stream, err := s.gitmirror.GetBlob(ctx, &gitmirrorv1.GetBlobRequest{Repo: repo.String(), Oid: oid}) if err != nil { return nil, err } var buf []byte for { chunk, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { return nil, err } buf = append(buf, chunk.GetData()...) } return buf, nil}
func isBinaryOrSubmodule(fc *gitmirrorv1.FileContent) bool { return fc != nil && (fc.GetIsBinary() || fc.GetIsSubmodule())}
func isNullOid(oid string) bool { if oid == "" { return true } for _, c := range oid { if c != '0' { return false } } return true}