forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package interdiff 2 3import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "strings" 9 10 "github.com/bluekeyes/go-gitdiff/gitdiff" 11) 12 13type ReconstructedLine struct { 14 LineNumber int64 15 Content string 16 IsUnknown bool 17} 18 19func NewLineAt(lineNumber int64, content string) ReconstructedLine { 20 return ReconstructedLine{ 21 LineNumber: lineNumber, 22 Content: content, 23 IsUnknown: false, 24 } 25} 26 27type ReconstructedFile struct { 28 File string 29 Data []*ReconstructedLine 30} 31 32func (r *ReconstructedFile) String() string { 33 var i, j int64 34 var b strings.Builder 35 for { 36 i += 1 37 38 if int(j) >= (len(r.Data)) { 39 break 40 } 41 42 if r.Data[j].LineNumber == i { 43 // b.WriteString(fmt.Sprintf("%d:", r.Data[j].LineNumber)) 44 b.WriteString(r.Data[j].Content) 45 j += 1 46 } else { 47 //b.WriteString(fmt.Sprintf("%d:\n", i)) 48 b.WriteString("\n") 49 } 50 } 51 52 return b.String() 53} 54 55func (r *ReconstructedFile) AddLine(line *ReconstructedLine) { 56 r.Data = append(r.Data, line) 57} 58 59func bestName(file *gitdiff.File) string { 60 if file.IsDelete { 61 return file.OldName 62 } else { 63 return file.NewName 64 } 65} 66 67// in-place reverse of a diff 68func reverseDiff(file *gitdiff.File) { 69 file.OldName, file.NewName = file.NewName, file.OldName 70 file.OldMode, file.NewMode = file.NewMode, file.OldMode 71 file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment 72 73 for _, fragment := range file.TextFragments { 74 // swap postions 75 fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition 76 fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines 77 fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded 78 79 for i := range fragment.Lines { 80 switch fragment.Lines[i].Op { 81 case gitdiff.OpAdd: 82 fragment.Lines[i].Op = gitdiff.OpDelete 83 case gitdiff.OpDelete: 84 fragment.Lines[i].Op = gitdiff.OpAdd 85 default: 86 // do nothing 87 } 88 } 89 } 90} 91 92// rebuild the original file from a patch 93func CreateOriginal(file *gitdiff.File) ReconstructedFile { 94 rf := ReconstructedFile{ 95 File: bestName(file), 96 } 97 98 for _, fragment := range file.TextFragments { 99 position := fragment.OldPosition 100 for _, line := range fragment.Lines { 101 switch line.Op { 102 case gitdiff.OpContext: 103 rl := NewLineAt(position, line.Line) 104 rf.Data = append(rf.Data, &rl) 105 position += 1 106 case gitdiff.OpDelete: 107 rl := NewLineAt(position, line.Line) 108 rf.Data = append(rf.Data, &rl) 109 position += 1 110 case gitdiff.OpAdd: 111 // do nothing here 112 } 113 } 114 } 115 116 return rf 117} 118 119type MergeError struct { 120 msg string 121 mismatchingLine int64 122} 123 124func (m MergeError) Error() string { 125 return fmt.Sprintf("%s: %v", m.msg, m.mismatchingLine) 126} 127 128// best effort merging of two reconstructed files 129func (this *ReconstructedFile) Merge(other *ReconstructedFile) (*ReconstructedFile, error) { 130 mergedFile := ReconstructedFile{} 131 132 var i, j int64 133 134 for int(i) < len(this.Data) || int(j) < len(other.Data) { 135 if int(i) >= len(this.Data) { 136 // first file is done; the rest of the lines from file 2 can go in 137 mergedFile.AddLine(other.Data[j]) 138 j++ 139 continue 140 } 141 142 if int(j) >= len(other.Data) { 143 // first file is done; the rest of the lines from file 2 can go in 144 mergedFile.AddLine(this.Data[i]) 145 i++ 146 continue 147 } 148 149 line1 := this.Data[i] 150 line2 := other.Data[j] 151 152 if line1.LineNumber == line2.LineNumber { 153 if line1.Content != line2.Content { 154 return nil, MergeError{ 155 msg: "mismatching lines, this patch might have undergone rebase", 156 mismatchingLine: line1.LineNumber, 157 } 158 } else { 159 mergedFile.AddLine(line1) 160 } 161 i++ 162 j++ 163 } else if line1.LineNumber < line2.LineNumber { 164 mergedFile.AddLine(line1) 165 i++ 166 } else { 167 mergedFile.AddLine(line2) 168 j++ 169 } 170 } 171 172 return &mergedFile, nil 173} 174 175func (r *ReconstructedFile) Apply(patch *gitdiff.File) (string, error) { 176 original := r.String() 177 var buffer bytes.Buffer 178 reader := strings.NewReader(original) 179 180 err := gitdiff.Apply(&buffer, reader, patch) 181 if err != nil { 182 return "", err 183 } 184 185 return buffer.String(), nil 186} 187 188func Unified(oldText, oldFile, newText, newFile string) (string, error) { 189 oldTemp, err := os.CreateTemp("", "old_*") 190 if err != nil { 191 return "", fmt.Errorf("failed to create temp file for oldText: %w", err) 192 } 193 defer os.Remove(oldTemp.Name()) 194 if _, err := oldTemp.WriteString(oldText); err != nil { 195 return "", fmt.Errorf("failed to write to old temp file: %w", err) 196 } 197 oldTemp.Close() 198 199 newTemp, err := os.CreateTemp("", "new_*") 200 if err != nil { 201 return "", fmt.Errorf("failed to create temp file for newText: %w", err) 202 } 203 defer os.Remove(newTemp.Name()) 204 if _, err := newTemp.WriteString(newText); err != nil { 205 return "", fmt.Errorf("failed to write to new temp file: %w", err) 206 } 207 newTemp.Close() 208 209 cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name()) 210 output, err := cmd.CombinedOutput() 211 212 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { 213 return string(output), nil 214 } 215 if err != nil { 216 return "", fmt.Errorf("diff command failed: %w", err) 217 } 218 219 return string(output), nil 220} 221 222type InterdiffResult struct { 223 Files []*InterdiffFile 224} 225 226func (i *InterdiffResult) String() string { 227 var b strings.Builder 228 for _, f := range i.Files { 229 b.WriteString(f.String()) 230 b.WriteString("\n") 231 } 232 233 return b.String() 234} 235 236type InterdiffFile struct { 237 *gitdiff.File 238 Name string 239 Status InterdiffFileStatus 240} 241 242func (s *InterdiffFile) String() string { 243 var b strings.Builder 244 b.WriteString(s.Status.String()) 245 b.WriteString(" ") 246 247 if s.File != nil { 248 b.WriteString(bestName(s.File)) 249 b.WriteString("\n") 250 b.WriteString(s.File.String()) 251 } 252 253 return b.String() 254} 255 256type InterdiffFileStatus struct { 257 StatusKind StatusKind 258 Error error 259} 260 261func (s *InterdiffFileStatus) String() string { 262 kind := s.StatusKind.String() 263 if s.Error != nil { 264 return fmt.Sprintf("%s [%s]", kind, s.Error.Error()) 265 } else { 266 return kind 267 } 268} 269 270func (s *InterdiffFileStatus) IsOk() bool { 271 return s.StatusKind == StatusOk 272} 273 274func (s *InterdiffFileStatus) IsUnchanged() bool { 275 return s.StatusKind == StatusUnchanged 276} 277 278func (s *InterdiffFileStatus) IsOnlyInOne() bool { 279 return s.StatusKind == StatusOnlyInOne 280} 281 282func (s *InterdiffFileStatus) IsOnlyInTwo() bool { 283 return s.StatusKind == StatusOnlyInTwo 284} 285 286func (s *InterdiffFileStatus) IsRebased() bool { 287 return s.StatusKind == StatusRebased 288} 289 290func (s *InterdiffFileStatus) IsError() bool { 291 return s.StatusKind == StatusError 292} 293 294type StatusKind int 295 296func (k StatusKind) String() string { 297 switch k { 298 case StatusOnlyInOne: 299 return "only in one" 300 case StatusOnlyInTwo: 301 return "only in two" 302 case StatusUnchanged: 303 return "unchanged" 304 case StatusRebased: 305 return "rebased" 306 case StatusError: 307 return "error" 308 default: 309 return "changed" 310 } 311} 312 313const ( 314 StatusOk StatusKind = iota 315 StatusOnlyInOne 316 StatusOnlyInTwo 317 StatusUnchanged 318 StatusRebased 319 StatusError 320) 321 322func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile { 323 re1 := CreateOriginal(f1) 324 re2 := CreateOriginal(f2) 325 326 interdiffFile := InterdiffFile{ 327 Name: bestName(f1), 328 } 329 330 merged, err := re1.Merge(&re2) 331 if err != nil { 332 interdiffFile.Status = InterdiffFileStatus{ 333 StatusKind: StatusRebased, 334 Error: err, 335 } 336 return &interdiffFile 337 } 338 339 rev1, err := merged.Apply(f1) 340 if err != nil { 341 interdiffFile.Status = InterdiffFileStatus{ 342 StatusKind: StatusError, 343 Error: err, 344 } 345 return &interdiffFile 346 } 347 348 rev2, err := merged.Apply(f2) 349 if err != nil { 350 interdiffFile.Status = InterdiffFileStatus{ 351 StatusKind: StatusError, 352 Error: err, 353 } 354 return &interdiffFile 355 } 356 357 diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2)) 358 if err != nil { 359 interdiffFile.Status = InterdiffFileStatus{ 360 StatusKind: StatusError, 361 Error: err, 362 } 363 return &interdiffFile 364 } 365 366 parsed, _, err := gitdiff.Parse(strings.NewReader(diff)) 367 if err != nil { 368 interdiffFile.Status = InterdiffFileStatus{ 369 StatusKind: StatusError, 370 Error: err, 371 } 372 return &interdiffFile 373 } 374 375 if len(parsed) != 1 { 376 // files are identical? 377 interdiffFile.Status = InterdiffFileStatus{ 378 StatusKind: StatusUnchanged, 379 } 380 return &interdiffFile 381 } 382 383 if interdiffFile.Status.StatusKind == StatusOk { 384 interdiffFile.File = parsed[0] 385 } 386 387 return &interdiffFile 388} 389 390func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult { 391 fileToIdx1 := make(map[string]int) 392 fileToIdx2 := make(map[string]int) 393 visited := make(map[string]struct{}) 394 var result InterdiffResult 395 396 for idx, f := range patch1 { 397 fileToIdx1[bestName(f)] = idx 398 } 399 400 for idx, f := range patch2 { 401 fileToIdx2[bestName(f)] = idx 402 } 403 404 for _, f1 := range patch1 { 405 var interdiffFile *InterdiffFile 406 407 fileName := bestName(f1) 408 if idx, ok := fileToIdx2[fileName]; ok { 409 f2 := patch2[idx] 410 411 // we have f1 and f2, calculate interdiff 412 interdiffFile = interdiffFiles(f1, f2) 413 } else { 414 // only in patch 1, this change would have to be "inverted" to dissapear 415 // from patch 2, so we reverseDiff(f1) 416 reverseDiff(f1) 417 418 interdiffFile = &InterdiffFile{ 419 File: f1, 420 Name: fileName, 421 Status: InterdiffFileStatus{ 422 StatusKind: StatusOnlyInOne, 423 }, 424 } 425 } 426 427 result.Files = append(result.Files, interdiffFile) 428 visited[fileName] = struct{}{} 429 } 430 431 // for all files in patch2 that remain unvisited; we can just add them into the output 432 for _, f2 := range patch2 { 433 fileName := bestName(f2) 434 if _, ok := visited[fileName]; ok { 435 continue 436 } 437 438 result.Files = append(result.Files, &InterdiffFile{ 439 File: f2, 440 Name: fileName, 441 Status: InterdiffFileStatus{ 442 StatusKind: StatusOnlyInTwo, 443 }, 444 }) 445 } 446 447 return &result 448}