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