···11-package interdiff22-33-import (44- "bytes"55- "fmt"66- "os"77- "os/exec"88- "strings"99-1010- "github.com/bluekeyes/go-gitdiff/gitdiff"1111-)1212-1313-type ReconstructedLine struct {1414- LineNumber int641515- Content string1616- IsUnknown bool1717-}1818-1919-func NewLineAt(lineNumber int64, content string) ReconstructedLine {2020- return ReconstructedLine{2121- LineNumber: lineNumber,2222- Content: content,2323- IsUnknown: false,2424- }2525-}2626-2727-type ReconstructedFile struct {2828- File string2929- Data []*ReconstructedLine3030-}3131-3232-func (r *ReconstructedFile) String() string {3333- var i, j int643434- var b strings.Builder3535- for {3636- i += 13737-3838- if int(j) >= (len(r.Data)) {3939- break4040- }4141-4242- if r.Data[j].LineNumber == i {4343- // b.WriteString(fmt.Sprintf("%d:", r.Data[j].LineNumber))4444- b.WriteString(r.Data[j].Content)4545- j += 14646- } else {4747- //b.WriteString(fmt.Sprintf("%d:\n", i))4848- b.WriteString("\n")4949- }5050- }5151-5252- return b.String()5353-}5454-5555-func (r *ReconstructedFile) AddLine(line *ReconstructedLine) {5656- r.Data = append(r.Data, line)5757-}5858-5959-func bestName(file *gitdiff.File) string {6060- if file.IsDelete {6161- return file.OldName6262- } else {6363- return file.NewName6464- }6565-}6666-6767-// in-place reverse of a diff6868-func reverseDiff(file *gitdiff.File) {6969- file.OldName, file.NewName = file.NewName, file.OldName7070- file.OldMode, file.NewMode = file.NewMode, file.OldMode7171- file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment7272-7373- for _, fragment := range file.TextFragments {7474- // swap postions7575- fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition7676- fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines7777- fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded7878-7979- for i := range fragment.Lines {8080- switch fragment.Lines[i].Op {8181- case gitdiff.OpAdd:8282- fragment.Lines[i].Op = gitdiff.OpDelete8383- case gitdiff.OpDelete:8484- fragment.Lines[i].Op = gitdiff.OpAdd8585- default:8686- // do nothing8787- }8888- }8989- }9090-}9191-9292-// rebuild the original file from a patch9393-func CreateOriginal(file *gitdiff.File) ReconstructedFile {9494- rf := ReconstructedFile{9595- File: bestName(file),9696- }9797-9898- for _, fragment := range file.TextFragments {9999- position := fragment.OldPosition100100- for _, line := range fragment.Lines {101101- switch line.Op {102102- case gitdiff.OpContext:103103- rl := NewLineAt(position, line.Line)104104- rf.Data = append(rf.Data, &rl)105105- position += 1106106- case gitdiff.OpDelete:107107- rl := NewLineAt(position, line.Line)108108- rf.Data = append(rf.Data, &rl)109109- position += 1110110- case gitdiff.OpAdd:111111- // do nothing here112112- }113113- }114114- }115115-116116- return rf117117-}118118-119119-type MergeError struct {120120- msg string121121- mismatchingLine int64122122-}123123-124124-func (m MergeError) Error() string {125125- return fmt.Sprintf("%s: %v", m.msg, m.mismatchingLine)126126-}127127-128128-// best effort merging of two reconstructed files129129-func (this *ReconstructedFile) Merge(other *ReconstructedFile) (*ReconstructedFile, error) {130130- mergedFile := ReconstructedFile{}131131-132132- var i, j int64133133-134134- for int(i) < len(this.Data) || int(j) < len(other.Data) {135135- if int(i) >= len(this.Data) {136136- // first file is done; the rest of the lines from file 2 can go in137137- mergedFile.AddLine(other.Data[j])138138- j++139139- continue140140- }141141-142142- if int(j) >= len(other.Data) {143143- // first file is done; the rest of the lines from file 2 can go in144144- mergedFile.AddLine(this.Data[i])145145- i++146146- continue147147- }148148-149149- line1 := this.Data[i]150150- line2 := other.Data[j]151151-152152- if line1.LineNumber == line2.LineNumber {153153- if line1.Content != line2.Content {154154- return nil, MergeError{155155- msg: "mismatching lines, this patch might have undergone rebase",156156- mismatchingLine: line1.LineNumber,157157- }158158- } else {159159- mergedFile.AddLine(line1)160160- }161161- i++162162- j++163163- } else if line1.LineNumber < line2.LineNumber {164164- mergedFile.AddLine(line1)165165- i++166166- } else {167167- mergedFile.AddLine(line2)168168- j++169169- }170170- }171171-172172- return &mergedFile, nil173173-}174174-175175-func (r *ReconstructedFile) Apply(patch *gitdiff.File) (string, error) {176176- original := r.String()177177- var buffer bytes.Buffer178178- reader := strings.NewReader(original)179179-180180- err := gitdiff.Apply(&buffer, reader, patch)181181- if err != nil {182182- return "", err183183- }184184-185185- return buffer.String(), nil186186-}187187-188188-func Unified(oldText, oldFile, newText, newFile string) (string, error) {189189- oldTemp, err := os.CreateTemp("", "old_*")190190- if err != nil {191191- return "", fmt.Errorf("failed to create temp file for oldText: %w", err)192192- }193193- defer os.Remove(oldTemp.Name())194194- if _, err := oldTemp.WriteString(oldText); err != nil {195195- return "", fmt.Errorf("failed to write to old temp file: %w", err)196196- }197197- oldTemp.Close()198198-199199- newTemp, err := os.CreateTemp("", "new_*")200200- if err != nil {201201- return "", fmt.Errorf("failed to create temp file for newText: %w", err)202202- }203203- defer os.Remove(newTemp.Name())204204- if _, err := newTemp.WriteString(newText); err != nil {205205- return "", fmt.Errorf("failed to write to new temp file: %w", err)206206- }207207- newTemp.Close()208208-209209- cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name())210210- output, err := cmd.CombinedOutput()211211-212212- if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {213213- return string(output), nil214214- }215215- if err != nil {216216- return "", fmt.Errorf("diff command failed: %w", err)217217- }218218-219219- return string(output), nil220220-}221221-222222-type InterdiffResult struct {223223- Files []*InterdiffFile224224-}225225-226226-func (i *InterdiffResult) String() string {227227- var b strings.Builder228228- for _, f := range i.Files {229229- b.WriteString(f.String())230230- b.WriteString("\n")231231- }232232-233233- return b.String()234234-}235235-236236-type InterdiffFile struct {237237- *gitdiff.File238238- Name string239239- Status InterdiffFileStatus240240-}241241-242242-func (s *InterdiffFile) String() string {243243- var b strings.Builder244244- b.WriteString(s.Status.String())245245- b.WriteString(" ")246246-247247- if s.File != nil {248248- b.WriteString(bestName(s.File))249249- b.WriteString("\n")250250- b.WriteString(s.File.String())251251- }252252-253253- return b.String()254254-}255255-256256-type InterdiffFileStatus struct {257257- StatusKind StatusKind258258- Error error259259-}260260-261261-func (s *InterdiffFileStatus) String() string {262262- kind := s.StatusKind.String()263263- if s.Error != nil {264264- return fmt.Sprintf("%s [%s]", kind, s.Error.Error())265265- } else {266266- return kind267267- }268268-}269269-270270-func (s *InterdiffFileStatus) IsOk() bool {271271- return s.StatusKind == StatusOk272272-}273273-274274-func (s *InterdiffFileStatus) IsUnchanged() bool {275275- return s.StatusKind == StatusUnchanged276276-}277277-278278-func (s *InterdiffFileStatus) IsOnlyInOne() bool {279279- return s.StatusKind == StatusOnlyInOne280280-}281281-282282-func (s *InterdiffFileStatus) IsOnlyInTwo() bool {283283- return s.StatusKind == StatusOnlyInTwo284284-}285285-286286-func (s *InterdiffFileStatus) IsRebased() bool {287287- return s.StatusKind == StatusRebased288288-}289289-290290-func (s *InterdiffFileStatus) IsError() bool {291291- return s.StatusKind == StatusError292292-}293293-294294-type StatusKind int295295-296296-func (k StatusKind) String() string {297297- switch k {298298- case StatusOnlyInOne:299299- return "only in one"300300- case StatusOnlyInTwo:301301- return "only in two"302302- case StatusUnchanged:303303- return "unchanged"304304- case StatusRebased:305305- return "rebased"306306- case StatusError:307307- return "error"308308- default:309309- return "changed"310310- }311311-}312312-313313-const (314314- StatusOk StatusKind = iota315315- StatusOnlyInOne316316- StatusOnlyInTwo317317- StatusUnchanged318318- StatusRebased319319- StatusError320320-)321321-322322-func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile {323323- re1 := CreateOriginal(f1)324324- re2 := CreateOriginal(f2)325325-326326- interdiffFile := InterdiffFile{327327- Name: bestName(f1),328328- }329329-330330- merged, err := re1.Merge(&re2)331331- if err != nil {332332- interdiffFile.Status = InterdiffFileStatus{333333- StatusKind: StatusRebased,334334- Error: err,335335- }336336- return &interdiffFile337337- }338338-339339- rev1, err := merged.Apply(f1)340340- if err != nil {341341- interdiffFile.Status = InterdiffFileStatus{342342- StatusKind: StatusError,343343- Error: err,344344- }345345- return &interdiffFile346346- }347347-348348- rev2, err := merged.Apply(f2)349349- if err != nil {350350- interdiffFile.Status = InterdiffFileStatus{351351- StatusKind: StatusError,352352- Error: err,353353- }354354- return &interdiffFile355355- }356356-357357- diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2))358358- if err != nil {359359- interdiffFile.Status = InterdiffFileStatus{360360- StatusKind: StatusError,361361- Error: err,362362- }363363- return &interdiffFile364364- }365365-366366- parsed, _, err := gitdiff.Parse(strings.NewReader(diff))367367- if err != nil {368368- interdiffFile.Status = InterdiffFileStatus{369369- StatusKind: StatusError,370370- Error: err,371371- }372372- return &interdiffFile373373- }374374-375375- if len(parsed) != 1 {376376- // files are identical?377377- interdiffFile.Status = InterdiffFileStatus{378378- StatusKind: StatusUnchanged,379379- }380380- return &interdiffFile381381- }382382-383383- if interdiffFile.Status.StatusKind == StatusOk {384384- interdiffFile.File = parsed[0]385385- }386386-387387- return &interdiffFile388388-}389389-390390-func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult {391391- fileToIdx1 := make(map[string]int)392392- fileToIdx2 := make(map[string]int)393393- visited := make(map[string]struct{})394394- var result InterdiffResult395395-396396- for idx, f := range patch1 {397397- fileToIdx1[bestName(f)] = idx398398- }399399-400400- for idx, f := range patch2 {401401- fileToIdx2[bestName(f)] = idx402402- }403403-404404- for _, f1 := range patch1 {405405- var interdiffFile *InterdiffFile406406-407407- fileName := bestName(f1)408408- if idx, ok := fileToIdx2[fileName]; ok {409409- f2 := patch2[idx]410410-411411- // we have f1 and f2, calculate interdiff412412- interdiffFile = interdiffFiles(f1, f2)413413- } else {414414- // only in patch 1, this change would have to be "inverted" to dissapear415415- // from patch 2, so we reverseDiff(f1)416416- reverseDiff(f1)417417-418418- interdiffFile = &InterdiffFile{419419- File: f1,420420- Name: fileName,421421- Status: InterdiffFileStatus{422422- StatusKind: StatusOnlyInOne,423423- },424424- }425425- }426426-427427- result.Files = append(result.Files, interdiffFile)428428- visited[fileName] = struct{}{}429429- }430430-431431- // for all files in patch2 that remain unvisited; we can just add them into the output432432- for _, f2 := range patch2 {433433- fileName := bestName(f2)434434- if _, ok := visited[fileName]; ok {435435- continue436436- }437437-438438- result.Files = append(result.Files, &InterdiffFile{439439- File: f2,440440- Name: fileName,441441- Status: InterdiffFileStatus{442442- StatusKind: StatusOnlyInTwo,443443- },444444- })445445- }446446-447447- return &result448448-}
+168
patchutil/combinediff.go
···11+package patchutil22+33+import (44+ "fmt"55+ "strings"66+77+ "github.com/bluekeyes/go-gitdiff/gitdiff"88+)99+1010+// original1 -> patch1 -> rev11111+// original2 -> patch2 -> rev21212+//1313+// original2 must be equal to rev1, so we can merge them to get maximal context1414+//1515+// finally,1616+// rev2' <- apply(patch2, merged)1717+// combineddiff <- diff(rev2', original1)1818+func combineFiles(file1, file2 *gitdiff.File) (*gitdiff.File, error) {1919+ fileName := bestName(file1)2020+2121+ o1 := CreatePreImage(file1)2222+ r1 := CreatePostImage(file1)2323+ o2 := CreatePreImage(file2)2424+2525+ merged, err := r1.Merge(&o2)2626+ if err != nil {2727+ return nil, err2828+ }2929+3030+ r2Prime, err := merged.Apply(file2)3131+ if err != nil {3232+ return nil, err3333+ }3434+3535+ // produce combined diff3636+ diff, err := Unified(o1.String(), fileName, r2Prime, fileName)3737+ if err != nil {3838+ return nil, err3939+ }4040+4141+ parsed, _, err := gitdiff.Parse(strings.NewReader(diff))4242+4343+ if len(parsed) != 1 {4444+ // no diff? the second commit reverted the changes from the first4545+ return nil, nil4646+ }4747+4848+ return parsed[0], nil4949+}5050+5151+// use empty lines for lines we are unaware of5252+//5353+// this raises an error only if the two patches were invalid or non-contiguous5454+func mergeLines(old, new string) (string, error) {5555+ var i, j int5656+5757+ // TODO: use strings.Lines5858+ linesOld := strings.Split(old, "\n")5959+ linesNew := strings.Split(new, "\n")6060+6161+ result := []string{}6262+6363+ for i < len(linesOld) || j < len(linesNew) {6464+ if i >= len(linesOld) {6565+ // rest of the file is populated from `new`6666+ result = append(result, linesNew[j])6767+ j++6868+ continue6969+ }7070+7171+ if j >= len(linesNew) {7272+ // rest of the file is populated from `old`7373+ result = append(result, linesOld[i])7474+ i++7575+ continue7676+ }7777+7878+ oldLine := linesOld[i]7979+ newLine := linesNew[j]8080+8181+ if oldLine != newLine && (oldLine != "" && newLine != "") {8282+ // context mismatch8383+ return "", fmt.Errorf("failed to merge files, found context mismatch at %d; oldLine: `%s`, newline: `%s`", i+1, oldLine, newLine)8484+ }8585+8686+ if oldLine == newLine {8787+ result = append(result, oldLine)8888+ } else if oldLine == "" {8989+ result = append(result, newLine)9090+ } else if newLine == "" {9191+ result = append(result, oldLine)9292+ }9393+ i++9494+ j++9595+ }9696+9797+ return strings.Join(result, "\n"), nil9898+}9999+100100+func combineTwo(patch1, patch2 []*gitdiff.File) []*gitdiff.File {101101+ fileToIdx1 := make(map[string]int)102102+ fileToIdx2 := make(map[string]int)103103+ visited := make(map[string]struct{})104104+ var result []*gitdiff.File105105+106106+ for idx, f := range patch1 {107107+ fileToIdx1[bestName(f)] = idx108108+ }109109+110110+ for idx, f := range patch2 {111111+ fileToIdx2[bestName(f)] = idx112112+ }113113+114114+ for _, f1 := range patch1 {115115+ fileName := bestName(f1)116116+ if idx, ok := fileToIdx2[fileName]; ok {117117+ f2 := patch2[idx]118118+119119+ // we have f1 and f2, combine them120120+ combined, err := combineFiles(f1, f2)121121+ if err != nil {122122+ fmt.Println(err)123123+ }124124+125125+ result = append(result, combined)126126+ } else {127127+ // only in patch1; add as-is128128+ result = append(result, f1)129129+ }130130+131131+ visited[fileName] = struct{}{}132132+ }133133+134134+ // for all files in patch2 that remain unvisited; we can just add them into the output135135+ for _, f2 := range patch2 {136136+ fileName := bestName(f2)137137+ if _, ok := visited[fileName]; ok {138138+ continue139139+ }140140+141141+ result = append(result, f2)142142+ }143143+144144+ return result145145+}146146+147147+// pairwise combination from first to last patch148148+func CombineDiff(patches ...[]*gitdiff.File) []*gitdiff.File {149149+ if len(patches) == 0 {150150+ return nil151151+ }152152+153153+ if len(patches) == 1 {154154+ return patches[0]155155+ }156156+157157+ combined := combineTwo(patches[0], patches[1])158158+159159+ newPatches := [][]*gitdiff.File{}160160+ newPatches = append(newPatches, combined)161161+ for i, p := range patches {162162+ if i >= 2 {163163+ newPatches = append(newPatches, p)164164+ }165165+ }166166+167167+ return CombineDiff(newPatches...)168168+}
+178
patchutil/image.go
···11+package patchutil22+33+import (44+ "bytes"55+ "fmt"66+ "strings"77+88+ "github.com/bluekeyes/go-gitdiff/gitdiff"99+)1010+1111+type Line struct {1212+ LineNumber int641313+ Content string1414+ IsUnknown bool1515+}1616+1717+func NewLineAt(lineNumber int64, content string) Line {1818+ return Line{1919+ LineNumber: lineNumber,2020+ Content: content,2121+ IsUnknown: false,2222+ }2323+}2424+2525+type Image struct {2626+ File string2727+ Data []*Line2828+}2929+3030+func (r *Image) String() string {3131+ var i, j int643232+ var b strings.Builder3333+ for {3434+ i += 13535+3636+ if int(j) >= (len(r.Data)) {3737+ break3838+ }3939+4040+ if r.Data[j].LineNumber == i {4141+ // b.WriteString(fmt.Sprintf("%d:", r.Data[j].LineNumber))4242+ b.WriteString(r.Data[j].Content)4343+ j += 14444+ } else {4545+ //b.WriteString(fmt.Sprintf("%d:\n", i))4646+ b.WriteString("\n")4747+ }4848+ }4949+5050+ return b.String()5151+}5252+5353+func (r *Image) AddLine(line *Line) {5454+ r.Data = append(r.Data, line)5555+}5656+5757+// rebuild the original file from a patch5858+func CreatePreImage(file *gitdiff.File) Image {5959+ rf := Image{6060+ File: bestName(file),6161+ }6262+6363+ for _, fragment := range file.TextFragments {6464+ position := fragment.OldPosition6565+ for _, line := range fragment.Lines {6666+ switch line.Op {6767+ case gitdiff.OpContext:6868+ rl := NewLineAt(position, line.Line)6969+ rf.Data = append(rf.Data, &rl)7070+ position += 17171+ case gitdiff.OpDelete:7272+ rl := NewLineAt(position, line.Line)7373+ rf.Data = append(rf.Data, &rl)7474+ position += 17575+ case gitdiff.OpAdd:7676+ // do nothing here7777+ }7878+ }7979+ }8080+8181+ return rf8282+}8383+8484+// rebuild the revised file from a patch8585+func CreatePostImage(file *gitdiff.File) Image {8686+ rf := Image{8787+ File: bestName(file),8888+ }8989+9090+ for _, fragment := range file.TextFragments {9191+ position := fragment.NewPosition9292+ for _, line := range fragment.Lines {9393+ switch line.Op {9494+ case gitdiff.OpContext:9595+ rl := NewLineAt(position, line.Line)9696+ rf.Data = append(rf.Data, &rl)9797+ position += 19898+ case gitdiff.OpAdd:9999+ rl := NewLineAt(position, line.Line)100100+ rf.Data = append(rf.Data, &rl)101101+ position += 1102102+ case gitdiff.OpDelete:103103+ // do nothing here104104+ }105105+ }106106+ }107107+108108+ return rf109109+}110110+111111+type MergeError struct {112112+ msg string113113+ mismatchingLine int64114114+}115115+116116+func (m MergeError) Error() string {117117+ return fmt.Sprintf("%s: %v", m.msg, m.mismatchingLine)118118+}119119+120120+// best effort merging of two reconstructed files121121+func (this *Image) Merge(other *Image) (*Image, error) {122122+ mergedFile := Image{}123123+124124+ var i, j int64125125+126126+ for int(i) < len(this.Data) || int(j) < len(other.Data) {127127+ if int(i) >= len(this.Data) {128128+ // first file is done; the rest of the lines from file 2 can go in129129+ mergedFile.AddLine(other.Data[j])130130+ j++131131+ continue132132+ }133133+134134+ if int(j) >= len(other.Data) {135135+ // first file is done; the rest of the lines from file 2 can go in136136+ mergedFile.AddLine(this.Data[i])137137+ i++138138+ continue139139+ }140140+141141+ line1 := this.Data[i]142142+ line2 := other.Data[j]143143+144144+ if line1.LineNumber == line2.LineNumber {145145+ if line1.Content != line2.Content {146146+ return nil, MergeError{147147+ msg: "mismatching lines, this patch might have undergone rebase",148148+ mismatchingLine: line1.LineNumber,149149+ }150150+ } else {151151+ mergedFile.AddLine(line1)152152+ }153153+ i++154154+ j++155155+ } else if line1.LineNumber < line2.LineNumber {156156+ mergedFile.AddLine(line1)157157+ i++158158+ } else {159159+ mergedFile.AddLine(line2)160160+ j++161161+ }162162+ }163163+164164+ return &mergedFile, nil165165+}166166+167167+func (r *Image) Apply(patch *gitdiff.File) (string, error) {168168+ original := r.String()169169+ var buffer bytes.Buffer170170+ reader := strings.NewReader(original)171171+172172+ err := gitdiff.Apply(&buffer, reader, patch)173173+ if err != nil {174174+ return "", err175175+ }176176+177177+ return buffer.String(), nil178178+}
+236
patchutil/interdiff.go
···11+package patchutil22+33+import (44+ "fmt"55+ "strings"66+77+ "github.com/bluekeyes/go-gitdiff/gitdiff"88+)99+1010+type InterdiffResult struct {1111+ Files []*InterdiffFile1212+}1313+1414+func (i *InterdiffResult) String() string {1515+ var b strings.Builder1616+ for _, f := range i.Files {1717+ b.WriteString(f.String())1818+ b.WriteString("\n")1919+ }2020+2121+ return b.String()2222+}2323+2424+type InterdiffFile struct {2525+ *gitdiff.File2626+ Name string2727+ Status InterdiffFileStatus2828+}2929+3030+func (s *InterdiffFile) String() string {3131+ var b strings.Builder3232+ b.WriteString(s.Status.String())3333+ b.WriteString(" ")3434+3535+ if s.File != nil {3636+ b.WriteString(bestName(s.File))3737+ b.WriteString("\n")3838+ b.WriteString(s.File.String())3939+ }4040+4141+ return b.String()4242+}4343+4444+type InterdiffFileStatus struct {4545+ StatusKind StatusKind4646+ Error error4747+}4848+4949+func (s *InterdiffFileStatus) String() string {5050+ kind := s.StatusKind.String()5151+ if s.Error != nil {5252+ return fmt.Sprintf("%s [%s]", kind, s.Error.Error())5353+ } else {5454+ return kind5555+ }5656+}5757+5858+func (s *InterdiffFileStatus) IsOk() bool {5959+ return s.StatusKind == StatusOk6060+}6161+6262+func (s *InterdiffFileStatus) IsUnchanged() bool {6363+ return s.StatusKind == StatusUnchanged6464+}6565+6666+func (s *InterdiffFileStatus) IsOnlyInOne() bool {6767+ return s.StatusKind == StatusOnlyInOne6868+}6969+7070+func (s *InterdiffFileStatus) IsOnlyInTwo() bool {7171+ return s.StatusKind == StatusOnlyInTwo7272+}7373+7474+func (s *InterdiffFileStatus) IsRebased() bool {7575+ return s.StatusKind == StatusRebased7676+}7777+7878+func (s *InterdiffFileStatus) IsError() bool {7979+ return s.StatusKind == StatusError8080+}8181+8282+type StatusKind int8383+8484+func (k StatusKind) String() string {8585+ switch k {8686+ case StatusOnlyInOne:8787+ return "only in one"8888+ case StatusOnlyInTwo:8989+ return "only in two"9090+ case StatusUnchanged:9191+ return "unchanged"9292+ case StatusRebased:9393+ return "rebased"9494+ case StatusError:9595+ return "error"9696+ default:9797+ return "changed"9898+ }9999+}100100+101101+const (102102+ StatusOk StatusKind = iota103103+ StatusOnlyInOne104104+ StatusOnlyInTwo105105+ StatusUnchanged106106+ StatusRebased107107+ StatusError108108+)109109+110110+func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile {111111+ re1 := CreatePreImage(f1)112112+ re2 := CreatePreImage(f2)113113+114114+ interdiffFile := InterdiffFile{115115+ Name: bestName(f1),116116+ }117117+118118+ merged, err := re1.Merge(&re2)119119+ if err != nil {120120+ interdiffFile.Status = InterdiffFileStatus{121121+ StatusKind: StatusRebased,122122+ Error: err,123123+ }124124+ return &interdiffFile125125+ }126126+127127+ rev1, err := merged.Apply(f1)128128+ if err != nil {129129+ interdiffFile.Status = InterdiffFileStatus{130130+ StatusKind: StatusError,131131+ Error: err,132132+ }133133+ return &interdiffFile134134+ }135135+136136+ rev2, err := merged.Apply(f2)137137+ if err != nil {138138+ interdiffFile.Status = InterdiffFileStatus{139139+ StatusKind: StatusError,140140+ Error: err,141141+ }142142+ return &interdiffFile143143+ }144144+145145+ diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2))146146+ if err != nil {147147+ interdiffFile.Status = InterdiffFileStatus{148148+ StatusKind: StatusError,149149+ Error: err,150150+ }151151+ return &interdiffFile152152+ }153153+154154+ parsed, _, err := gitdiff.Parse(strings.NewReader(diff))155155+ if err != nil {156156+ interdiffFile.Status = InterdiffFileStatus{157157+ StatusKind: StatusError,158158+ Error: err,159159+ }160160+ return &interdiffFile161161+ }162162+163163+ if len(parsed) != 1 {164164+ // files are identical?165165+ interdiffFile.Status = InterdiffFileStatus{166166+ StatusKind: StatusUnchanged,167167+ }168168+ return &interdiffFile169169+ }170170+171171+ if interdiffFile.Status.StatusKind == StatusOk {172172+ interdiffFile.File = parsed[0]173173+ }174174+175175+ return &interdiffFile176176+}177177+178178+func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult {179179+ fileToIdx1 := make(map[string]int)180180+ fileToIdx2 := make(map[string]int)181181+ visited := make(map[string]struct{})182182+ var result InterdiffResult183183+184184+ for idx, f := range patch1 {185185+ fileToIdx1[bestName(f)] = idx186186+ }187187+188188+ for idx, f := range patch2 {189189+ fileToIdx2[bestName(f)] = idx190190+ }191191+192192+ for _, f1 := range patch1 {193193+ var interdiffFile *InterdiffFile194194+195195+ fileName := bestName(f1)196196+ if idx, ok := fileToIdx2[fileName]; ok {197197+ f2 := patch2[idx]198198+199199+ // we have f1 and f2, calculate interdiff200200+ interdiffFile = interdiffFiles(f1, f2)201201+ } else {202202+ // only in patch 1, this change would have to be "inverted" to dissapear203203+ // from patch 2, so we reverseDiff(f1)204204+ reverseDiff(f1)205205+206206+ interdiffFile = &InterdiffFile{207207+ File: f1,208208+ Name: fileName,209209+ Status: InterdiffFileStatus{210210+ StatusKind: StatusOnlyInOne,211211+ },212212+ }213213+ }214214+215215+ result.Files = append(result.Files, interdiffFile)216216+ visited[fileName] = struct{}{}217217+ }218218+219219+ // for all files in patch2 that remain unvisited; we can just add them into the output220220+ for _, f2 := range patch2 {221221+ fileName := bestName(f2)222222+ if _, ok := visited[fileName]; ok {223223+ continue224224+ }225225+226226+ result.Files = append(result.Files, &InterdiffFile{227227+ File: f2,228228+ Name: fileName,229229+ Status: InterdiffFileStatus{230230+ StatusKind: StatusOnlyInTwo,231231+ },232232+ })233233+ }234234+235235+ return &result236236+}
+69
patchutil/patchutil.go
···2233import (44 "fmt"55+ "os"66+ "os/exec"57 "regexp"68 "strings"79···126124 patches[i] = strings.TrimSpace(patchText[startPos:endPos])127125 }128126 return patches127127+}128128+129129+func bestName(file *gitdiff.File) string {130130+ if file.IsDelete {131131+ return file.OldName132132+ } else {133133+ return file.NewName134134+ }135135+}136136+137137+// in-place reverse of a diff138138+func reverseDiff(file *gitdiff.File) {139139+ file.OldName, file.NewName = file.NewName, file.OldName140140+ file.OldMode, file.NewMode = file.NewMode, file.OldMode141141+ file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment142142+143143+ for _, fragment := range file.TextFragments {144144+ // swap postions145145+ fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition146146+ fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines147147+ fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded148148+149149+ for i := range fragment.Lines {150150+ switch fragment.Lines[i].Op {151151+ case gitdiff.OpAdd:152152+ fragment.Lines[i].Op = gitdiff.OpDelete153153+ case gitdiff.OpDelete:154154+ fragment.Lines[i].Op = gitdiff.OpAdd155155+ default:156156+ // do nothing157157+ }158158+ }159159+ }160160+}161161+162162+func Unified(oldText, oldFile, newText, newFile string) (string, error) {163163+ oldTemp, err := os.CreateTemp("", "old_*")164164+ if err != nil {165165+ return "", fmt.Errorf("failed to create temp file for oldText: %w", err)166166+ }167167+ defer os.Remove(oldTemp.Name())168168+ if _, err := oldTemp.WriteString(oldText); err != nil {169169+ return "", fmt.Errorf("failed to write to old temp file: %w", err)170170+ }171171+ oldTemp.Close()172172+173173+ newTemp, err := os.CreateTemp("", "new_*")174174+ if err != nil {175175+ return "", fmt.Errorf("failed to create temp file for newText: %w", err)176176+ }177177+ defer os.Remove(newTemp.Name())178178+ if _, err := newTemp.WriteString(newText); err != nil {179179+ return "", fmt.Errorf("failed to write to new temp file: %w", err)180180+ }181181+ newTemp.Close()182182+183183+ cmd := exec.Command("diff", "-U", "9999", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name())184184+ output, err := cmd.CombinedOutput()185185+186186+ if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {187187+ return string(output), nil188188+ }189189+ if err != nil {190190+ return "", fmt.Errorf("diff command failed: %w", err)191191+ }192192+193193+ return string(output), nil129194}