Monorepo for Tangled tangled.org

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.

oppi.li aa17e806 f7d60daa

verified
+170 -54
+2 -2
appview/pulls/opengraph.go
··· 18 "tangled.org/core/types" 19 ) 20 21 - func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffStat, filesChanged int) (*ogcard.Card, error) { 22 width, height := ogcard.DefaultSize() 23 mainCard, err := ogcard.NewCard(width, height) 24 if err != nil { ··· 284 commentCount := len(comments) 285 286 // Calculate diff stats from latest submission using patchutil 287 - var diffStats types.DiffStat 288 filesChanged := 0 289 if len(pull.Submissions) > 0 { 290 latestSubmission := pull.Submissions[len(pull.Submissions)-1]
··· 18 "tangled.org/core/types" 19 ) 20 21 + func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffFileStat, filesChanged int) (*ogcard.Card, error) { 22 width, height := ogcard.DefaultSize() 23 mainCard, err := ogcard.NewCard(width, height) 24 if err != nil { ··· 284 commentCount := len(comments) 285 286 // Calculate diff stats from latest submission using patchutil 287 + var diffStats types.DiffFileStat 288 filesChanged := 0 289 if len(pull.Submissions) > 0 { 290 latestSubmission := pull.Submissions[len(pull.Submissions)-1]
+3 -8
knotserver/git/diff.go
··· 64 65 for _, tf := range d.TextFragments { 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 - } 75 } 76 77 nd.Diff = append(nd.Diff, ndiff) 78 } 79 80 nd.Commit.FromGoGitCommit(c) 81 82 return &nd, nil
··· 64 65 for _, tf := range d.TextFragments { 66 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 67 + nd.Stat.Insertions += tf.LinesAdded 68 + nd.Stat.Deletions += tf.LinesDeleted 69 } 70 71 nd.Diff = append(nd.Diff, ndiff) 72 } 73 74 + nd.Stat.FilesChanged += len(diffs) 75 nd.Commit.FromGoGitCommit(c) 76 77 return &nd, nil
+66 -10
patchutil/interdiff.go
··· 5 "strings" 6 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 "tangled.org/core/types" 9 ) 10 ··· 12 Files []*InterdiffFile 13 } 14 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) 19 } 20 - return files 21 } 22 23 func (i *InterdiffResult) String() string { ··· 36 Status InterdiffFileStatus 37 } 38 39 - func (s *InterdiffFile) Split() *types.SplitDiff { 40 fragments := make([]types.SplitFragment, len(s.TextFragments)) 41 42 for i, fragment := range s.TextFragments { ··· 49 } 50 } 51 52 - return &types.SplitDiff{ 53 Name: s.Id(), 54 TextFragments: fragments, 55 } 56 } 57 58 - // used by html elements as a unique ID for hrefs 59 - func (s *InterdiffFile) Id() string { 60 - return s.Name 61 } 62 63 func (s *InterdiffFile) String() string {
··· 5 "strings" 6 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 + "tangled.org/core/appview/filetree" 9 "tangled.org/core/types" 10 ) 11 ··· 13 Files []*InterdiffFile 14 } 15 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), 27 } 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) 44 } 45 46 func (i *InterdiffResult) String() string { ··· 59 Status InterdiffFileStatus 60 } 61 62 + func (s *InterdiffFile) Id() string { 63 + return s.Name 64 + } 65 + 66 + func (s *InterdiffFile) Split() types.SplitDiff { 67 fragments := make([]types.SplitFragment, len(s.TextFragments)) 68 69 for i, fragment := range s.TextFragments { ··· 76 } 77 } 78 79 + return types.SplitDiff{ 80 Name: s.Id(), 81 TextFragments: fragments, 82 } 83 } 84 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 + } 117 } 118 119 func (s *InterdiffFile) String() string {
+9
patchutil/patchutil_test.go
··· 4 "errors" 5 "reflect" 6 "testing" 7 ) 8 9 func TestIsPatchValid(t *testing.T) { ··· 323 }) 324 } 325 }
··· 4 "errors" 5 "reflect" 6 "testing" 7 + 8 + "tangled.org/core/types" 9 ) 10 11 func TestIsPatchValid(t *testing.T) { ··· 325 }) 326 } 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 package types 2 3 import ( 4 "github.com/bluekeyes/go-gitdiff/gitdiff" 5 ) 6 7 type DiffOpts struct { 8 Split bool `json:"split"` 9 } 10 11 - type TextFragment struct { 12 - Header string `json:"comment"` 13 - Lines []gitdiff.Line `json:"lines"` 14 } 15 16 type Diff struct { ··· 26 IsRename bool `json:"is_rename"` 27 } 28 29 - type DiffStat struct { 30 - Insertions int64 31 - Deletions int64 32 - } 33 - 34 - func (d *Diff) Stats() DiffStat { 35 - var stats DiffStat 36 for _, f := range d.TextFragments { 37 stats.Insertions += f.LinesAdded 38 stats.Deletions += f.LinesDeleted ··· 40 return stats 41 } 42 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"` 52 } 53 54 type DiffTree struct { ··· 58 Diff []*gitdiff.File `json:"diff"` 59 } 60 61 - func (d *NiceDiff) ChangedFiles() []string { 62 - files := make([]string, len(d.Diff)) 63 64 - for i, f := range d.Diff { 65 - if f.IsDelete { 66 - files[i] = f.Name.Old 67 } else { 68 - files[i] = f.Name.New 69 } 70 } 71 72 - return files 73 } 74 75 - // used by html elements as a unique ID for hrefs 76 - func (d *Diff) Id() string { 77 if d.IsDelete { 78 return d.Name.Old 79 } 80 return d.Name.New 81 } 82 83 - func (d *Diff) Split() *SplitDiff { 84 fragments := make([]SplitFragment, len(d.TextFragments)) 85 for i, fragment := range d.TextFragments { 86 leftLines, rightLines := SeparateLines(&fragment) ··· 91 } 92 } 93 94 - return &SplitDiff{ 95 Name: d.Id(), 96 TextFragments: fragments, 97 }
··· 1 package types 2 3 import ( 4 + "net/url" 5 + 6 "github.com/bluekeyes/go-gitdiff/gitdiff" 7 + "tangled.org/core/appview/filetree" 8 ) 9 10 type DiffOpts struct { 11 Split bool `json:"split"` 12 } 13 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"` 29 } 30 31 type Diff struct { ··· 41 IsRename bool `json:"is_rename"` 42 } 43 44 + func (d Diff) Stats() DiffFileStat { 45 + var stats DiffFileStat 46 for _, f := range d.TextFragments { 47 stats.Insertions += f.LinesAdded 48 stats.Deletions += f.LinesDeleted ··· 50 return stats 51 } 52 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 62 } 63 64 type DiffTree struct { ··· 68 Diff []*gitdiff.File `json:"diff"` 69 } 70 71 + type DiffFileName struct { 72 + Old string 73 + New string 74 + } 75 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 90 } else { 91 + fs[i] = n.New 92 } 93 } 94 + return filetree.FileTree(fs) 95 + } 96 97 + func (d NiceDiff) Stats() DiffStat { 98 + return d.Stat 99 } 100 101 + func (d Diff) Id() string { 102 if d.IsDelete { 103 return d.Name.Old 104 } 105 return d.Name.New 106 } 107 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 { 132 fragments := make([]SplitFragment, len(d.TextFragments)) 133 for i, fragment := range d.TextFragments { 134 leftLines, rightLines := SeparateLines(&fragment) ··· 139 } 140 } 141 142 + return SplitDiff{ 143 Name: d.Id(), 144 TextFragments: fragments, 145 }
+11 -2
types/diff_test.go
··· 1 package types 2 3 - import "testing" 4 5 func TestDiffId(t *testing.T) { 6 tests := []struct { ··· 105 } 106 107 for i, diff := range nd.Diff { 108 - if changedFiles[i] != diff.Id() { 109 t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 110 } 111 } 112 }
··· 1 package types 2 3 + import ( 4 + "testing" 5 + ) 6 7 func TestDiffId(t *testing.T) { 8 tests := []struct { ··· 107 } 108 109 for i, diff := range nd.Diff { 110 + if changedFiles[i].Id() != diff.Id() { 111 t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 112 } 113 } 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 TextFragments []SplitFragment `json:"fragments"` 23 } 24 25 - // used by html elements as a unique ID for hrefs 26 - func (d *SplitDiff) Id() string { 27 return d.Name 28 } 29
··· 22 TextFragments []SplitFragment `json:"fragments"` 23 } 24 25 + func (d SplitDiff) Id() string { 26 return d.Name 27 } 28