at default-knot 325 lines 6.5 kB view raw
1package patchutil 2 3import ( 4 "fmt" 5 "strings" 6 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 "tangled.org/core/appview/filetree" 9 "tangled.org/core/types" 10) 11 12type InterdiffResult struct { 13 Files []*InterdiffFile 14} 15 16func (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 30func (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 38func (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 46func (i *InterdiffResult) String() string { 47 var b strings.Builder 48 for _, f := range i.Files { 49 b.WriteString(f.String()) 50 b.WriteString("\n") 51 } 52 53 return b.String() 54} 55 56type InterdiffFile struct { 57 *gitdiff.File 58 Name string 59 Status InterdiffFileStatus 60} 61 62func (s *InterdiffFile) Id() string { 63 return s.Name 64} 65 66func (s *InterdiffFile) Split() types.SplitDiff { 67 fragments := make([]types.SplitFragment, len(s.TextFragments)) 68 69 for i, fragment := range s.TextFragments { 70 leftLines, rightLines := types.SeparateLines(fragment) 71 72 fragments[i] = types.SplitFragment{ 73 Header: fragment.Header(), 74 LeftLines: leftLines, 75 RightLines: rightLines, 76 } 77 } 78 79 return types.SplitDiff{ 80 Name: s.Id(), 81 TextFragments: fragments, 82 } 83} 84 85func (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 97func (s *InterdiffFile) Names() types.DiffFileName { 98 var n types.DiffFileName 99 n.New = s.Name 100 return n 101} 102 103func (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 119func (s *InterdiffFile) String() string { 120 var b strings.Builder 121 b.WriteString(s.Status.String()) 122 b.WriteString(" ") 123 124 if s.File != nil { 125 b.WriteString(bestName(s.File)) 126 b.WriteString("\n") 127 b.WriteString(s.File.String()) 128 } 129 130 return b.String() 131} 132 133type InterdiffFileStatus struct { 134 StatusKind StatusKind 135 Error error 136} 137 138func (s *InterdiffFileStatus) String() string { 139 kind := s.StatusKind.String() 140 if s.Error != nil { 141 return fmt.Sprintf("%s [%s]", kind, s.Error.Error()) 142 } else { 143 return kind 144 } 145} 146 147func (s *InterdiffFileStatus) IsOk() bool { 148 return s.StatusKind == StatusOk 149} 150 151func (s *InterdiffFileStatus) IsUnchanged() bool { 152 return s.StatusKind == StatusUnchanged 153} 154 155func (s *InterdiffFileStatus) IsOnlyInOne() bool { 156 return s.StatusKind == StatusOnlyInOne 157} 158 159func (s *InterdiffFileStatus) IsOnlyInTwo() bool { 160 return s.StatusKind == StatusOnlyInTwo 161} 162 163func (s *InterdiffFileStatus) IsRebased() bool { 164 return s.StatusKind == StatusRebased 165} 166 167func (s *InterdiffFileStatus) IsError() bool { 168 return s.StatusKind == StatusError 169} 170 171type StatusKind int 172 173func (k StatusKind) String() string { 174 switch k { 175 case StatusOnlyInOne: 176 return "only in one" 177 case StatusOnlyInTwo: 178 return "only in two" 179 case StatusUnchanged: 180 return "unchanged" 181 case StatusRebased: 182 return "rebased" 183 case StatusError: 184 return "error" 185 default: 186 return "changed" 187 } 188} 189 190const ( 191 StatusOk StatusKind = iota 192 StatusOnlyInOne 193 StatusOnlyInTwo 194 StatusUnchanged 195 StatusRebased 196 StatusError 197) 198 199func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile { 200 re1 := CreatePreImage(f1) 201 re2 := CreatePreImage(f2) 202 203 interdiffFile := InterdiffFile{ 204 Name: bestName(f1), 205 } 206 207 merged, err := re1.Merge(&re2) 208 if err != nil { 209 interdiffFile.Status = InterdiffFileStatus{ 210 StatusKind: StatusRebased, 211 Error: err, 212 } 213 return &interdiffFile 214 } 215 216 rev1, err := merged.Apply(f1) 217 if err != nil { 218 interdiffFile.Status = InterdiffFileStatus{ 219 StatusKind: StatusError, 220 Error: err, 221 } 222 return &interdiffFile 223 } 224 225 rev2, err := merged.Apply(f2) 226 if err != nil { 227 interdiffFile.Status = InterdiffFileStatus{ 228 StatusKind: StatusError, 229 Error: err, 230 } 231 return &interdiffFile 232 } 233 234 diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2)) 235 if err != nil { 236 interdiffFile.Status = InterdiffFileStatus{ 237 StatusKind: StatusError, 238 Error: err, 239 } 240 return &interdiffFile 241 } 242 243 parsed, _, err := gitdiff.Parse(strings.NewReader(diff)) 244 if err != nil { 245 interdiffFile.Status = InterdiffFileStatus{ 246 StatusKind: StatusError, 247 Error: err, 248 } 249 return &interdiffFile 250 } 251 252 if len(parsed) != 1 { 253 // files are identical? 254 interdiffFile.Status = InterdiffFileStatus{ 255 StatusKind: StatusUnchanged, 256 } 257 return &interdiffFile 258 } 259 260 if interdiffFile.Status.StatusKind == StatusOk { 261 interdiffFile.File = parsed[0] 262 } 263 264 return &interdiffFile 265} 266 267func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult { 268 fileToIdx1 := make(map[string]int) 269 fileToIdx2 := make(map[string]int) 270 visited := make(map[string]struct{}) 271 var result InterdiffResult 272 273 for idx, f := range patch1 { 274 fileToIdx1[bestName(f)] = idx 275 } 276 277 for idx, f := range patch2 { 278 fileToIdx2[bestName(f)] = idx 279 } 280 281 for _, f1 := range patch1 { 282 var interdiffFile *InterdiffFile 283 284 fileName := bestName(f1) 285 if idx, ok := fileToIdx2[fileName]; ok { 286 f2 := patch2[idx] 287 288 // we have f1 and f2, calculate interdiff 289 interdiffFile = interdiffFiles(f1, f2) 290 } else { 291 // only in patch 1, this change would have to be "inverted" to dissapear 292 // from patch 2, so we reverseDiff(f1) 293 reverseDiff(f1) 294 295 interdiffFile = &InterdiffFile{ 296 File: f1, 297 Name: fileName, 298 Status: InterdiffFileStatus{ 299 StatusKind: StatusOnlyInOne, 300 }, 301 } 302 } 303 304 result.Files = append(result.Files, interdiffFile) 305 visited[fileName] = struct{}{} 306 } 307 308 // for all files in patch2 that remain unvisited; we can just add them into the output 309 for _, f2 := range patch2 { 310 fileName := bestName(f2) 311 if _, ok := visited[fileName]; ok { 312 continue 313 } 314 315 result.Files = append(result.Files, &InterdiffFile{ 316 File: f2, 317 Name: fileName, 318 Status: InterdiffFileStatus{ 319 StatusKind: StatusOnlyInTwo, 320 }, 321 }) 322 } 323 324 return &result 325}