A vibe coded tangled fork which supports pijul.

patchutil,types: implement DiffRenderer interface for all diffs

types.NiceDiff and patchutil.Interdiff now implement the new interface.
this allows us to remove the differing rendering logic necessary to
present each kind of diff.

+170 -54
+2 -2
appview/pulls/opengraph.go
··· 18 18 "tangled.org/core/types" 19 19 ) 20 20 21 - func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffStat, filesChanged int) (*ogcard.Card, error) { 21 + func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffFileStat, filesChanged int) (*ogcard.Card, error) { 22 22 width, height := ogcard.DefaultSize() 23 23 mainCard, err := ogcard.NewCard(width, height) 24 24 if err != nil { ··· 284 284 commentCount := len(comments) 285 285 286 286 // Calculate diff stats from latest submission using patchutil 287 - var diffStats types.DiffStat 287 + var diffStats types.DiffFileStat 288 288 filesChanged := 0 289 289 if len(pull.Submissions) > 0 { 290 290 latestSubmission := pull.Submissions[len(pull.Submissions)-1]
+3 -8
knotserver/git/diff.go
··· 64 64 65 65 for _, tf := range d.TextFragments { 66 66 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 67 - for _, l := range tf.Lines { 68 - switch l.Op { 69 - case gitdiff.OpAdd: 70 - nd.Stat.Insertions += 1 71 - case gitdiff.OpDelete: 72 - nd.Stat.Deletions += 1 73 - } 74 - } 67 + nd.Stat.Insertions += tf.LinesAdded 68 + nd.Stat.Deletions += tf.LinesDeleted 75 69 } 76 70 77 71 nd.Diff = append(nd.Diff, ndiff) 78 72 } 79 73 74 + nd.Stat.FilesChanged += len(diffs) 80 75 nd.Commit.FromGoGitCommit(c) 81 76 82 77 return &nd, nil
+66 -10
patchutil/interdiff.go
··· 5 5 "strings" 6 6 7 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 + "tangled.org/core/appview/filetree" 8 9 "tangled.org/core/types" 9 10 ) 10 11 ··· 12 13 Files []*InterdiffFile 13 14 } 14 15 15 - func (i *InterdiffResult) AffectedFiles() []string { 16 - files := make([]string, len(i.Files)) 17 - for _, f := range i.Files { 18 - files = append(files, f.Name) 16 + func (i *InterdiffResult) Stats() types.DiffStat { 17 + var ins, del int64 18 + for _, s := range i.ChangedFiles() { 19 + stat := s.Stats() 20 + ins += stat.Insertions 21 + del += stat.Deletions 22 + } 23 + return types.DiffStat{ 24 + Insertions: ins, 25 + Deletions: del, 26 + FilesChanged: len(i.Files), 19 27 } 20 - return files 28 + } 29 + 30 + func (i *InterdiffResult) ChangedFiles() []types.DiffFileRenderer { 31 + drs := make([]types.DiffFileRenderer, len(i.Files)) 32 + for i, s := range i.Files { 33 + drs[i] = s 34 + } 35 + return drs 36 + } 37 + 38 + func (i *InterdiffResult) FileTree() *filetree.FileTreeNode { 39 + fs := make([]string, len(i.Files)) 40 + for i, s := range i.Files { 41 + fs[i] = s.Name 42 + } 43 + return filetree.FileTree(fs) 21 44 } 22 45 23 46 func (i *InterdiffResult) String() string { ··· 36 59 Status InterdiffFileStatus 37 60 } 38 61 39 - func (s *InterdiffFile) Split() *types.SplitDiff { 62 + func (s *InterdiffFile) Id() string { 63 + return s.Name 64 + } 65 + 66 + func (s *InterdiffFile) Split() types.SplitDiff { 40 67 fragments := make([]types.SplitFragment, len(s.TextFragments)) 41 68 42 69 for i, fragment := range s.TextFragments { ··· 49 76 } 50 77 } 51 78 52 - return &types.SplitDiff{ 79 + return types.SplitDiff{ 53 80 Name: s.Id(), 54 81 TextFragments: fragments, 55 82 } 56 83 } 57 84 58 - // used by html elements as a unique ID for hrefs 59 - func (s *InterdiffFile) Id() string { 60 - return s.Name 85 + func (s *InterdiffFile) CanRender() string { 86 + if s.Status.IsUnchanged() { 87 + return "This file has not been changed." 88 + } else if s.Status.IsRebased() { 89 + return "This patch was likely rebased, as context lines do not match." 90 + } else if s.Status.IsError() { 91 + return "Failed to calculate interdiff for this file." 92 + } else { 93 + return "" 94 + } 95 + } 96 + 97 + func (s *InterdiffFile) Names() types.DiffFileName { 98 + var n types.DiffFileName 99 + n.New = s.Name 100 + return n 101 + } 102 + 103 + func (s *InterdiffFile) Stats() types.DiffFileStat { 104 + var ins, del int64 105 + 106 + if s.File != nil { 107 + for _, f := range s.TextFragments { 108 + ins += f.LinesAdded 109 + del += f.LinesDeleted 110 + } 111 + } 112 + 113 + return types.DiffFileStat{ 114 + Insertions: ins, 115 + Deletions: del, 116 + } 61 117 } 62 118 63 119 func (s *InterdiffFile) String() string {
+9
patchutil/patchutil_test.go
··· 4 4 "errors" 5 5 "reflect" 6 6 "testing" 7 + 8 + "tangled.org/core/types" 7 9 ) 8 10 9 11 func TestIsPatchValid(t *testing.T) { ··· 323 325 }) 324 326 } 325 327 } 328 + 329 + func TestImplsInterfaces(t *testing.T) { 330 + id := &InterdiffResult{} 331 + _ = isDiffsRenderer(id) 332 + } 333 + 334 + func isDiffsRenderer[S types.DiffRenderer](S) bool { return true }
+78 -30
types/diff.go
··· 1 1 package types 2 2 3 3 import ( 4 + "net/url" 5 + 4 6 "github.com/bluekeyes/go-gitdiff/gitdiff" 7 + "tangled.org/core/appview/filetree" 5 8 ) 6 9 7 10 type DiffOpts struct { 8 11 Split bool `json:"split"` 9 12 } 10 13 11 - type TextFragment struct { 12 - Header string `json:"comment"` 13 - Lines []gitdiff.Line `json:"lines"` 14 + func (d DiffOpts) Encode() string { 15 + values := make(url.Values) 16 + if d.Split { 17 + values.Set("diff", "split") 18 + } else { 19 + values.Set("diff", "unified") 20 + } 21 + return values.Encode() 22 + } 23 + 24 + // A nicer git diff representation. 25 + type NiceDiff struct { 26 + Commit Commit `json:"commit"` 27 + Stat DiffStat `json:"stat"` 28 + Diff []Diff `json:"diff"` 14 29 } 15 30 16 31 type Diff struct { ··· 26 41 IsRename bool `json:"is_rename"` 27 42 } 28 43 29 - type DiffStat struct { 30 - Insertions int64 31 - Deletions int64 32 - } 33 - 34 - func (d *Diff) Stats() DiffStat { 35 - var stats DiffStat 44 + func (d Diff) Stats() DiffFileStat { 45 + var stats DiffFileStat 36 46 for _, f := range d.TextFragments { 37 47 stats.Insertions += f.LinesAdded 38 48 stats.Deletions += f.LinesDeleted ··· 40 50 return stats 41 51 } 42 52 43 - // A nicer git diff representation. 44 - type NiceDiff struct { 45 - Commit Commit `json:"commit"` 46 - Stat struct { 47 - FilesChanged int `json:"files_changed"` 48 - Insertions int `json:"insertions"` 49 - Deletions int `json:"deletions"` 50 - } `json:"stat"` 51 - Diff []Diff `json:"diff"` 53 + type DiffStat struct { 54 + Insertions int64 `json:"insertions"` 55 + Deletions int64 `json:"deletions"` 56 + FilesChanged int `json:"files_changed"` 57 + } 58 + 59 + type DiffFileStat struct { 60 + Insertions int64 61 + Deletions int64 52 62 } 53 63 54 64 type DiffTree struct { ··· 58 68 Diff []*gitdiff.File `json:"diff"` 59 69 } 60 70 61 - func (d *NiceDiff) ChangedFiles() []string { 62 - files := make([]string, len(d.Diff)) 71 + type DiffFileName struct { 72 + Old string 73 + New string 74 + } 63 75 64 - for i, f := range d.Diff { 65 - if f.IsDelete { 66 - files[i] = f.Name.Old 76 + func (d NiceDiff) ChangedFiles() []DiffFileRenderer { 77 + drs := make([]DiffFileRenderer, len(d.Diff)) 78 + for i, s := range d.Diff { 79 + drs[i] = s 80 + } 81 + return drs 82 + } 83 + 84 + func (d NiceDiff) FileTree() *filetree.FileTreeNode { 85 + fs := make([]string, len(d.Diff)) 86 + for i, s := range d.Diff { 87 + n := s.Names() 88 + if n.New == "" { 89 + fs[i] = n.Old 67 90 } else { 68 - files[i] = f.Name.New 91 + fs[i] = n.New 69 92 } 70 93 } 94 + return filetree.FileTree(fs) 95 + } 71 96 72 - return files 97 + func (d NiceDiff) Stats() DiffStat { 98 + return d.Stat 73 99 } 74 100 75 - // used by html elements as a unique ID for hrefs 76 - func (d *Diff) Id() string { 101 + func (d Diff) Id() string { 77 102 if d.IsDelete { 78 103 return d.Name.Old 79 104 } 80 105 return d.Name.New 81 106 } 82 107 83 - func (d *Diff) Split() *SplitDiff { 108 + func (d Diff) Names() DiffFileName { 109 + var n DiffFileName 110 + if d.IsDelete { 111 + n.Old = d.Name.Old 112 + return n 113 + } else if d.IsCopy || d.IsRename { 114 + n.Old = d.Name.Old 115 + n.New = d.Name.New 116 + return n 117 + } else { 118 + n.New = d.Name.New 119 + return n 120 + } 121 + } 122 + 123 + func (d Diff) CanRender() string { 124 + if d.IsBinary { 125 + return "This is a binary file and will not be displayed." 126 + } 127 + 128 + return "" 129 + } 130 + 131 + func (d Diff) Split() SplitDiff { 84 132 fragments := make([]SplitFragment, len(d.TextFragments)) 85 133 for i, fragment := range d.TextFragments { 86 134 leftLines, rightLines := SeparateLines(&fragment) ··· 91 139 } 92 140 } 93 141 94 - return &SplitDiff{ 142 + return SplitDiff{ 95 143 Name: d.Id(), 96 144 TextFragments: fragments, 97 145 }
+11 -2
types/diff_test.go
··· 1 1 package types 2 2 3 - import "testing" 3 + import ( 4 + "testing" 5 + ) 4 6 5 7 func TestDiffId(t *testing.T) { 6 8 tests := []struct { ··· 105 107 } 106 108 107 109 for i, diff := range nd.Diff { 108 - if changedFiles[i] != diff.Id() { 110 + if changedFiles[i].Id() != diff.Id() { 109 111 t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 110 112 } 111 113 } 112 114 } 115 + 116 + func TestImplsInterfaces(t *testing.T) { 117 + nd := NiceDiff{} 118 + _ = isDiffsRenderer(nd) 119 + } 120 + 121 + func isDiffsRenderer[S DiffRenderer](S) bool { return true }
+1 -2
types/split.go
··· 22 22 TextFragments []SplitFragment `json:"fragments"` 23 23 } 24 24 25 - // used by html elements as a unique ID for hrefs 26 - func (d *SplitDiff) Id() string { 25 + func (d SplitDiff) Id() string { 27 26 return d.Name 28 27 } 29 28