├── test_examples ├── empty.diff ├── s3_a_b.diff ├── s3_a.diff ├── s3_b.diff ├── f2_b.diff ├── source_1_d │ ├── file_2.txt │ └── file_3.txt ├── source_1_b │ ├── file_1.txt │ └── file_2.txt ├── f2_a.diff ├── source_1 │ ├── file_1.txt │ └── file_2.txt ├── source_1_c │ ├── file_2.txt │ └── file_1.txt ├── f1_a_wrong_origin.diff ├── source_1_a │ ├── file_2.txt │ └── file_1.txt ├── f1_a.diff ├── s1_a_d.diff ├── f2_b_c.diff ├── f2_a_c.diff ├── s1_c_d.diff ├── f1_b.diff ├── f1_a_c.diff ├── f1_b_c.diff ├── s1_a.diff ├── s1_b.diff ├── s1_a_c.diff ├── s1_a_b.diff ├── s1_b_c.diff ├── s2_a.diff ├── s2_b.diff └── s2_a_b.diff ├── docs ├── mixed_mode.png └── interdiff_mode.png ├── .github └── workflows │ ├── masteronly.yml │ └── main.yml ├── go.mod ├── cli ├── main.go ├── interdiff.go └── mixed.go ├── LICENSE ├── go.sum ├── README.md ├── patchutils_test.go └── patchutils.go /test_examples/empty.diff: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/mixed_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-patchutils/HEAD/docs/mixed_mode.png -------------------------------------------------------------------------------- /docs/interdiff_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/go-patchutils/HEAD/docs/interdiff_mode.png -------------------------------------------------------------------------------- /test_examples/s3_a_b.diff: -------------------------------------------------------------------------------- 1 | --- old/file 2020-09-21 17:07:07.661320477 +0200 2 | +++ new/file 2020-09-21 17:07:07.661320477 +0200 3 | @@ -1,1 +1,3 @@ 4 | +new 5 | patched 6 | +new 7 | -------------------------------------------------------------------------------- /test_examples/s3_a.diff: -------------------------------------------------------------------------------- 1 | diff -u orig/file old/file 2 | --- orig/file 2020-09-21 17:07:07.661320477 +0200 3 | +++ old/file 2020-09-21 17:07:07.661320477 +0200 4 | @@ -1 +1 @@ 5 | -orig 6 | +patched 7 | -------------------------------------------------------------------------------- /test_examples/s3_b.diff: -------------------------------------------------------------------------------- 1 | diff -u orig/file new/file 2 | --- orig/file 2020-09-21 17:07:07.661320477 +0200 3 | +++ new/file 2020-09-21 17:07:07.661320477 +0200 4 | @@ -1 +1,3 @@ 5 | -orig 6 | +new 7 | +patched 8 | +new 9 | -------------------------------------------------------------------------------- /.github/workflows/masteronly.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Check latest master 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Update reportcard 11 | uses: creekorful/goreportcard-action@v1.0 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/go-patchutils 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/google/subcommands v1.2.0 8 | github.com/kylelemons/godebug v1.1.0 9 | github.com/sourcegraph/go-diff v0.6.0 10 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 11 | ) 12 | -------------------------------------------------------------------------------- /test_examples/f2_b.diff: -------------------------------------------------------------------------------- 1 | --- source_1/file_2.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_b/file_2.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -9,4 +9,5 @@ 4 | Outward general passage another as it. 5 | Very his are come man walk one next. 6 | Delighted prevailed supported too not remainder perpetual who furnished. 7 | +Outward general it. 8 | Nay affronting bed projection compliment instrument. 9 | -------------------------------------------------------------------------------- /test_examples/source_1_d/file_2.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | Affronting everything discretion men now own did. 4 | Still round match we to. 5 | Frankness pronounce daughters remainder extensive has but. 6 | Plenty season beyond by hardly giving of. 7 | Very his are come man walk one next. 8 | Man walk one next. 9 | Delighted prevailed supported too not remainder perpetual who furnished. 10 | Nay affronting bed projection compliment instrument. 11 | -------------------------------------------------------------------------------- /test_examples/source_1_b/file_1.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | In to am attended desirous raptures declared diverted confined at. 4 | At or happiness commanded daughters as. 5 | Collected instantly remaining up certainly to necessary as. 6 | Over walk dull into son boy door went new. 7 | Only week bore boy what fat case left use. 8 | Is handsome an declared at received in extended vicinity subjects. 9 | Match round scale now style far times. Your me past an much. 10 | -------------------------------------------------------------------------------- /test_examples/f2_a.diff: -------------------------------------------------------------------------------- 1 | --- source_1/file_2.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_a/file_2.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -3,7 +3,6 @@ 4 | Affronting everything discretion men now own did. 5 | Still round match we to. 6 | Frankness pronounce daughters remainder extensive has but. 7 | -Happiness cordially one determine concluded fat. 8 | Plenty season beyond by hardly giving of. 9 | Consulted or acuteness dejection an smallness if. 10 | Outward general passage another as it. 11 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | // Package main provides a cli tool to compute the diff between source and diff files. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "os" 8 | 9 | "github.com/google/subcommands" 10 | ) 11 | 12 | func main() { 13 | subcommands.Register(subcommands.HelpCommand(), "") 14 | subcommands.Register(subcommands.FlagsCommand(), "") 15 | subcommands.Register(subcommands.CommandsCommand(), "") 16 | 17 | flag.Parse() 18 | ctx := context.Background() 19 | os.Exit(int(subcommands.Execute(ctx))) 20 | } 21 | -------------------------------------------------------------------------------- /test_examples/source_1/file_1.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | In to am attended desirous raptures declared diverted confined at. 4 | Collected instantly remaining up certainly to necessary as. 5 | Over walk dull into son boy door went new. 6 | At or happiness commanded daughters as. 7 | Is handsome an declared at received in extended vicinity subjects. 8 | Into miss on he over been late pain an. 9 | Only week bore boy what fat case left use. 10 | Match round scale now style far times. Your me past an much. 11 | -------------------------------------------------------------------------------- /test_examples/source_1_c/file_2.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | Affronting everything discretion men now own did. 4 | Still round match we to. 5 | Frankness pronounce daughters remainder extensive has but. 6 | Happiness cordially one determine concluded fat. 7 | Plenty season beyond by hardly giving of. 8 | Very his are come man walk one next. 9 | Delighted prevailed supported too not remainder perpetual who furnished. 10 | Nay affronting bed projection compliment instrument. 11 | Still round match we to here. 12 | -------------------------------------------------------------------------------- /test_examples/f1_a_wrong_origin.diff: -------------------------------------------------------------------------------- 1 | --- source_1/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_a/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -5,6 +5,8 @@ 4 | Over walk dull into son boy door went new. 5 | At or happiness commanded daughters as. 6 | Is. 7 | +Is handsome received in extended vicinity subjects. 8 | +Is handsome an declared at vicinity subjects. 9 | Into miss on he over been late pain an. 10 | Only week bore boy what fat case left use. 11 | Match round scale now style far times. Your me past an much. 12 | -------------------------------------------------------------------------------- /test_examples/source_1_a/file_2.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | Affronting everything discretion men now own did. 4 | Still round match we to. 5 | Frankness pronounce daughters remainder extensive has but. 6 | Plenty season beyond by hardly giving of. 7 | Consulted or acuteness dejection an smallness if. 8 | Outward general passage another as it. 9 | Very his are come man walk one next. 10 | Delighted prevailed supported too not remainder perpetual who furnished. 11 | Nay affronting bed projection compliment instrument. 12 | -------------------------------------------------------------------------------- /test_examples/f1_a.diff: -------------------------------------------------------------------------------- 1 | --- source_1/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_a/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -5,6 +5,8 @@ 4 | Over walk dull into son boy door went new. 5 | At or happiness commanded daughters as. 6 | Is handsome an declared at received in extended vicinity subjects. 7 | +Is handsome received in extended vicinity subjects. 8 | +Is handsome an declared at vicinity subjects. 9 | Into miss on he over been late pain an. 10 | Only week bore boy what fat case left use. 11 | Match round scale now style far times. Your me past an much. 12 | -------------------------------------------------------------------------------- /test_examples/source_1/file_2.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | Affronting everything discretion men now own did. 4 | Still round match we to. 5 | Frankness pronounce daughters remainder extensive has but. 6 | Happiness cordially one determine concluded fat. 7 | Plenty season beyond by hardly giving of. 8 | Consulted or acuteness dejection an smallness if. 9 | Outward general passage another as it. 10 | Very his are come man walk one next. 11 | Delighted prevailed supported too not remainder perpetual who furnished. 12 | Nay affronting bed projection compliment instrument. 13 | -------------------------------------------------------------------------------- /test_examples/source_1_b/file_2.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | Affronting everything discretion men now own did. 4 | Still round match we to. 5 | Frankness pronounce daughters remainder extensive has but. 6 | Happiness cordially one determine concluded fat. 7 | Plenty season beyond by hardly giving of. 8 | Consulted or acuteness dejection an smallness if. 9 | Outward general passage another as it. 10 | Very his are come man walk one next. 11 | Delighted prevailed supported too not remainder perpetual who furnished. 12 | Outward general it. 13 | Nay affronting bed projection compliment instrument. 14 | -------------------------------------------------------------------------------- /test_examples/source_1_a/file_1.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | In to am attended desirous raptures declared diverted confined at. 4 | Collected instantly remaining up certainly to necessary as. 5 | Over walk dull into son boy door went new. 6 | At or happiness commanded daughters as. 7 | Is handsome an declared at received in extended vicinity subjects. 8 | Is handsome received in extended vicinity subjects. 9 | Is handsome an declared at vicinity subjects. 10 | Into miss on he over been late pain an. 11 | Only week bore boy what fat case left use. 12 | Match round scale now style far times. Your me past an much. 13 | -------------------------------------------------------------------------------- /test_examples/s1_a_d.diff: -------------------------------------------------------------------------------- 1 | Only in source_1_a: file_1.txt 2 | --- source_1_a/file_2.txt 2020-07-28 12:54:18.000000000 +0000 3 | +++ source_1_d/file_2.txt 2020-08-26 01:17:35.440157091 +0000 4 | @@ -5,7 +5,6 @@ 5 | Frankness pronounce daughters remainder extensive has but. 6 | Plenty season beyond by hardly giving of. 7 | -Consulted or acuteness dejection an smallness if. 8 | -Outward general passage another as it. 9 | Very his are come man walk one next. 10 | +Man walk one next. 11 | Delighted prevailed supported too not remainder perpetual who furnished. 12 | Nay affronting bed projection compliment instrument. 13 | Only in source_1_d: file_3.txt 14 | -------------------------------------------------------------------------------- /test_examples/f2_b_c.diff: -------------------------------------------------------------------------------- 1 | --- source_1_b/file_2.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_c/file_2.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -5,9 +5,7 @@ 4 | Frankness pronounce daughters remainder extensive has but. 5 | Happiness cordially one determine concluded fat. 6 | Plenty season beyond by hardly giving of. 7 | -Consulted or acuteness dejection an smallness if. 8 | -Outward general passage another as it. 9 | Very his are come man walk one next. 10 | Delighted prevailed supported too not remainder perpetual who furnished. 11 | -Outward general it. 12 | Nay affronting bed projection compliment instrument. 13 | +Still round match we to here. 14 | -------------------------------------------------------------------------------- /test_examples/f2_a_c.diff: -------------------------------------------------------------------------------- 1 | --- source_1_a/file_2.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_c/file_2.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -4,8 +4,8 @@ 4 | Still round match we to. 5 | Frankness pronounce daughters remainder extensive has but. 6 | +Happiness cordially one determine concluded fat. 7 | Plenty season beyond by hardly giving of. 8 | -Consulted or acuteness dejection an smallness if. 9 | -Outward general passage another as it. 10 | Very his are come man walk one next. 11 | Delighted prevailed supported too not remainder perpetual who furnished. 12 | Nay affronting bed projection compliment instrument. 13 | +Still round match we to here. 14 | -------------------------------------------------------------------------------- /test_examples/s1_c_d.diff: -------------------------------------------------------------------------------- 1 | Only in source_1_c: file_1.txt 2 | --- source_1_c/file_2.txt 2020-07-28 12:54:18.000000000 +0000 3 | +++ source_1_d/file_2.txt 2020-08-26 01:17:35.440157091 +0000 4 | @@ -4,8 +4,7 @@ 5 | Still round match we to. 6 | Frankness pronounce daughters remainder extensive has but. 7 | -Happiness cordially one determine concluded fat. 8 | Plenty season beyond by hardly giving of. 9 | Very his are come man walk one next. 10 | +Man walk one next. 11 | Delighted prevailed supported too not remainder perpetual who furnished. 12 | Nay affronting bed projection compliment instrument. 13 | -Still round match we to here. 14 | Only in source_1_d: file_3.txt 15 | -------------------------------------------------------------------------------- /test_examples/source_1_c/file_1.txt: -------------------------------------------------------------------------------- 1 | Text generated by www.randomtextgenerator.com 2 | 3 | 4 | In to am attended desirous raptures declared diverted confined at. 5 | In to are desirous declared diverted confined at. 6 | In to am attended raptures declared diverted confined at. 7 | In to am attended desirous confined at. 8 | Collected instantly remaining up certainly to necessary as. 9 | Over walk dull into son boy door went new. 10 | At or happiness commanded daughters as. 11 | Is handsome an declared at received in extended vicinity subjects. 12 | Into miss on he over been late pain an. 13 | Only week bore boy what fat case left use. 14 | Match round scale now style far times. Your me past an much. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /test_examples/f1_b.diff: -------------------------------------------------------------------------------- 1 | --- source_1/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_b/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -1,10 +1,9 @@ 4 | Text generated by www.randomtextgenerator.com 5 | 6 | In to am attended desirous raptures declared diverted confined at. 7 | +At or happiness commanded daughters as. 8 | Collected instantly remaining up certainly to necessary as. 9 | Over walk dull into son boy door went new. 10 | -At or happiness commanded daughters as. 11 | -Is handsome an declared at received in extended vicinity subjects. 12 | -Into miss on he over been late pain an. 13 | Only week bore boy what fat case left use. 14 | +Is handsome an declared at received in extended vicinity subjects. 15 | Match round scale now style far times. Your me past an much. 16 | -------------------------------------------------------------------------------- /test_examples/source_1_d/file_3.txt: -------------------------------------------------------------------------------- 1 | // findOverlappingHunkSet finds next set (two arrays: oldHunks and newHunks) of 2 | // overlapping hunks in oldFileDiff and newFileDiff, starting from position i, j relatively. 3 | func findOverlappingHunkSet(oldFileDiff, newFileDiff *diff.FileDiff, i, j *int) (oldHunks, newHunks []*diff.Hunk) {} 4 | 5 | // mergeOverlappingHunks returns a new diff.Hunk that is a diff hunk between overlapping oldHunks and newHunks, 6 | // related to the same source file. 7 | func mergeOverlappingHunks(oldHunks, newHunks []*diff.Hunk) (*diff.Hunk, error) {} 8 | 9 | // configureResultHunk returns a new diff.Hunk (with configured StartLines and NumberLines) 10 | // and currentOrgI (number of anchor line) based on oldHunks and newHunks, for their further merge. 11 | func configureResultHunk(oldHunks, newHunks []*diff.Hunk) (*diff.Hunk, int32, error) {} 12 | -------------------------------------------------------------------------------- /test_examples/f1_a_c.diff: -------------------------------------------------------------------------------- 1 | --- source_1_a/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_c/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -1,11 +1,13 @@ 4 | Text generated by www.randomtextgenerator.com 5 | 6 | + 7 | In to am attended desirous raptures declared diverted confined at. 8 | +In to are desirous declared diverted confined at. 9 | +In to am attended raptures declared diverted confined at. 10 | +In to am attended desirous confined at. 11 | Collected instantly remaining up certainly to necessary as. 12 | Over walk dull into son boy door went new. 13 | At or happiness commanded daughters as. 14 | Is handsome an declared at received in extended vicinity subjects. 15 | -Is handsome received in extended vicinity subjects. 16 | -Is handsome an declared at vicinity subjects. 17 | Into miss on he over been late pain an. 18 | Only week bore boy what fat case left use. 19 | -------------------------------------------------------------------------------- /test_examples/f1_b_c.diff: -------------------------------------------------------------------------------- 1 | --- source_1_b/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_c/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -1,9 +1,14 @@ 4 | Text generated by www.randomtextgenerator.com 5 | 6 | + 7 | In to am attended desirous raptures declared diverted confined at. 8 | -At or happiness commanded daughters as. 9 | +In to are desirous declared diverted confined at. 10 | +In to am attended raptures declared diverted confined at. 11 | +In to am attended desirous confined at. 12 | Collected instantly remaining up certainly to necessary as. 13 | Over walk dull into son boy door went new. 14 | -Only week bore boy what fat case left use. 15 | +At or happiness commanded daughters as. 16 | Is handsome an declared at received in extended vicinity subjects. 17 | +Into miss on he over been late pain an. 18 | +Only week bore boy what fat case left use. 19 | Match round scale now style far times. Your me past an much. 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - v* 5 | branches: 6 | - master 7 | pull_request: 8 | name: Test and Lint 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.13.x, 1.14.x] 14 | platform: [ubuntu-latest] 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | - name: Test 24 | run: go test ./... 25 | lint: 26 | name: lint 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: golangci-lint 31 | uses: golangci/golangci-lint-action@v1 32 | with: 33 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 34 | version: v1.28 -------------------------------------------------------------------------------- /test_examples/s1_a.diff: -------------------------------------------------------------------------------- 1 | diff -u source_1/file_1.txt source_1_a/file_1.txt 2 | --- source_1/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | +++ source_1_a/file_1.txt 2020-07-28 12:54:18.000000000 +0000 4 | @@ -5,6 +5,8 @@ 5 | Over walk dull into son boy door went new. 6 | At or happiness commanded daughters as. 7 | Is handsome an declared at received in extended vicinity subjects. 8 | +Is handsome received in extended vicinity subjects. 9 | +Is handsome an declared at vicinity subjects. 10 | Into miss on he over been late pain an. 11 | Only week bore boy what fat case left use. 12 | Match round scale now style far times. Your me past an much. 13 | diff -u source_1/file_2.txt source_1_a/file_2.txt 14 | --- source_1/file_2.txt 2020-07-28 12:54:18.000000000 +0000 15 | +++ source_1_a/file_2.txt 2020-07-28 12:54:18.000000000 +0000 16 | @@ -3,7 +3,6 @@ 17 | Affronting everything discretion men now own did. 18 | Still round match we to. 19 | Frankness pronounce daughters remainder extensive has but. 20 | -Happiness cordially one determine concluded fat. 21 | Plenty season beyond by hardly giving of. 22 | Consulted or acuteness dejection an smallness if. 23 | Outward general passage another as it. 24 | -------------------------------------------------------------------------------- /test_examples/s1_b.diff: -------------------------------------------------------------------------------- 1 | diff -u source_1/file_1.txt source_1_b/file_1.txt 2 | --- source_1/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | +++ source_1_b/file_1.txt 2020-07-28 12:54:18.000000000 +0000 4 | @@ -1,10 +1,9 @@ 5 | Text generated by www.randomtextgenerator.com 6 | 7 | In to am attended desirous raptures declared diverted confined at. 8 | +At or happiness commanded daughters as. 9 | Collected instantly remaining up certainly to necessary as. 10 | Over walk dull into son boy door went new. 11 | -At or happiness commanded daughters as. 12 | -Is handsome an declared at received in extended vicinity subjects. 13 | -Into miss on he over been late pain an. 14 | Only week bore boy what fat case left use. 15 | +Is handsome an declared at received in extended vicinity subjects. 16 | Match round scale now style far times. Your me past an much. 17 | diff -u source_1/file_2.txt source_1_b/file_2.txt 18 | --- source_1/file_2.txt 2020-07-28 12:54:18.000000000 +0000 19 | +++ source_1_b/file_2.txt 2020-07-28 12:54:18.000000000 +0000 20 | @@ -9,4 +9,5 @@ 21 | Outward general passage another as it. 22 | Very his are come man walk one next. 23 | Delighted prevailed supported too not remainder perpetual who furnished. 24 | +Outward general it. 25 | Nay affronting bed projection compliment instrument. 26 | -------------------------------------------------------------------------------- /test_examples/s1_a_c.diff: -------------------------------------------------------------------------------- 1 | --- source_1_a/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_c/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -1,11 +1,13 @@ 4 | Text generated by www.randomtextgenerator.com 5 | 6 | + 7 | In to am attended desirous raptures declared diverted confined at. 8 | +In to are desirous declared diverted confined at. 9 | +In to am attended raptures declared diverted confined at. 10 | +In to am attended desirous confined at. 11 | Collected instantly remaining up certainly to necessary as. 12 | Over walk dull into son boy door went new. 13 | At or happiness commanded daughters as. 14 | Is handsome an declared at received in extended vicinity subjects. 15 | -Is handsome received in extended vicinity subjects. 16 | -Is handsome an declared at vicinity subjects. 17 | Into miss on he over been late pain an. 18 | Only week bore boy what fat case left use. 19 | --- source_1_a/file_2.txt 2020-07-28 12:54:18.000000000 +0000 20 | +++ source_1_c/file_2.txt 2020-07-28 12:54:18.000000000 +0000 21 | @@ -4,8 +4,8 @@ 22 | Still round match we to. 23 | Frankness pronounce daughters remainder extensive has but. 24 | +Happiness cordially one determine concluded fat. 25 | Plenty season beyond by hardly giving of. 26 | -Consulted or acuteness dejection an smallness if. 27 | -Outward general passage another as it. 28 | Very his are come man walk one next. 29 | Delighted prevailed supported too not remainder perpetual who furnished. 30 | Nay affronting bed projection compliment instrument. 31 | +Still round match we to here. 32 | -------------------------------------------------------------------------------- /test_examples/s1_a_b.diff: -------------------------------------------------------------------------------- 1 | --- source_1_a/file_1.txt 2020-07-28 12:54:18.000000000 +0000 2 | +++ source_1_b/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | @@ -1,12 +1,9 @@ 4 | Text generated by www.randomtextgenerator.com 5 | 6 | In to am attended desirous raptures declared diverted confined at. 7 | +At or happiness commanded daughters as. 8 | Collected instantly remaining up certainly to necessary as. 9 | Over walk dull into son boy door went new. 10 | -At or happiness commanded daughters as. 11 | -Is handsome an declared at received in extended vicinity subjects. 12 | -Is handsome received in extended vicinity subjects. 13 | -Is handsome an declared at vicinity subjects. 14 | -Into miss on he over been late pain an. 15 | Only week bore boy what fat case left use. 16 | +Is handsome an declared at received in extended vicinity subjects. 17 | Match round scale now style far times. Your me past an much. 18 | --- source_1_a/file_2.txt 2020-07-28 12:54:18.000000000 +0000 19 | +++ source_1_b/file_2.txt 2020-07-28 12:54:18.000000000 +0000 20 | @@ -3,9 +3,11 @@ 21 | Affronting everything discretion men now own did. 22 | Still round match we to. 23 | Frankness pronounce daughters remainder extensive has but. 24 | +Happiness cordially one determine concluded fat. 25 | Plenty season beyond by hardly giving of. 26 | Consulted or acuteness dejection an smallness if. 27 | Outward general passage another as it. 28 | Very his are come man walk one next. 29 | Delighted prevailed supported too not remainder perpetual who furnished. 30 | +Outward general it. 31 | Nay affronting bed projection compliment instrument. 32 | -------------------------------------------------------------------------------- /test_examples/s1_b_c.diff: -------------------------------------------------------------------------------- 1 | diff -u source_1_b/file_1.txt source_1_c/file_1.txt 2 | --- source_1_b/file_1.txt 2020-07-28 12:54:18.000000000 +0000 3 | +++ source_1_c/file_1.txt 2020-07-28 12:54:18.000000000 +0000 4 | @@ -1,9 +1,14 @@ 5 | Text generated by www.randomtextgenerator.com 6 | 7 | + 8 | In to am attended desirous raptures declared diverted confined at. 9 | -At or happiness commanded daughters as. 10 | +In to are desirous declared diverted confined at. 11 | +In to am attended raptures declared diverted confined at. 12 | +In to am attended desirous confined at. 13 | Collected instantly remaining up certainly to necessary as. 14 | Over walk dull into son boy door went new. 15 | -Only week bore boy what fat case left use. 16 | +At or happiness commanded daughters as. 17 | Is handsome an declared at received in extended vicinity subjects. 18 | +Into miss on he over been late pain an. 19 | +Only week bore boy what fat case left use. 20 | Match round scale now style far times. Your me past an much. 21 | diff -u source_1_b/file_2.txt source_1_c/file_2.txt 22 | --- source_1_b/file_2.txt 2020-07-28 12:54:18.000000000 +0000 23 | +++ source_1_c/file_2.txt 2020-07-28 12:54:18.000000000 +0000 24 | @@ -5,9 +5,7 @@ 25 | Frankness pronounce daughters remainder extensive has but. 26 | Happiness cordially one determine concluded fat. 27 | Plenty season beyond by hardly giving of. 28 | -Consulted or acuteness dejection an smallness if. 29 | -Outward general passage another as it. 30 | Very his are come man walk one next. 31 | Delighted prevailed supported too not remainder perpetual who furnished. 32 | -Outward general it. 33 | Nay affronting bed projection compliment instrument. 34 | +Still round match we to here. 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 2 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 3 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 4 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= 7 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 8 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 9 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 10 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= 11 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 12 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 13 | github.com/sourcegraph/go-diff v0.6.0 h1:WbN9e/jD8ujU+o0vd9IFN5AEwtfB0rn/zM/AANaClqQ= 14 | github.com/sourcegraph/go-diff v0.6.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= 15 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= 16 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 18 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 19 | -------------------------------------------------------------------------------- /cli/interdiff.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/golang/glog" 10 | "github.com/google/go-patchutils" 11 | "github.com/google/subcommands" 12 | ) 13 | 14 | type interdiffCmd struct { 15 | oldDiff string 16 | newDiff string 17 | } 18 | 19 | func init() { 20 | subcommands.Register(&interdiffCmd{}, "") 21 | } 22 | 23 | func (*interdiffCmd) Name() string { return "interdiff" } 24 | func (*interdiffCmd) Synopsis() string { 25 | return "compute difference between " + 26 | "source patched with oldDiff and same source patched with newDiff." 27 | } 28 | func (*interdiffCmd) Usage() string { 29 | return "interdiff -olddiff= -newdiff=: " + 30 | "Compute difference between source patched with oldDiff and same source patched with newDiff.\n" 31 | } 32 | 33 | func (c *interdiffCmd) SetFlags(f *flag.FlagSet) { 34 | f.StringVar(&c.oldDiff, "olddiff", "", "path to the old version of diff") 35 | f.StringVar(&c.newDiff, "newdiff", "", "path to the new version of diff") 36 | } 37 | 38 | func (c *interdiffCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 39 | if (c.oldDiff == "") || (c.newDiff == "") { 40 | glog.Error("Error: necessary flags aren't assigned") 41 | glog.Infof("Usage: %s %s", os.Args[0], c.Usage()) 42 | return subcommands.ExitUsageError 43 | } 44 | 45 | oldD, err := os.Open(c.oldDiff) 46 | if err != nil { 47 | glog.Errorf("Failed to open oldDiffFile: %q\n", c.oldDiff) 48 | return subcommands.ExitFailure 49 | } 50 | defer oldD.Close() 51 | 52 | newD, err := os.Open(c.newDiff) 53 | if err != nil { 54 | glog.Errorf("Failed to open newDiffFile %q\n", c.newDiff) 55 | return subcommands.ExitFailure 56 | } 57 | defer newD.Close() 58 | 59 | result, err := patchutils.InterDiff(oldD, newD) 60 | if err != nil { 61 | glog.Errorf("Error during computing diff for %q and %q: %v\n", c.oldDiff, c.newDiff, err) 62 | return subcommands.ExitFailure 63 | } 64 | 65 | fmt.Println(result) 66 | return subcommands.ExitSuccess 67 | } 68 | -------------------------------------------------------------------------------- /cli/mixed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/golang/glog" 10 | "github.com/google/go-patchutils" 11 | "github.com/google/subcommands" 12 | ) 13 | 14 | type mixedCmd struct { 15 | oldSource string 16 | oldDiff string 17 | newSource string 18 | newDiff string 19 | } 20 | 21 | func init() { 22 | subcommands.Register(&mixedCmd{}, "") 23 | } 24 | 25 | func (*mixedCmd) Name() string { return "mixed" } 26 | func (*mixedCmd) Synopsis() string { 27 | return "compute difference between " + 28 | "oldSource patched with oldDiff and newSource patched with newDiff." 29 | } 30 | func (*mixedCmd) Usage() string { 31 | return "mixed -oldsource= -olddiff= -newsource= -newdiff=: " + 32 | "Compute difference between oldSource patched with oldDiff and newSource patched with newDiff.\n" 33 | } 34 | 35 | func (c *mixedCmd) SetFlags(f *flag.FlagSet) { 36 | f.StringVar(&c.oldSource, "oldsource", "", "path to the old version of source") 37 | f.StringVar(&c.oldDiff, "olddiff", "", "path to the old version of diff") 38 | f.StringVar(&c.newSource, "newsource", "", "path to the new version of source") 39 | f.StringVar(&c.newDiff, "newdiff", "", "path to the new version of diff") 40 | } 41 | 42 | func (c *mixedCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 43 | if (c.oldSource == "") || (c.oldDiff == "") || (c.newSource == "") || (c.newDiff == "") { 44 | glog.Errorf("Error: necessary flags aren't assigned") 45 | glog.Infof("Usage: %s %s", os.Args[0], c.Usage()) 46 | return subcommands.ExitUsageError 47 | } 48 | 49 | oldD, err := os.Open(c.oldDiff) 50 | if err != nil { 51 | glog.Errorf("Failed to open oldDiffFile %q\n", c.oldDiff) 52 | return subcommands.ExitFailure 53 | } 54 | defer oldD.Close() 55 | 56 | newD, err := os.Open(c.newDiff) 57 | if err != nil { 58 | glog.Errorf("Failed to open newDiffFile %q\n", c.newDiff) 59 | return subcommands.ExitFailure 60 | } 61 | defer newD.Close() 62 | 63 | result, err := patchutils.MixedModePath(c.oldSource, c.newSource, oldD, newD) 64 | if err != nil { 65 | glog.Errorf("Error during computing diff for (%q + %q) and (%q + %q): %v\n", 66 | c.oldSource, c.oldDiff, c.newSource, c.newDiff, err) 67 | return subcommands.ExitFailure 68 | } 69 | 70 | fmt.Println(result) 71 | return subcommands.ExitSuccess 72 | } 73 | -------------------------------------------------------------------------------- /test_examples/s2_a.diff: -------------------------------------------------------------------------------- 1 | diff -u source/file_1 source_a/file_1 2 | --- source/file_1 2020-07-16 14:34:21.000000000 +0000 3 | +++ source_a/file_1 2020-07-16 13:57:10.000000000 +0000 4 | @@ -7,7 +7,7 @@ 5 | Is handsome an declared at received in extended vicinity subjects. 6 | Into miss on he over been late pain an. 7 | Only week bore boy what fat case left use. 8 | -Match round scale now style far times. Your me past an much. 9 | +Match round scale now sex style far times. Your me past an much. 10 | 11 | Ought these are balls place mrs their times add she. 12 | Taken no great widow spoke of it small. 13 | @@ -33,4 +33,13 @@ 14 | Window admire matter praise you bed whence. 15 | Delivered ye sportsmen zealously arranging frankness estimable as. 16 | Nay any article enabled musical shyness yet sixteen yet blushes. 17 | -Entire its the did figure wonder off. 18 | \ No newline at end of file 19 | +Entire its the did figure wonder off. 20 | + 21 | +Add you viewing ten equally believe put. 22 | +Separate families my on drawings do oh offended strictly elegance. 23 | +Perceive jointure be mistress by jennings properly. 24 | +An admiration at he discovered difficulty continuing. 25 | +We in building removing possible suitable friendly on. 26 | +Nay middleton him admitting consulted and behaviour son household. 27 | +Recurred advanced he oh together entrance speedily suitable. 28 | +Ready tried gay state fat could boy its among shall. 29 | diff -u source/file_2 source_a/file_2 30 | --- source/file_2 2020-07-16 13:54:56.000000000 +0000 31 | +++ source_a/file_2 2020-07-16 13:56:47.000000000 +0000 32 | @@ -1,41 +1,40 @@ 33 | Text generated by www.randomtextgenerator.com 34 | 35 | -Affronting everything discretion men now own did. 36 | -Still round match we to. 37 | +Affronting everything discretion men now own did. 38 | +Still round match we to. 39 | +Very his are come man walk one next. 40 | +Outward general passage another as it. 41 | Frankness pronounce daughters remainder extensive has but. 42 | Happiness cordially one determine concluded fat. 43 | Plenty season beyond by hardly giving of. 44 | Consulted or acuteness dejection an smallness if. 45 | -Outward general passage another as it. 46 | -Very his are come man walk one next. 47 | Delighted prevailed supported too not remainder perpetual who furnished. 48 | -Nay affronting bed projection compliment instrument. 49 | +Nay affronting bed projection compliment instrument. 50 | 51 | -Old unsatiable our now but considered travelling impression. 52 | -In excuse hardly summer in basket misery. 53 | -By rent an part need. 54 | -At wrong of of water those linen. 55 | -Needed oppose seemed how all. 56 | -Very mrs shed shew gave you. 57 | -Oh shutters do removing reserved wandered an. 58 | -But described questions for recommend advantage belonging estimable had. 59 | -Pianoforte reasonable as so am inhabiting. 60 | -Chatty design remark and his abroad figure but its. 61 | +Old unsatiable our now but considered travelling impression. 62 | +In excuse hardly summer in basket misery. 63 | +By rent an part need. 64 | +At wrong of of water those linen. 65 | +Needed oppose seemed how all. 66 | +Very mrs shed shew gave you. 67 | +Oh shutters do removing reserved wandered an. 68 | +But described questions for recommend advantage belonging estimable had. 69 | +Pianoforte reasonable as so am inhabiting. 70 | +Chatty design remark and his abroad figure but its. 71 | 72 | -Dependent certainty off discovery him his tolerably offending. 73 | -Ham for attention remainder sometimes additions recommend fat our. 74 | -Direction has strangers now believing. 75 | -Respect enjoyed gay far exposed parlors towards. 76 | -Enjoyment use tolerably dependent listening men. 77 | +Dependent certainty off discovery him his tolerably offending. 78 | +Ham for attention remainder sometimes additions recommend fat our. 79 | +Direction has strangers now believing. 80 | No peculiar in handsome together unlocked do by. 81 | +Enjoyment use tolerably dependent listening men. 82 | Article concern joy anxious did picture sir her. 83 | Although desirous not recurred disposed off shy you numerous securing. 84 | 85 | -Her old collecting she considered discovered. 86 | -So at parties he warrant oh staying. 87 | +Her old collecting she considered discovered. 88 | Square new horses and put better end. 89 | Sincerity collected happiness do is contented. 90 | Sigh ever way now many. 91 | +So at parties he warrant oh staying. 92 | Alteration you any nor unsatiable diminution reasonable companions shy partiality. 93 | -Leaf by left deal mile oh if easy. 94 | -Added woman first get led joy not early jokes. 95 | +Leaf by left deal mile oh if easy. 96 | +Added woman first get led joy not early jokes. 97 | -------------------------------------------------------------------------------- /test_examples/s2_b.diff: -------------------------------------------------------------------------------- 1 | diff -u source/file_1 source_b/file_1 2 | --- source/file_1 2020-07-16 14:34:21.000000000 +0000 3 | +++ source_b/file_1 2020-07-16 14:34:21.000000000 +0000 4 | @@ -4,16 +4,10 @@ 5 | Collected instantly remaining up certainly to necessary as. 6 | Over walk dull into son boy door went new. 7 | At or happiness commanded daughters as. 8 | -Is handsome an declared at received in extended vicinity subjects. 9 | -Into miss on he over been late pain an. 10 | -Only week bore boy what fat case left use. 11 | Match round scale now style far times. Your me past an much. 12 | 13 | Ought these are balls place mrs their times add she. 14 | Taken no great widow spoke of it small. 15 | -Genius use except son esteem merely her limits. 16 | -Sons park by do make on. It do oh cottage offered cottage in written. 17 | -Especially of dissimilar up attachment themselves by interested boisterous. 18 | Linen mrs seems men table. 19 | Jennings dashwood to quitting marriage bachelor in. 20 | On as conviction in of appearance apartments boisterous. 21 | @@ -22,7 +16,8 @@ 22 | Justice joy manners boy met resolve produce. 23 | Bed head loud next plan rent had easy add him. 24 | As earnestly shameless elsewhere defective estimable fulfilled of. 25 | -Esteem my advice it an excuse enable. 26 | +Esteem my advice it an. 27 | +Ham for attention remainder sometimes additions recommend fat our. 28 | Few household abilities believing determine zealously his repulsive. 29 | To open draw dear be by side like. 30 | 31 | diff -u source/file_2 source_b/file_2 32 | --- source/file_2 2020-07-16 13:54:56.000000000 +0000 33 | +++ source_b/file_2 2020-07-16 13:55:43.000000000 +0000 34 | @@ -1,41 +1,39 @@ 35 | Text generated by www.randomtextgenerator.com 36 | 37 | -Affronting everything discretion men now own did. 38 | -Still round match we to. 39 | -Frankness pronounce daughters remainder extensive has but. 40 | +Affronting everything discretion men now own did. 41 | +Still round match we to. 42 | +Frankness pronounce daughters remainder extensive has but. 43 | Happiness cordially one determine concluded fat. 44 | -Plenty season beyond by hardly giving of. 45 | -Consulted or acuteness dejection an smallness if. 46 | -Outward general passage another as it. 47 | -Very his are come man walk one next. 48 | -Delighted prevailed supported too not remainder perpetual who furnished. 49 | -Nay affronting bed projection compliment instrument. 50 | +Plenty season beyond by hardly giving of. 51 | +Consulted or acuteness dejection an smallness if. 52 | +Outward general passage another as it. 53 | +Very his are come man walk one next. 54 | +Delighted prevailed supported too not remainder perpetual who furnished. 55 | +Nay affronting bed projection compliment instrument. 56 | 57 | -Old unsatiable our now but considered travelling impression. 58 | -In excuse hardly summer in basket misery. 59 | +Old unsatiable our now but considered travelling impression. 60 | +In excuse hardly summer in basket misery. 61 | By rent an part need. 62 | -At wrong of of water those linen. 63 | -Needed oppose seemed how all. 64 | -Very mrs shed shew gave you. 65 | -Oh shutters do removing reserved wandered an. 66 | -But described questions for recommend advantage belonging estimable had. 67 | -Pianoforte reasonable as so am inhabiting. 68 | -Chatty design remark and his abroad figure but its. 69 | +Her old collecting she considered discovered. 70 | +At wrong of of water those linen. 71 | +Needed oppose seemed how all. 72 | +Very mrs shed shew gave you. 73 | +Oh shutters do removing reserved wandered an. 74 | +But described questions for recommend advantage belonging estimable had. 75 | +Pianoforte reasonable as so am inhabiting. 76 | +Chatty design remark and his abroad figure but its. 77 | 78 | -Dependent certainty off discovery him his tolerably offending. 79 | -Ham for attention remainder sometimes additions recommend fat our. 80 | -Direction has strangers now believing. 81 | -Respect enjoyed gay far exposed parlors towards. 82 | -Enjoyment use tolerably dependent listening men. 83 | -No peculiar in handsome together unlocked do by. 84 | -Article concern joy anxious did picture sir her. 85 | -Although desirous not recurred disposed off shy you numerous securing. 86 | +Dependent certainty off discovery him his tolerably offending. 87 | +Ham for attention remainder sometimes additions recommend fat our. 88 | +Direction has believing. 89 | +Respect enjoyed gay far exposed parlors towards. 90 | +Enjoyment use tolerably dependent listening men. 91 | +No peculiar in do by. 92 | +Pianoforte reasonable as so am inhabiting. 93 | +Although desirous not recurred disposed off shy you numerous securing. 94 | 95 | Her old collecting she considered discovered. 96 | -So at parties he warrant oh staying. 97 | -Square new horses and put better end. 98 | -Sincerity collected happiness do is contented. 99 | -Sigh ever way now many. 100 | -Alteration you any nor unsatiable diminution reasonable companions shy partiality. 101 | -Leaf by left deal mile oh if easy. 102 | -Added woman first get led joy not early jokes. 103 | +Sigh ever way now many. 104 | +Alteration you any nor unsatiable diminution reasonable companions shy partiality. 105 | +Leaf by left deal mile oh if easy. 106 | +Added woman first get led joy not early jokes. 107 | -------------------------------------------------------------------------------- /test_examples/s2_a_b.diff: -------------------------------------------------------------------------------- 1 | --- source_a/file_1 2020-07-16 13:57:10.000000000 +0000 2 | +++ source_b/file_1 2020-07-16 14:34:21.000000000 +0000 3 | @@ -4,16 +4,10 @@ 4 | Collected instantly remaining up certainly to necessary as. 5 | Over walk dull into son boy door went new. 6 | At or happiness commanded daughters as. 7 | -Is handsome an declared at received in extended vicinity subjects. 8 | -Into miss on he over been late pain an. 9 | -Only week bore boy what fat case left use. 10 | +Match round scale now style far times. Your me past an much. 11 | -Match round scale now sex style far times. Your me past an much. 12 | 13 | Ought these are balls place mrs their times add she. 14 | Taken no great widow spoke of it small. 15 | -Genius use except son esteem merely her limits. 16 | -Sons park by do make on. It do oh cottage offered cottage in written. 17 | -Especially of dissimilar up attachment themselves by interested boisterous. 18 | Linen mrs seems men table. 19 | Jennings dashwood to quitting marriage bachelor in. 20 | On as conviction in of appearance apartments boisterous. 21 | @@ -22,7 +16,8 @@ 22 | Justice joy manners boy met resolve produce. 23 | Bed head loud next plan rent had easy add him. 24 | As earnestly shameless elsewhere defective estimable fulfilled of. 25 | -Esteem my advice it an excuse enable. 26 | +Esteem my advice it an. 27 | +Ham for attention remainder sometimes additions recommend fat our. 28 | Few household abilities believing determine zealously his repulsive. 29 | To open draw dear be by side like. 30 | 31 | @@ -33,4 +33,13 @@ 32 | Window admire matter praise you bed whence. 33 | Delivered ye sportsmen zealously arranging frankness estimable as. 34 | Nay any article enabled musical shyness yet sixteen yet blushes. 35 | +Entire its the did figure wonder off. 36 | \ No newline at end of file 37 | -Entire its the did figure wonder off. 38 | - 39 | -Add you viewing ten equally believe put. 40 | -Separate families my on drawings do oh offended strictly elegance. 41 | -Perceive jointure be mistress by jennings properly. 42 | -An admiration at he discovered difficulty continuing. 43 | -We in building removing possible suitable friendly on. 44 | -Nay middleton him admitting consulted and behaviour son household. 45 | -Recurred advanced he oh together entrance speedily suitable. 46 | -Ready tried gay state fat could boy its among shall. 47 | 48 | --- source_a/file_2 2020-07-16 13:56:47.000000000 +0000 49 | +++ source_b/file_2 2020-07-16 13:55:43.000000000 +0000 50 | @@ -1,40 +1,39 @@ 51 | Text generated by www.randomtextgenerator.com 52 | 53 | -Affronting everything discretion men now own did. 54 | -Still round match we to. 55 | -Very his are come man walk one next. 56 | -Outward general passage another as it. 57 | -Frankness pronounce daughters remainder extensive has but. 58 | +Affronting everything discretion men now own did. 59 | +Still round match we to. 60 | +Frankness pronounce daughters remainder extensive has but. 61 | Happiness cordially one determine concluded fat. 62 | -Plenty season beyond by hardly giving of. 63 | -Consulted or acuteness dejection an smallness if. 64 | -Delighted prevailed supported too not remainder perpetual who furnished. 65 | +Plenty season beyond by hardly giving of. 66 | +Consulted or acuteness dejection an smallness if. 67 | +Outward general passage another as it. 68 | +Very his are come man walk one next. 69 | +Delighted prevailed supported too not remainder perpetual who furnished. 70 | Nay affronting bed projection compliment instrument. 71 | 72 | +Old unsatiable our now but considered travelling impression. 73 | +In excuse hardly summer in basket misery. 74 | +By rent an part need. 75 | -Old unsatiable our now but considered travelling impression. 76 | -In excuse hardly summer in basket misery. 77 | -By rent an part need. 78 | +Her old collecting she considered discovered. 79 | At wrong of of water those linen. 80 | Needed oppose seemed how all. 81 | Very mrs shed shew gave you. 82 | Oh shutters do removing reserved wandered an. 83 | But described questions for recommend advantage belonging estimable had. 84 | Pianoforte reasonable as so am inhabiting. 85 | Chatty design remark and his abroad figure but its. 86 | 87 | -Dependent certainty off discovery him his tolerably offending. 88 | -Ham for attention remainder sometimes additions recommend fat our. 89 | -Direction has strangers now believing. 90 | -No peculiar in handsome together unlocked do by. 91 | -Enjoyment use tolerably dependent listening men. 92 | -Article concern joy anxious did picture sir her. 93 | -Although desirous not recurred disposed off shy you numerous securing. 94 | +Dependent certainty off discovery him his tolerably offending. 95 | +Ham for attention remainder sometimes additions recommend fat our. 96 | +Direction has believing. 97 | +Respect enjoyed gay far exposed parlors towards. 98 | +Enjoyment use tolerably dependent listening men. 99 | +No peculiar in do by. 100 | +Pianoforte reasonable as so am inhabiting. 101 | +Although desirous not recurred disposed off shy you numerous securing. 102 | 103 | +Her old collecting she considered discovered. 104 | -Her old collecting she considered discovered. 105 | -Square new horses and put better end. 106 | -Sincerity collected happiness do is contented. 107 | -Sigh ever way now many. 108 | -So at parties he warrant oh staying. 109 | -Alteration you any nor unsatiable diminution reasonable companions shy partiality. 110 | +Sigh ever way now many. 111 | +Alteration you any nor unsatiable diminution reasonable companions shy partiality. 112 | Leaf by left deal mile oh if easy. 113 | Added woman first get led joy not early jokes. 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-patchutils [![GoDoc](https://godoc.org/github.com/google/go-patchutils?status.svg)](https://godoc.org/github.com/google/go-patchutils) [![Go Report Card](https://goreportcard.com/badge/github.com/google/go-patchutils)](https://goreportcard.com/report/github.com/google/go-patchutils) 2 | Package patchutils provides tools to compute the diff between source and diff files. 3 | 4 | Works with diff files in [unified format](http://gnu.org/software/diffutils/manual/html_node/Unified-Format.html). 5 | 6 | ### Table of contents 7 | * [Design](https://github.com/google/go-patchutils#design) 8 | * [Interdiff mode](https://github.com/google/go-patchutils#interdiff-mode) 9 | * [Mixed mode](https://github.com/google/go-patchutils#mixed-mode) 10 | * [Installation](https://github.com/google/go-patchutils#installation) 11 | * [Usage](https://github.com/google/go-patchutils#usage) 12 | * [API](https://github.com/google/go-patchutils#api) 13 | * [CLI tool](https://github.com/google/go-patchutils#cli-tool) 14 | 15 | ## Design 16 | 17 | ### Interdiff mode 18 | 19 | 20 | **InterDiff** computes the diff of a source file patched with **oldDiff** 21 | and the same source file patched with **newDiff**, without access to the source. 22 | 23 |
24 | Example 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 54 | 74 | 75 | 76 | 77 | 78 | 79 | 101 | 102 |
oldDiffnewDiff
33 | 34 | ```diff 35 | @@ -1,10 +1,13 @@ 36 | 80 days around the world. 37 | -We’ll find a pot of gold 38 | +You’ll find a pot of gold 39 | just sitting where the rainbow’s ending. 40 | 41 | +Top Cat! The most effectual Top Cat! 42 | +Who’s intellectual close friends get to call, 43 | +providing it’s with dignity. 44 | +The indisputable leader of the gang. 45 | 46 | Time — we’ll fight against the time, 47 | and we’ll fly on the white wings of the wind. 48 | 49 | -80 days around the world, 50 | -no we won’t say a word before 51 | the ship is really back. 52 | ``` 53 | 55 | 56 | ```diff 57 | @@ -2,9 +2,13 @@ 58 | We’ll find a pot of gold 59 | just sitting where the rainbow’s ending. 60 | 61 | +There’s a voice that keeps on calling me. 62 | +Who’s intellectual close friends get to call, 63 | +providing it’s with dignity. 64 | +The indisputable leader of the gang. 65 | 66 | Time — we’ll fight against the time, 67 | and we’ll fly on the white wings of the wind. 68 | 69 | -80 days around the world, 70 | no we won’t say a word before 71 | the ship is really back. 72 | ``` 73 |
result
80 | 81 | ```diff 82 | @@ -1,8 +1,8 @@ 83 | 80 days around the world. 84 | +We’ll find a pot of gold 85 | -You’ll find a pot of gold 86 | just sitting where the rainbow’s ending. 87 | 88 | -Top Cat! The most effectual Top Cat! 89 | +There’s a voice that keeps on calling me. 90 | Who’s intellectual close friends get to call, 91 | providing it’s with dignity. 92 | The indisputable leader of the gang. 93 | 94 | Time — we’ll fight against the time, 95 | and we’ll fly on the white wings of the wind. 96 | 97 | +no we won’t say a word before 98 | the ship is really back. 99 | ``` 100 |
103 | 104 |
105 | 106 | ### Mixed mode 107 | 108 | 109 | **MixedMode** computes the diff of an **oldSource** patched with **oldDiff** and 110 | **newSource** patched with **newDiff**. 111 | 112 |
113 | Example 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 139 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 176 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 214 | 232 | 233 | 234 | 235 | 236 | 237 | 260 | 261 |
oldSourcenewSource
122 | 123 | ``` 124 | 80 days around the world. 125 | You’ll find a pot of gold 126 | just sitting where the rainbow’s ending. 127 | 128 | Top Cat! The most effectual Top Cat! 129 | Who’s intellectual close friends get to call, 130 | providing it’s with dignity. 131 | The indisputable leader of the gang. 132 | 133 | Time — we’ll fight against the time, 134 | and we’ll fly on the white wings of the wind. 135 | 136 | the ship is really back. 137 | ``` 138 | 140 | 141 | ``` 142 | 80 days around the world. 143 | We’ll find a pot of gold 144 | just sitting where the rainbow’s ending. 145 | 146 | There’s a voice that keeps on calling me. 147 | Who’s intellectual close friends get to call, 148 | providing it’s with dignity. 149 | The indisputable leader of the gang. 150 | 151 | Time — we’ll fight against the time, 152 | and we’ll fly on the white wings of the wind. 153 | 154 | no we won’t say a word before 155 | the ship is really back. 156 | ``` 157 |
oldDiffnewDiff
165 | 166 | ```diff 167 | @@ -4,6 +4,7 @@ 168 | 169 | Top Cat! The most effectual Top Cat! 170 | Who’s intellectual close friends get to call, 171 | +Round, round, all around the world. 172 | providing it’s with dignity. 173 | The indisputable leader of the gang. 174 | ``` 175 | 177 | 178 | ```diff 179 | @@ -5,7 +5,6 @@ 180 | There’s a voice that keeps on calling me. 181 | Who’s intellectual close friends get to call, 182 | providing it’s with dignity. 183 | -The indisputable leader of the gang. 184 | 185 | Time — we’ll fight against the time, 186 | and we’ll fly on the white wings of the wind. 187 | ``` 188 |
oldSource + oldDiffnewSource + newDiff
196 | 197 | ```diff 198 | 80 days around the world. 199 | You’ll find a pot of gold 200 | just sitting where the rainbow’s ending. 201 | 202 | Top Cat! The most effectual Top Cat! 203 | Who’s intellectual close friends get to call, 204 | Round, round, all around the world. 205 | providing it’s with dignity. 206 | The indisputable leader of the gang. 207 | 208 | Time — we’ll fight against the time, 209 | and we’ll fly on the white wings of the wind. 210 | 211 | the ship is really back. 212 | ``` 213 | 215 | 216 | ``` 217 | 80 days around the world. 218 | We’ll find a pot of gold 219 | just sitting where the rainbow’s ending. 220 | 221 | There’s a voice that keeps on calling me. 222 | Who’s intellectual close friends get to call, 223 | providing it’s with dignity. 224 | 225 | Time — we’ll fight against the time, 226 | and we’ll fly on the white wings of the wind. 227 | 228 | no we won’t say a word before 229 | the ship is really back. 230 | ``` 231 |
result
238 | 239 | ```diff 240 | @@ -1,14 +1,13 @@ 241 | 80 days around the world. 242 | -You’ll find a pot of gold 243 | +We’ll find a pot of gold 244 | just sitting where the rainbow’s ending. 245 | 246 | -Top Cat! The most effectual Top Cat! 247 | +There’s a voice that keeps on calling me. 248 | Who’s intellectual close friends get to call, 249 | -Round, round, all around the world. 250 | providing it’s with dignity. 251 | -The indisputable leader of the gang. 252 | 253 | Time — we’ll fight against the time, 254 | and we’ll fly on the white wings of the wind. 255 | 256 | +no we won’t say a word before 257 | the ship is really back. 258 | ``` 259 |
262 |
263 | 264 | ## Installation 265 | 266 | ```shell 267 | go get -u github.com/google/go-patchutils 268 | ``` 269 | 270 | ## Usage 271 | 272 | ### API 273 | [Godoc](https://godoc.org/github.com/google/go-patchutils) is available. 274 | 275 | ### CLI tool 276 | 277 | Build CLI tool 278 | ```shell 279 | cd cli 280 | go build 281 | ``` 282 | 283 | **Interdiff mode** 284 | ```shell 285 | ./cli interdiff -olddiff= -newdiff= 286 | ``` 287 | 288 | **Mixed mode** 289 | ```shell 290 | ./cli mixed -oldsource= -olddiff= 291 | -newsource= -newdiff= 292 | ``` 293 | 294 | -------------------------------------------------------------------------------- /patchutils_test.go: -------------------------------------------------------------------------------- 1 | package patchutils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/sourcegraph/go-diff/diff" 14 | ) 15 | 16 | var interDiffFileTests = []struct { 17 | diffAFile string 18 | diffBFile string 19 | resultFile string 20 | wantErr error 21 | }{ 22 | { 23 | diffAFile: "s1_a.diff", 24 | diffBFile: "s1_b.diff", 25 | resultFile: "s1_a_b.diff", 26 | wantErr: nil, 27 | }, 28 | { 29 | diffAFile: "s2_a.diff", 30 | diffBFile: "s2_b.diff", 31 | resultFile: "s2_a_b.diff", 32 | wantErr: nil, 33 | }, 34 | { 35 | diffAFile: "s3_a.diff", 36 | diffBFile: "s3_b.diff", 37 | resultFile: "s3_a_b.diff", 38 | wantErr: nil, 39 | }, 40 | { 41 | diffAFile: "f1_a_wrong_origin.diff", 42 | diffBFile: "f1_b.diff", 43 | resultFile: "f1_a_c.diff", 44 | wantErr: ErrContentMismatch, 45 | }, 46 | { 47 | // Not a diff file 48 | diffAFile: "source_1/file_1.txt", 49 | diffBFile: "s2_b.diff", 50 | resultFile: "s2_a_b.diff", 51 | wantErr: ErrEmptyDiffFile, 52 | }, 53 | { 54 | diffAFile: "s2_a.diff", 55 | // Not a diff file 56 | diffBFile: "source_1/file_2.txt", 57 | resultFile: "s2_a_b.diff", 58 | wantErr: ErrEmptyDiffFile, 59 | }, 60 | { 61 | // Empty diff file 62 | diffAFile: "empty.diff", 63 | diffBFile: "s2_b.diff", 64 | resultFile: "s2_a_b.diff", 65 | wantErr: ErrEmptyDiffFile, 66 | }, 67 | // Contains added/deleted files 68 | { 69 | diffAFile: "s1_a_c.diff", 70 | diffBFile: "s1_a_d.diff", 71 | resultFile: "s1_c_d.diff", 72 | }, 73 | } 74 | 75 | var applyDiffFileTests = []struct { 76 | sourceFile string 77 | diffFile string 78 | resultFile string 79 | wantErr error 80 | }{ 81 | { 82 | sourceFile: "source_1/file_1.txt", 83 | diffFile: "f1_a.diff", 84 | resultFile: "source_1_a/file_1.txt", 85 | wantErr: nil, 86 | }, 87 | { 88 | sourceFile: "source_1_a/file_1.txt", 89 | diffFile: "f1_a_c.diff", 90 | resultFile: "source_1_c/file_1.txt", 91 | wantErr: nil, 92 | }, 93 | { 94 | sourceFile: "source_1/file_1.txt", 95 | diffFile: "f1_b.diff", 96 | resultFile: "source_1_b/file_1.txt", 97 | wantErr: nil, 98 | }, 99 | { 100 | sourceFile: "source_1_b/file_1.txt", 101 | diffFile: "f1_b_c.diff", 102 | resultFile: "source_1_c/file_1.txt", 103 | wantErr: nil, 104 | }, 105 | { 106 | sourceFile: "source_1/file_2.txt", 107 | diffFile: "f2_a.diff", 108 | resultFile: "source_1_a/file_2.txt", 109 | wantErr: nil, 110 | }, 111 | { 112 | sourceFile: "source_1_a/file_2.txt", 113 | diffFile: "f2_a_c.diff", 114 | resultFile: "source_1_c/file_2.txt", 115 | wantErr: nil, 116 | }, 117 | { 118 | sourceFile: "source_1/file_2.txt", 119 | diffFile: "f2_b.diff", 120 | resultFile: "source_1_b/file_2.txt", 121 | wantErr: nil, 122 | }, 123 | { 124 | sourceFile: "source_1_b/file_2.txt", 125 | diffFile: "f2_b_c.diff", 126 | resultFile: "source_1_c/file_2.txt", 127 | wantErr: nil, 128 | }, 129 | // sourceFile and diffFile have different origin content. 130 | { 131 | sourceFile: "source_1/file_1.txt", 132 | diffFile: "f1_a_wrong_origin.diff", 133 | resultFile: "source_1_a/file_1.txt", 134 | wantErr: ErrContentMismatch, 135 | }, 136 | } 137 | 138 | var mixedModeFileTests = []struct { 139 | oldSourceFile string 140 | oldDiffFile string 141 | newSourceFile string 142 | newDiffFile string 143 | resultFile string 144 | }{ 145 | { 146 | oldSourceFile: "source_1/file_1.txt", 147 | oldDiffFile: "f1_a.diff", 148 | newSourceFile: "source_1_b/file_1.txt", 149 | newDiffFile: "f1_b_c.diff", 150 | resultFile: "f1_a_c.diff", 151 | }, 152 | { 153 | oldSourceFile: "source_1/file_2.txt", 154 | oldDiffFile: "f2_a.diff", 155 | newSourceFile: "source_1_b/file_2.txt", 156 | newDiffFile: "f2_b_c.diff", 157 | resultFile: "f2_a_c.diff", 158 | }, 159 | } 160 | 161 | var mixedModePathFileTests = []struct { 162 | oldSource string 163 | oldDiffFile string 164 | newSource string 165 | newDiffFile string 166 | resultFile string 167 | wantErr bool 168 | }{ 169 | // Files 170 | { 171 | oldSource: "source_1/file_1.txt", 172 | oldDiffFile: "f1_a.diff", 173 | newSource: "source_1_b/file_1.txt", 174 | newDiffFile: "f1_b_c.diff", 175 | resultFile: "f1_a_c.diff", 176 | wantErr: false, 177 | }, 178 | { 179 | oldSource: "source_1/file_2.txt", 180 | oldDiffFile: "f2_a.diff", 181 | newSource: "source_1_b/file_2.txt", 182 | newDiffFile: "f2_b_c.diff", 183 | resultFile: "f2_a_c.diff", 184 | wantErr: false, 185 | }, 186 | // Directories 187 | { 188 | oldSource: "source_1", 189 | oldDiffFile: "s1_a.diff", 190 | newSource: "source_1_b", 191 | newDiffFile: "s1_b_c.diff", 192 | resultFile: "s1_a_c.diff", 193 | wantErr: false, 194 | }, 195 | // Sources have different modes (file & directory) 196 | { 197 | oldSource: "source_1", 198 | oldDiffFile: "s1_a.diff", 199 | newSource: "source_1_/file_1.txt", 200 | newDiffFile: "f1_a.diff", 201 | resultFile: "s1_a_c.diff", 202 | wantErr: true, 203 | }, 204 | // Contains added and unchanged files 205 | { 206 | oldSource: "source_1", 207 | oldDiffFile: "s1_a.diff", 208 | newSource: "source_1_c", 209 | newDiffFile: "s1_c_d.diff", 210 | resultFile: "s1_a_d.diff", 211 | wantErr: false, 212 | }, 213 | } 214 | 215 | // Reference: https://www.programming-books.io/essential/go/normalize-newlines-1d3abcf6f17c4186bb9617fa14074e48 216 | // NormalizeNewlines normalizes \r\n (windows) and \r (mac) 217 | // into \n (unix) 218 | func normalizeNewlines(d []byte) []byte { 219 | // replace CR LF \r\n (windows) with LF \n (unix) 220 | d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1) 221 | // replace CF \r (mac) with LF \n (unix) 222 | d = bytes.Replace(d, []byte{13}, []byte{10}, -1) 223 | return d 224 | } 225 | 226 | func init() { 227 | time.Local = time.UTC 228 | testFilesDir := "test_examples" 229 | if err := os.Chdir(testFilesDir); err != nil { 230 | fmt.Println(fmt.Errorf("failed change dir to: %q: %w", testFilesDir, err)) 231 | } 232 | } 233 | 234 | func TestInterDiffMode(t *testing.T) { 235 | for _, tt := range interDiffFileTests { 236 | t.Run(tt.resultFile, func(t *testing.T) { 237 | var fileA, errA = os.Open(tt.diffAFile) 238 | var fileB, errB = os.Open(tt.diffBFile) 239 | 240 | if errA != nil { 241 | t.Errorf("Error in opening %s file.", tt.diffAFile) 242 | } 243 | 244 | if errB != nil { 245 | t.Errorf("Error in opening %s file.", tt.diffBFile) 246 | } 247 | 248 | correctResult, err := ioutil.ReadFile(tt.resultFile) 249 | 250 | if err != nil { 251 | t.Error(err) 252 | } 253 | 254 | var readerA io.Reader = fileA 255 | var readerB io.Reader = fileB 256 | 257 | currentResult, err := InterDiff(readerA, readerB) 258 | 259 | if (tt.wantErr == nil) && (err == nil) { 260 | if !bytes.Equal(normalizeNewlines([]byte(currentResult)), normalizeNewlines(correctResult)) { 261 | t.Errorf("File contents mismatch for %s.\nExpected:\n%s\nGot:\n%s\n", 262 | tt.resultFile, correctResult, currentResult) 263 | } 264 | } else if !errors.Is(err, tt.wantErr) { 265 | t.Errorf("Interdiff mode for %q: got error %v; want error %v", tt.resultFile, err, tt.wantErr) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | func TestApplyDiff(t *testing.T) { 272 | for _, tt := range applyDiffFileTests { 273 | t.Run(tt.resultFile, func(t *testing.T) { 274 | source, err := ioutil.ReadFile(tt.sourceFile) 275 | if err != nil { 276 | t.Errorf("Error reading sourceFile %q", tt.sourceFile) 277 | } 278 | 279 | diffFile, err := os.Open(tt.diffFile) 280 | if err != nil { 281 | t.Errorf("Error opening diffFile %q", tt.diffFile) 282 | } 283 | 284 | d, err := diff.NewFileDiffReader(diffFile).Read() 285 | if err != nil { 286 | t.Errorf("Error parsing diffFile %q", tt.diffFile) 287 | } 288 | 289 | correctResult, err := ioutil.ReadFile(tt.resultFile) 290 | if err != nil { 291 | t.Errorf("Error reading resultFile %q", tt.resultFile) 292 | } 293 | 294 | currentResult, err := applyDiff(string(source), d) 295 | if (tt.wantErr == nil) && (err == nil) { 296 | if !bytes.Equal(normalizeNewlines([]byte(currentResult)), normalizeNewlines(correctResult)) { 297 | t.Errorf("File contents mismatch for %s.\nGot:\n%s\nWant:\n%s\n", 298 | tt.resultFile, currentResult, correctResult) 299 | } 300 | } else if !errors.Is(err, tt.wantErr) { 301 | t.Errorf("Applying diff for %q: got error %v; want error %v", tt.resultFile, err, tt.wantErr) 302 | } 303 | }) 304 | } 305 | } 306 | 307 | func TestMixedMode(t *testing.T) { 308 | for _, tt := range mixedModeFileTests { 309 | t.Run(tt.resultFile, func(t *testing.T) { 310 | oldSource, err := os.Open(tt.oldSourceFile) 311 | if err != nil { 312 | t.Errorf("Error opening oldSourceFile %q", tt.oldSourceFile) 313 | } 314 | 315 | newSource, err := os.Open(tt.newSourceFile) 316 | if err != nil { 317 | t.Errorf("Error opening newSourceFile %q", tt.newSourceFile) 318 | } 319 | 320 | oldDiffFile, err := os.Open(tt.oldDiffFile) 321 | if err != nil { 322 | t.Errorf("Error opening oldDiffFile %q", tt.oldDiffFile) 323 | } 324 | 325 | oldD, err := diff.NewFileDiffReader(oldDiffFile).Read() 326 | if err != nil { 327 | t.Errorf("Error parsing oldDiffFile %q", tt.oldDiffFile) 328 | } 329 | 330 | newDiffFile, err := os.Open(tt.newDiffFile) 331 | if err != nil { 332 | t.Errorf("Error opening newDiffFile %q", tt.newDiffFile) 333 | } 334 | 335 | newD, err := diff.NewFileDiffReader(newDiffFile).Read() 336 | if err != nil { 337 | t.Errorf("Error parsing newDiffFile %q", tt.newDiffFile) 338 | } 339 | 340 | correctResult, err := ioutil.ReadFile(tt.resultFile) 341 | if err != nil { 342 | t.Errorf("Error reading resultFile %q", tt.resultFile) 343 | } 344 | 345 | currentResult, err := mixedMode(oldSource, newSource, oldD, newD) 346 | 347 | if err != nil { 348 | t.Errorf("Mixed mode for %q: got error %v; want error nil", tt.resultFile, err) 349 | } 350 | 351 | if !bytes.Equal(normalizeNewlines([]byte(currentResult)), normalizeNewlines(correctResult)) { 352 | t.Errorf("File contents mismatch for %s.\nGot:\n%s\nWant:\n%s\n", 353 | tt.resultFile, currentResult, correctResult) 354 | } 355 | }) 356 | } 357 | } 358 | 359 | func TestMixedModeFile(t *testing.T) { 360 | for _, tt := range mixedModeFileTests { 361 | t.Run(tt.resultFile, func(t *testing.T) { 362 | oldSource, err := os.Open(tt.oldSourceFile) 363 | if err != nil { 364 | t.Errorf("Error opening oldSourceFile %q", tt.oldSourceFile) 365 | } 366 | 367 | newSource, err := os.Open(tt.newSourceFile) 368 | if err != nil { 369 | t.Errorf("Error opening newSourceFile %q", tt.newSourceFile) 370 | } 371 | 372 | oldDiffFile, err := os.Open(tt.oldDiffFile) 373 | if err != nil { 374 | t.Errorf("Error opening oldDiffFile %q", tt.oldDiffFile) 375 | } 376 | 377 | newDiffFile, err := os.Open(tt.newDiffFile) 378 | if err != nil { 379 | t.Errorf("Error opening newDiffFile %q", tt.newDiffFile) 380 | } 381 | 382 | correctResult, err := ioutil.ReadFile(tt.resultFile) 383 | if err != nil { 384 | t.Errorf("Error reading resultFile %q", tt.resultFile) 385 | } 386 | 387 | currentResult, err := MixedModeFile(oldSource, newSource, oldDiffFile, newDiffFile) 388 | 389 | if err != nil { 390 | t.Errorf("Mixed mode for %q: got error %v; want error nil", tt.resultFile, err) 391 | } 392 | 393 | if !bytes.Equal(normalizeNewlines([]byte(currentResult)), normalizeNewlines(correctResult)) { 394 | t.Errorf("File contents mismatch for %s.\nGot:\n%s\nWant:\n%s\n", 395 | tt.resultFile, currentResult, correctResult) 396 | } 397 | }) 398 | } 399 | } 400 | 401 | func TestMixedModePath(t *testing.T) { 402 | for _, tt := range mixedModePathFileTests { 403 | t.Run(tt.resultFile, func(t *testing.T) { 404 | oldDiffFile, err := os.Open(tt.oldDiffFile) 405 | if err != nil { 406 | t.Errorf("Error opening oldDiffFile %q", tt.oldDiffFile) 407 | } 408 | 409 | newDiffFile, err := os.Open(tt.newDiffFile) 410 | if err != nil { 411 | t.Errorf("Error opening newDiffFile %q", tt.newDiffFile) 412 | } 413 | 414 | correctResult, err := ioutil.ReadFile(tt.resultFile) 415 | if err != nil { 416 | t.Errorf("Error reading resultFile %q", tt.resultFile) 417 | } 418 | 419 | currentResult, err := MixedModePath(tt.oldSource, tt.newSource, oldDiffFile, newDiffFile) 420 | 421 | if tt.wantErr && err == nil { 422 | t.Errorf("MixedModePath for %q: got error nil; want error non-nil", tt.resultFile) 423 | } else if !tt.wantErr { 424 | if err != nil { 425 | t.Errorf("MixedModePath for %q: got error %v; want error nil", tt.resultFile, err) 426 | } 427 | 428 | if !bytes.Equal(normalizeNewlines([]byte(currentResult)), normalizeNewlines(correctResult)) { 429 | t.Errorf("File contents mismatch for %s.\nGot:\n%s\nWant:\n%s\n", 430 | tt.resultFile, currentResult, correctResult) 431 | } 432 | } 433 | }) 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /patchutils.go: -------------------------------------------------------------------------------- 1 | // Package patchutils provides tools to compute the diff between source and diff files. 2 | package patchutils 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "sort" 12 | "strings" 13 | "sync" 14 | 15 | dbd "github.com/kylelemons/godebug/diff" 16 | "github.com/sourcegraph/go-diff/diff" 17 | "golang.org/x/sync/errgroup" 18 | ) 19 | 20 | // InterDiff computes the diff of a source file patched with oldDiff 21 | // and the same source file patched with newDiff. 22 | // oldDiff and newDiff should be in unified format. 23 | func InterDiff(oldDiff, newDiff io.Reader) (string, error) { 24 | oldFileDiffs, err := diff.NewMultiFileDiffReader(oldDiff).ReadAllFiles() 25 | if err != nil { 26 | return "", fmt.Errorf("parsing oldDiff: %w", err) 27 | } 28 | if len(oldFileDiffs) == 0 { 29 | return "", fmt.Errorf("oldDiff: %w", ErrEmptyDiffFile) 30 | } 31 | 32 | newFileDiffs, err := diff.NewMultiFileDiffReader(newDiff).ReadAllFiles() 33 | if err != nil { 34 | return "", fmt.Errorf("parsing newDiff: %w", err) 35 | } 36 | if len(newFileDiffs) == 0 { 37 | return "", fmt.Errorf("newDiff: %w", ErrEmptyDiffFile) 38 | } 39 | 40 | resultFiles := make(map[string]string) 41 | // Iterate over files in FileDiff arrays 42 | i, j := 0, 0 43 | eg, _ := errgroup.WithContext(context.Background()) 44 | Loop: 45 | for i < len(oldFileDiffs) && j < len(newFileDiffs) { 46 | switch { 47 | case oldFileDiffs[i].OrigName == newFileDiffs[j].OrigName: 48 | switch { 49 | case oldFileDiffs[i].NewName == "" && newFileDiffs[j].NewName == "": 50 | // In both versions file has been added/deleted 51 | i++ 52 | j++ 53 | continue Loop 54 | case oldFileDiffs[i].NewName == "": 55 | // File was deleted in old version 56 | resultFiles[newFileDiffs[j].OrigName] = fmt.Sprintf("Only in %s: %s\n", filepath.Dir(newFileDiffs[j].NewName), 57 | filepath.Base(newFileDiffs[j].NewName)) 58 | case newFileDiffs[j].NewName == "": 59 | // File deleted in new version 60 | resultFiles[oldFileDiffs[i].OrigName] = fmt.Sprintf("Only in %s: %s\n", filepath.Dir(oldFileDiffs[i].NewName), 61 | filepath.Base(oldFileDiffs[i].NewName)) 62 | default: 63 | // interdiff of two versions 64 | var mu sync.Mutex 65 | i, j := i, j 66 | eg.Go(func() error { 67 | interFileDiff, err := interFileDiff(oldFileDiffs[i], newFileDiffs[j]) 68 | if err != nil { 69 | return fmt.Errorf("merging diffs for file %q: %w", oldFileDiffs[i].OrigName, err) 70 | } 71 | 72 | fileDiffContent, err := diff.PrintFileDiff(interFileDiff) 73 | if err != nil { 74 | return fmt.Errorf("printing merged diffs for file %q: %w", oldFileDiffs[i].OrigName, err) 75 | } 76 | 77 | mu.Lock() 78 | defer mu.Unlock() 79 | resultFiles[oldFileDiffs[i].OrigName] = string(fileDiffContent) 80 | return nil 81 | }) 82 | } 83 | i++ 84 | j++ 85 | case oldFileDiffs[i].OrigName < newFileDiffs[j].OrigName: 86 | // current file is only mentioned in oldDiff 87 | // determine if file has been added or just changed in only one version 88 | revertHunks(oldFileDiffs[i]) 89 | oldD, err := interPrintSingleFileDiff(oldFileDiffs[i]) 90 | if err != nil { 91 | return "", fmt.Errorf("printing oldDiff: %w", err) 92 | } 93 | resultFiles[oldFileDiffs[i].OrigName] = oldD 94 | i++ 95 | case oldFileDiffs[i].OrigName > newFileDiffs[j].OrigName: 96 | // current file is only mentioned in newDiff 97 | // determine if file has been added or just changed in only one version 98 | newD, err := interPrintSingleFileDiff(newFileDiffs[j]) 99 | if err != nil { 100 | return "", fmt.Errorf("printing newDiff: %w", err) 101 | } 102 | resultFiles[newFileDiffs[j].OrigName] = newD 103 | j++ 104 | } 105 | } 106 | 107 | // In case there are more oldFileDiffs, while newFileDiffs are run out 108 | for i < len(oldFileDiffs) { 109 | oldD, err := interPrintSingleFileDiff(oldFileDiffs[i]) 110 | if err != nil { 111 | return "", fmt.Errorf("printing oldDiff: %w", err) 112 | } 113 | resultFiles[oldFileDiffs[i].OrigName] = oldD 114 | i++ 115 | } 116 | 117 | // In case there are more newFileDiffs, while oldFileDiffs are run out 118 | for j < len(newFileDiffs) { 119 | newD, err := interPrintSingleFileDiff(newFileDiffs[j]) 120 | if err != nil { 121 | return "", fmt.Errorf("printing newDiff: %w", err) 122 | } 123 | resultFiles[newFileDiffs[j].OrigName] = newD 124 | j++ 125 | } 126 | 127 | if err := eg.Wait(); err != nil { 128 | return "", fmt.Errorf("wait all routines: %w", err) 129 | } 130 | 131 | // Add diff files to result in order 132 | var originalFilenames []string 133 | for f := range resultFiles { 134 | originalFilenames = append(originalFilenames, f) 135 | } 136 | sort.Strings(originalFilenames) 137 | var result string 138 | for _, k := range originalFilenames { 139 | result += resultFiles[k] 140 | } 141 | 142 | return result, nil 143 | } 144 | 145 | // mixedMode computes the diff of a oldSource file patched with oldDiff 146 | // and the newSource file patched with newDiff. 147 | // Check if files are added/deleted in old/new versions is skipped. 148 | func mixedMode(oldSource, newSource io.Reader, oldFileDiff, newFileDiff *diff.FileDiff) (string, error) { 149 | // Skip check if in some version the file has been added/deleted as this is already done in MixedModeFilePath, 150 | // before opening oldSource and newSource files 151 | oldSourceContent, err := readContent(oldSource) 152 | if err != nil { 153 | return "", fmt.Errorf("reading content of OldSource: %w", err) 154 | } 155 | 156 | newSourceContent, err := readContent(newSource) 157 | if err != nil { 158 | return "", fmt.Errorf("reading content of NewSource: %w", err) 159 | } 160 | 161 | updatedOldSource, err := applyDiff(oldSourceContent, oldFileDiff) 162 | if err != nil { 163 | return "", fmt.Errorf("applying diff to OldSource: %w", err) 164 | } 165 | 166 | updatedNewSource, err := applyDiff(newSourceContent, newFileDiff) 167 | if err != nil { 168 | return "", fmt.Errorf("applying diff to NewSource: %w", err) 169 | } 170 | 171 | ch := dbd.DiffChunks(strings.Split(strings.TrimSuffix(updatedOldSource, "\n"), "\n"), 172 | strings.Split(strings.TrimSuffix(updatedNewSource, "\n"), "\n")) 173 | 174 | // TODO: something with extended (extended header lines) 175 | resultFileDiff := &diff.FileDiff{ 176 | OrigName: oldFileDiff.NewName, 177 | OrigTime: oldFileDiff.NewTime, 178 | NewName: newFileDiff.NewName, 179 | NewTime: newFileDiff.NewTime, 180 | Extended: []string{}, 181 | Hunks: []*diff.Hunk{}, 182 | } 183 | 184 | convertChunksIntoFileDiff(ch, resultFileDiff) 185 | result, err := diff.PrintFileDiff(resultFileDiff) 186 | if err != nil { 187 | return "", fmt.Errorf("printing result diff for file %q: %w", 188 | oldFileDiff.NewName, err) 189 | } 190 | 191 | return string(result), nil 192 | } 193 | 194 | // MixedModeFile computes the diff of an oldSource file patched with oldDiff and 195 | // newSource file patched with newDiff. 196 | func MixedModeFile(oldSource, newSource, oldDiff, newDiff io.Reader) (string, error) { 197 | oldD, err := diff.NewFileDiffReader(oldDiff).Read() 198 | if err != nil { 199 | return "", fmt.Errorf("parsing oldDiff: %w", err) 200 | } 201 | 202 | newD, err := diff.NewFileDiffReader(newDiff).Read() 203 | if err != nil { 204 | return "", fmt.Errorf("parsing newDiff: %w", err) 205 | } 206 | 207 | result, err := mixedMode(oldSource, newSource, oldD, newD) 208 | if err != nil { 209 | return "", fmt.Errorf("mixedMode: %w", err) 210 | } 211 | 212 | return result, nil 213 | } 214 | 215 | // MixedModePath recursively computes the diff of an oldSource patched with oldDiff 216 | // and the newSource patched with newDiff, recursively if OldSource and NewSource are directories. 217 | func MixedModePath(oldSourcePath, newSourcePath string, oldDiff, newDiff io.Reader) (string, error) { 218 | // Get stats of sources 219 | oldSourceStat, err := os.Stat(oldSourcePath) 220 | if err != nil { 221 | return "", fmt.Errorf("get stat from oldSourcePath %q: %w", 222 | oldSourcePath, err) 223 | } 224 | 225 | newSourceStat, err := os.Stat(newSourcePath) 226 | if err != nil { 227 | return "", fmt.Errorf("get stat from newSourcePath %q: %w", 228 | newSourcePath, err) 229 | } 230 | 231 | // Check mode of sources 232 | switch { 233 | case !oldSourceStat.IsDir() && !newSourceStat.IsDir(): 234 | // Both sources are files 235 | oldD, err := diff.NewFileDiffReader(oldDiff).Read() 236 | if err != nil { 237 | return "", fmt.Errorf("parsing oldDiff for %q: %w", 238 | oldSourcePath, err) 239 | } 240 | 241 | if oldSourcePath != oldD.OrigName { 242 | return "", fmt.Errorf("filenames mismatch for oldSourcePath: %q and oldDiff: %q", 243 | oldSourcePath, oldD.OrigName) 244 | } 245 | 246 | newD, err := diff.NewFileDiffReader(newDiff).Read() 247 | if err != nil { 248 | return "", fmt.Errorf("parsing newDiff for %q: %w", 249 | newSourcePath, err) 250 | } 251 | 252 | if newSourcePath != newD.OrigName { 253 | return "", fmt.Errorf("filenames mismatch for newSourcePath: %q and newDiff: %q", 254 | newSourcePath, newD.OrigName) 255 | } 256 | 257 | resultString, err := mixedModeFilePath(oldSourcePath, newSourcePath, oldD, newD) 258 | return resultString, err 259 | 260 | case oldSourceStat.IsDir() && newSourceStat.IsDir(): 261 | // Both paths are directories 262 | resultString, err := mixedModeDirPath(oldSourcePath, newSourcePath, oldDiff, newDiff) 263 | if err != nil { 264 | return "", fmt.Errorf("compute diff for %q and %q: %w", 265 | oldSourcePath, newSourcePath, err) 266 | } 267 | 268 | return resultString, nil 269 | } 270 | 271 | return "", errors.New("sources should be both dirs or files") 272 | } 273 | 274 | // readContent returns content of source as string 275 | func readContent(source io.Reader) (string, error) { 276 | buf := new(strings.Builder) 277 | _, err := io.Copy(buf, source) 278 | if err != nil { 279 | return "", fmt.Errorf("copying source: %w", err) 280 | } 281 | return buf.String(), nil 282 | } 283 | 284 | // applyDiff returns applied changes from diffFile to source 285 | func applyDiff(source string, diffFile *diff.FileDiff) (string, error) { 286 | sourceBody := strings.Split(source, "\n") 287 | 288 | // currentOrgSourceI = 1 -- In diff lines started counting from 1 289 | var currentOrgSourceI int32 = 1 290 | var newBody []string 291 | 292 | for _, hunk := range diffFile.Hunks { 293 | // Add untouched part of source 294 | newBody = append(newBody, sourceBody[currentOrgSourceI-1:hunk.OrigStartLine-1]...) 295 | currentOrgSourceI = hunk.OrigStartLine 296 | 297 | hunkBody := strings.Split(strings.TrimSuffix(string(hunk.Body), "\n"), "\n") 298 | 299 | for _, line := range hunkBody { 300 | if currentOrgSourceI > int32(len(sourceBody)) { 301 | return "", errors.New("diff content is out of source content") 302 | } 303 | 304 | if strings.HasPrefix(line, "+") { 305 | newBody = append(newBody, line[1:]) 306 | } else { 307 | if line[1:] != sourceBody[currentOrgSourceI-1] { 308 | return "", fmt.Errorf( 309 | "line %d in source (%q) and diff (%q): %w", 310 | currentOrgSourceI, sourceBody[currentOrgSourceI-1], line[1:], ErrContentMismatch) 311 | } 312 | 313 | if strings.HasPrefix(line, " ") { 314 | newBody = append(newBody, sourceBody[currentOrgSourceI-1]) 315 | } 316 | 317 | currentOrgSourceI++ 318 | } 319 | } 320 | } 321 | 322 | newBody = append(newBody, sourceBody[currentOrgSourceI-1:]...) 323 | 324 | return strings.Join(newBody, "\n"), nil 325 | } 326 | 327 | // mixedModeFilePath computes the diff of a oldSourcePath file patched with oldFileDiff 328 | // and the newSourcePath file patched with newFileDiff. 329 | func mixedModeFilePath(oldSourcePath, newSourcePath string, oldFileDiff, newFileDiff *diff.FileDiff) (string, error) { 330 | if oldFileDiff.OrigName != "" && oldFileDiff.NewName == "" && newFileDiff.OrigName != "" && newFileDiff.NewName == "" { 331 | // In both updated version file has been deleted 332 | return "", nil 333 | } 334 | 335 | if oldFileDiff.OrigName != "" && oldFileDiff.NewName == "" { 336 | // File has been deleted in updated old version 337 | return fmt.Sprintf("Only in %s: %s\n", filepath.Dir(newFileDiff.NewName), 338 | filepath.Base(newFileDiff.NewName)), nil 339 | } 340 | 341 | if newFileDiff.OrigName != "" && newFileDiff.NewName == "" { 342 | // File has been deleted in updated new version 343 | return fmt.Sprintf("Only in %s: %s\n", filepath.Dir(oldFileDiff.NewName), 344 | filepath.Base(oldFileDiff.NewName)), nil 345 | } 346 | 347 | oldSourceFile, err := os.Open(oldSourcePath) 348 | if err != nil { 349 | return "", fmt.Errorf("opening oldSource file %q: %w", 350 | oldSourcePath, err) 351 | } 352 | 353 | newSourceFile, err := os.Open(newSourcePath) 354 | if err != nil { 355 | return "", fmt.Errorf("opening newSource file %q: %w", 356 | newSourcePath, err) 357 | } 358 | 359 | resultString, err := mixedMode(oldSourceFile, newSourceFile, oldFileDiff, newFileDiff) 360 | if err != nil { 361 | return "", fmt.Errorf("compute diff for %q: %w", 362 | oldFileDiff.OrigName, err) 363 | } 364 | 365 | return resultString, nil 366 | } 367 | 368 | // mixedModeDirPath computes the diff of a oldSourcePath directory patched with oldDiff 369 | // and the newSourcePath directory patched with newDiff. 370 | func mixedModeDirPath(oldSourcePath, newSourcePath string, oldDiff, newDiff io.Reader) (string, error) { 371 | oldFileNames, err := getAllFileNamesInDir(oldSourcePath) 372 | if err != nil { 373 | return "", fmt.Errorf("get all filenames for oldSource: %w", err) 374 | } 375 | 376 | newFileNames, err := getAllFileNamesInDir(newSourcePath) 377 | if err != nil { 378 | return "", fmt.Errorf("get all filenames for newSourcePath: %w", err) 379 | } 380 | 381 | oldFileDiffReader := diff.NewMultiFileDiffReader(oldDiff) 382 | newFileDiffReader := diff.NewMultiFileDiffReader(newDiff) 383 | 384 | lastOldFileDiff, err := oldFileDiffReader.ReadFile() 385 | if err != nil && !errors.Is(err, io.EOF) { 386 | return "", fmt.Errorf("parsing next FileDiff in oldDiff: %w", err) 387 | } 388 | 389 | lastNewFileDiff, err := newFileDiffReader.ReadFile() 390 | if err != nil && !errors.Is(err, io.EOF) { 391 | return "", fmt.Errorf("parsing next FileDiff in newDiff: %w", err) 392 | } 393 | 394 | result := "" 395 | updateOldDiff, updateNewDiff := false, false 396 | onlyOldFile, onlyNewFile := false, false 397 | 398 | // Iterate over files in FileDiff arrays 399 | i, j := 0, 0 400 | for i < len(oldFileNames) || j < len(newFileNames) { 401 | if lastOldFileDiff != nil && i < len(oldFileNames) && oldFileNames[i] > lastOldFileDiff.OrigName { 402 | if lastOldFileDiff.NewName != "" { 403 | return "", fmt.Errorf("oldFileDiff: %q doesn't have relative file in oldSource", 404 | lastOldFileDiff.OrigName) 405 | } 406 | // File has been added in old version 407 | result += fmt.Sprintf("Only in %s: %s\n", filepath.Dir(lastOldFileDiff.OrigName), 408 | filepath.Base(lastOldFileDiff.OrigName)) 409 | } 410 | 411 | if lastNewFileDiff != nil && j < len(newFileNames) && newFileNames[j] > lastNewFileDiff.OrigName { 412 | if lastNewFileDiff.NewName != "" { 413 | return "", fmt.Errorf("newFileDiff: %q doesn't have relative file in newSource", 414 | lastNewFileDiff.OrigName) 415 | } 416 | // File has been added in new version 417 | result += fmt.Sprintf("Only in %s: %s\n", filepath.Dir(lastNewFileDiff.OrigName), 418 | filepath.Base(lastNewFileDiff.OrigName)) 419 | } 420 | 421 | switch { 422 | case i < len(oldFileNames) && j < len(newFileNames): 423 | switch { 424 | // Comparing parts after oldSourcePath and newSourcePath 425 | case strings.TrimPrefix(oldFileNames[i], oldSourcePath) == strings.TrimPrefix(newFileNames[j], newSourcePath): 426 | switch { 427 | case lastOldFileDiff != nil && lastNewFileDiff != nil && 428 | oldFileNames[i] == lastOldFileDiff.OrigName && newFileNames[j] == lastNewFileDiff.OrigName: 429 | // Both oldFile and newFile have updates 430 | currentResult, err := mixedModeFilePath(oldFileNames[i], newFileNames[j], lastOldFileDiff, lastNewFileDiff) 431 | if err != nil { 432 | return "", fmt.Errorf("mixedModeFilePath for oldFile: %q and newFile: %q: %w", 433 | oldFileNames[i], newFileNames[j], err) 434 | } 435 | result += currentResult 436 | 437 | updateOldDiff = true 438 | updateNewDiff = true 439 | 440 | case lastOldFileDiff != nil && oldFileNames[i] == lastOldFileDiff.OrigName: 441 | // Only oldFile has updates 442 | // Empty FileDiff instead of lastNewFileDiff 443 | currentResult, err := mixedModeFilePath(oldFileNames[i], newFileNames[j], lastOldFileDiff, &diff.FileDiff{}) 444 | if err != nil { 445 | return "", fmt.Errorf("mixedModeFilePath for oldFile: %q and newFile: %q: %w", 446 | oldFileNames[i], newFileNames[j], err) 447 | } 448 | result += currentResult 449 | 450 | updateOldDiff = true 451 | 452 | case lastNewFileDiff != nil && newFileNames[j] == lastNewFileDiff.OrigName: 453 | // Only newFile has updates 454 | // Empty FileDiff instead of lastOldFileDiff 455 | currentResult, err := mixedModeFilePath(oldFileNames[i], newFileNames[j], &diff.FileDiff{}, lastNewFileDiff) 456 | if err != nil { 457 | return "", fmt.Errorf("mixedModeFilePath for oldFile: %q and newFile: %q: %w", 458 | oldFileNames[i], newFileNames[j], err) 459 | } 460 | result += currentResult 461 | 462 | updateNewDiff = true 463 | 464 | default: 465 | // None of oldFile and newFile have updates 466 | currentResult, err := mixedModeFilePath(oldFileNames[i], newFileNames[j], &diff.FileDiff{}, &diff.FileDiff{}) 467 | if err != nil { 468 | return "", fmt.Errorf("mixedModeFilePath for oldFile: %q and newFile: %q: %w", 469 | oldFileNames[i], newFileNames[j], err) 470 | } 471 | result += currentResult 472 | } 473 | i++ 474 | j++ 475 | case strings.TrimPrefix(oldFileNames[i], oldSourcePath) < strings.TrimPrefix(newFileNames[j], newSourcePath): 476 | onlyOldFile = true 477 | default: 478 | onlyNewFile = true 479 | } 480 | case i < len(oldFileNames): 481 | // In case there are more oldFileDiffs, while newFileDiffs are run out 482 | onlyOldFile = true 483 | default: 484 | // In case there are more newFileDiffs, while oldFileDiffs are run out 485 | onlyNewFile = true 486 | } 487 | 488 | if onlyOldFile { 489 | // mark to update oldFileDiff if last one was related to current oldFile 490 | if lastOldFileDiff != nil && oldFileNames[i] == lastOldFileDiff.OrigName { 491 | updateOldDiff = true 492 | // If file was deleted in oldFileDiff, don't add "Only in" message later 493 | if lastOldFileDiff.NewName == "" { 494 | onlyOldFile = false 495 | } 496 | } 497 | if onlyOldFile { 498 | result += fmt.Sprintf("Only in %s: %s\n", filepath.Dir(oldFileNames[i]), 499 | filepath.Base(oldFileNames[i])) 500 | } 501 | i++ 502 | onlyOldFile = false 503 | } 504 | 505 | if onlyNewFile { 506 | // mark to update newFileDiff if last one was related to current newFile 507 | if lastNewFileDiff != nil && newFileNames[j] == lastNewFileDiff.OrigName { 508 | updateNewDiff = true 509 | // If file was deleted in newFileDiff, don't add "Only in" message later 510 | if lastNewFileDiff.NewName == "" { 511 | onlyNewFile = true 512 | } 513 | } 514 | if onlyNewFile { 515 | result += fmt.Sprintf("Only in %s: %s\n", filepath.Dir(newFileNames[j]), 516 | filepath.Base(newFileNames[j])) 517 | } 518 | j++ 519 | onlyNewFile = false 520 | } 521 | 522 | if updateOldDiff { 523 | // get next lastOldFileDiff 524 | lastOldFileDiff, err = oldFileDiffReader.ReadFile() 525 | if err != nil { 526 | if !errors.Is(err, io.EOF) { 527 | return "", fmt.Errorf("parsing next FileDiff in oldDiff: %w", err) 528 | } 529 | lastOldFileDiff = nil 530 | } 531 | updateOldDiff = false 532 | } 533 | 534 | if updateNewDiff { 535 | // get next lastNewFileDiff 536 | lastNewFileDiff, err = newFileDiffReader.ReadFile() 537 | if err != nil { 538 | if !errors.Is(err, io.EOF) { 539 | return "", fmt.Errorf("parsing next FileDiff in newDiff: %w", err) 540 | } 541 | lastNewFileDiff = nil 542 | } 543 | updateNewDiff = false 544 | } 545 | } 546 | 547 | // Check if more files have been added in old version 548 | for lastOldFileDiff != nil { 549 | if lastOldFileDiff.NewName != "" { 550 | return "", fmt.Errorf("oldFileDiff: %q doesn't have relative file in oldSource", 551 | lastOldFileDiff.OrigName) 552 | } 553 | // File has been added 554 | result += fmt.Sprintf("Only in %s: %s\n", filepath.Dir(lastOldFileDiff.OrigName), 555 | filepath.Base(lastOldFileDiff.OrigName)) 556 | 557 | // Update lastOldFileDiff 558 | lastOldFileDiff, err = oldFileDiffReader.ReadFile() 559 | if err != nil { 560 | if !errors.Is(err, io.EOF) { 561 | return "", fmt.Errorf("parsing next FileDiff in oldDiff: %w", err) 562 | } 563 | lastOldFileDiff = nil 564 | } 565 | } 566 | 567 | // Check if more files have been added in new version 568 | for lastNewFileDiff != nil { 569 | if lastNewFileDiff.NewName != "" { 570 | return "", fmt.Errorf("newFileDiff: %q doesn't have relative file in newSource", 571 | lastNewFileDiff.OrigName) 572 | } 573 | // File has been added 574 | result += fmt.Sprintf("Only in %s: %s\n", filepath.Dir(lastNewFileDiff.OrigName), 575 | filepath.Base(lastNewFileDiff.OrigName)) 576 | 577 | // Update lastNewFileDiff 578 | lastNewFileDiff, err = newFileDiffReader.ReadFile() 579 | if err != nil { 580 | if !errors.Is(err, io.EOF) { 581 | return "", fmt.Errorf("parsing next FileDiff in newDiff: %w", err) 582 | } 583 | lastNewFileDiff = nil 584 | } 585 | } 586 | 587 | return result, nil 588 | } 589 | 590 | // getAllFileNamesInDir returns array of paths to files in root recursively. 591 | func getAllFileNamesInDir(root string) ([]string, error) { 592 | var allFiles []string 593 | err := filepath.Walk(root, 594 | func(path string, info os.FileInfo, err error) error { 595 | if err != nil { 596 | return fmt.Errorf("walk into %q: %w", 597 | path, err) 598 | } 599 | if !info.IsDir() { 600 | allFiles = append(allFiles, path) 601 | } 602 | return nil 603 | }) 604 | 605 | return allFiles, err 606 | } 607 | 608 | const contextLines = 2 609 | 610 | // convertChunksIntoFileDiff adds the given chunks to the fileDiff struct. 611 | func convertChunksIntoFileDiff(chunks []dbd.Chunk, fileDiff *diff.FileDiff) { 612 | var currentOldI, currentNewI int32 = 1, 1 613 | currentHunk := &diff.Hunk{ 614 | OrigStartLine: currentOldI, 615 | NewStartLine: currentNewI, 616 | } 617 | // Delete empty chunks in the beginning 618 | for len(chunks) > 0 && len(chunks[0].Added) == 0 && len(chunks[0].Deleted) == 0 && len(chunks[0].Equal) == 0 { 619 | chunks = chunks[1:] 620 | } 621 | // Delete empty chunks in the end 622 | last := len(chunks) - 1 623 | for len(chunks) > 0 && len(chunks[last].Added) == 0 && len(chunks[last].Deleted) == 0 && len(chunks[last].Equal) == 0 { 624 | chunks = chunks[:last] 625 | last-- 626 | } 627 | 628 | // If chunks contains only one element with only unchanged lines 629 | if len(chunks) == 1 && len(chunks[0].Added) == 0 && len(chunks[0].Deleted) == 0 { 630 | return 631 | } 632 | 633 | var currentHunkBody []string 634 | 635 | // If array of chunks is already empty 636 | if len(chunks) == 0 { 637 | return 638 | } 639 | 640 | // If first chunk contains only equal lines, we are adding last contextLines to currentHunk 641 | if len(chunks[0].Added) == 0 && len(chunks[0].Deleted) == 0 { 642 | currentOldI += int32(len(chunks[0].Equal)) 643 | currentNewI += int32(len(chunks[0].Equal)) 644 | if len(chunks[0].Equal) > contextLines { 645 | for _, line := range chunks[0].Equal[len(chunks[0].Equal)-contextLines:] { 646 | currentHunkBody = append(currentHunkBody, " "+line) 647 | currentHunk.OrigStartLine = currentOldI - contextLines 648 | currentHunk.NewStartLine = currentNewI - contextLines 649 | } 650 | } else { 651 | for _, line := range chunks[0].Equal { 652 | currentHunkBody = append(currentHunkBody, " "+line) 653 | } 654 | } 655 | // Removing processed first hunk 656 | chunks = chunks[1:] 657 | } 658 | 659 | var lastLines []string 660 | last = len(chunks) - 1 661 | // If last chunk contains equal lines, save first contextLines of equal lines for further processing 662 | if len(chunks[last].Equal) > 0 { 663 | if len(chunks[last].Equal) > contextLines { 664 | for _, line := range chunks[last].Equal[:contextLines] { 665 | lastLines = append(lastLines, " "+line) 666 | } 667 | } else { 668 | for _, line := range chunks[last].Equal { 669 | lastLines = append(lastLines, " "+line) 670 | } 671 | } 672 | // Removing processed equal lines from last chunk 673 | chunks[last].Equal = []string{} 674 | } 675 | 676 | for _, c := range chunks { 677 | // A chunk will not have both added and deleted lines. 678 | for _, line := range c.Added { 679 | currentHunkBody = append(currentHunkBody, "+"+line) 680 | currentNewI++ 681 | } 682 | for _, line := range c.Deleted { 683 | currentHunkBody = append(currentHunkBody, "-"+line) 684 | currentOldI++ 685 | } 686 | 687 | // Next piece of content contains too many unchanged lines. 688 | // Current hunk will be 'closed' and started new one. 689 | if len(c.Equal) > 2*contextLines+1 { 690 | if len(currentHunkBody) > 0 { 691 | for _, line := range c.Equal[:contextLines] { 692 | currentHunkBody = append(currentHunkBody, " "+line) 693 | } 694 | currentHunk.OrigLines = currentOldI + contextLines + 1 - currentHunk.OrigStartLine 695 | currentHunk.NewLines = currentNewI + contextLines + 1 - currentHunk.NewStartLine 696 | currentHunk.Body = []byte(strings.Join(currentHunkBody, "\n") + "\n") 697 | fileDiff.Hunks = append(fileDiff.Hunks, currentHunk) 698 | } 699 | 700 | currentOldI += int32(len(c.Equal)) 701 | currentNewI += int32(len(c.Equal)) 702 | 703 | currentHunk = &diff.Hunk{ 704 | OrigStartLine: currentOldI - contextLines, 705 | NewStartLine: currentNewI - contextLines, 706 | } 707 | 708 | // Clean currentHunkBody 709 | currentHunkBody = []string{} 710 | for _, line := range c.Equal[len(c.Equal)-contextLines-1:] { 711 | currentHunkBody = append(currentHunkBody, " "+line) 712 | } 713 | 714 | } else { 715 | for _, line := range c.Equal { 716 | currentHunkBody = append(currentHunkBody, " "+line) 717 | currentOldI++ 718 | currentNewI++ 719 | } 720 | } 721 | } 722 | 723 | // Add lastLines (equal) to last hunk 724 | for _, line := range lastLines { 725 | currentHunkBody = append(currentHunkBody, line) 726 | currentOldI++ 727 | currentNewI++ 728 | } 729 | 730 | // currentHunkBody contains some lines. It need to be 'closed' and added to fileDiff.Hunks 731 | currentHunk.OrigLines = currentOldI - currentHunk.OrigStartLine 732 | currentHunk.NewLines = currentNewI - currentHunk.NewStartLine 733 | currentHunk.Body = []byte(strings.Join(currentHunkBody, "\n") + "\n") 734 | fileDiff.Hunks = append(fileDiff.Hunks, currentHunk) 735 | } 736 | 737 | // interPrintSingleFileDiff returns printed version of diffFile, which was found only in one out of two versions. 738 | func interPrintSingleFileDiff(diffFile *diff.FileDiff) (string, error) { 739 | if diffFile.NewName == "" { 740 | // File has been added in current version 741 | return fmt.Sprintf("Only in %s: %s\n", filepath.Dir(diffFile.OrigName), 742 | filepath.Base(diffFile.OrigName)), nil 743 | } 744 | 745 | // File has been changed in current version and left unchanged in other version 746 | oldD, err := diff.PrintFileDiff(diffFile) 747 | if err != nil { 748 | return "", fmt.Errorf("printing diff for file %q: %w", 749 | diffFile.NewName, err) 750 | } 751 | return string(oldD), nil 752 | } 753 | 754 | // interFileDiff returns a new diff.FileDiff that is a diff of a source file patched with oldFileDiff 755 | // and the same source file patched with newFileDiff. 756 | func interFileDiff(oldFileDiff, newFileDiff *diff.FileDiff) (*diff.FileDiff, error) { 757 | 758 | // Configuration of result FileDiff 759 | // TODO: something with extended (extended header lines) 760 | 761 | resultFileDiff := &diff.FileDiff{ 762 | OrigName: oldFileDiff.NewName, 763 | OrigTime: oldFileDiff.NewTime, 764 | NewName: newFileDiff.NewName, 765 | NewTime: newFileDiff.NewTime, 766 | Extended: []string{}, 767 | Hunks: []*diff.Hunk{}} 768 | 769 | // Iterating over hunks in order they start in origin 770 | i, j := 0, 0 771 | for i < len(oldFileDiff.Hunks) && j < len(newFileDiff.Hunks) { 772 | switch { 773 | case oldFileDiff.Hunks[i].OrigStartLine+oldFileDiff.Hunks[i].OrigLines < newFileDiff.Hunks[j].OrigStartLine: 774 | // Whole oldHunk is before starting of newHunk 775 | resultFileDiff.Hunks = append(resultFileDiff.Hunks, 776 | revertedHunkBody(oldFileDiff.Hunks[i])) 777 | i++ 778 | case newFileDiff.Hunks[j].OrigStartLine+newFileDiff.Hunks[j].OrigLines < oldFileDiff.Hunks[i].OrigStartLine: 779 | // Whole newHunk is before starting of oldHunk 780 | resultFileDiff.Hunks = append(resultFileDiff.Hunks, newFileDiff.Hunks[j]) 781 | j++ 782 | default: 783 | // oldHunk and newHunk are overlapping somehow 784 | // Collecting a whole set of overlapping hunks to produce one continuous hunk 785 | oldHunks, newHunks := findOverlappingHunkSet(oldFileDiff, newFileDiff, &i, &j) 786 | mergedOverlappingHunk, err := mergeOverlappingHunks(oldHunks, newHunks) 787 | 788 | if err != nil { 789 | return nil, fmt.Errorf("merging overlapping hunks: %w", err) 790 | } 791 | 792 | // In case opposite hunks aren't doing same changes. 793 | if mergedOverlappingHunk != nil { 794 | resultFileDiff.Hunks = append(resultFileDiff.Hunks, mergedOverlappingHunk) 795 | } 796 | } 797 | } 798 | 799 | // In case there are more hunks in oldFileDiff, while hunks of newFileDiff are run out 800 | for i < len(oldFileDiff.Hunks) { 801 | resultFileDiff.Hunks = append(resultFileDiff.Hunks, 802 | revertedHunkBody(oldFileDiff.Hunks[i])) 803 | i++ 804 | } 805 | 806 | // In case there are more hunks in newFileDiff, while hunks of oldFileDiff are run out 807 | for j < len(newFileDiff.Hunks) { 808 | resultFileDiff.Hunks = append(resultFileDiff.Hunks, newFileDiff.Hunks[j]) 809 | j++ 810 | } 811 | 812 | return resultFileDiff, nil 813 | } 814 | 815 | // findOverlappingHunkSet finds next set (two arrays: oldHunks and newHunks) of 816 | // overlapping hunks in oldFileDiff and newFileDiff, starting from position i, j relatively. 817 | func findOverlappingHunkSet(oldFileDiff, newFileDiff *diff.FileDiff, i, j *int) (oldHunks, newHunks []*diff.Hunk) { 818 | // Collecting overlapped hunks into two arrays 819 | 820 | oldHunks = append(oldHunks, oldFileDiff.Hunks[*i]) 821 | newHunks = append(newHunks, newFileDiff.Hunks[*j]) 822 | *i++ 823 | *j++ 824 | 825 | Loop: 826 | for { 827 | switch { 828 | // Starting line of oldHunk is in previous newHunk body (between start and last lines) 829 | case *i < len(oldFileDiff.Hunks) && oldFileDiff.Hunks[*i].OrigStartLine >= newFileDiff.Hunks[*j-1].OrigStartLine && 830 | oldFileDiff.Hunks[*i].OrigStartLine < newFileDiff.Hunks[*j-1].OrigStartLine+newFileDiff.Hunks[*j-1].OrigLines: 831 | oldHunks = append(oldHunks, oldFileDiff.Hunks[*i]) 832 | *i++ 833 | // Starting line of newHunk is in previous oldHunk body (between start and last lines) 834 | case *j < len(newFileDiff.Hunks) && newFileDiff.Hunks[*j].OrigStartLine >= oldFileDiff.Hunks[*i-1].OrigStartLine && 835 | newFileDiff.Hunks[*j].OrigStartLine < oldFileDiff.Hunks[*i-1].OrigStartLine+oldFileDiff.Hunks[*i-1].OrigLines: 836 | newHunks = append(newHunks, newFileDiff.Hunks[*j]) 837 | *j++ 838 | default: 839 | // No overlapping hunks left 840 | break Loop 841 | } 842 | } 843 | 844 | return oldHunks, newHunks 845 | } 846 | 847 | // mergeOverlappingHunks returns a new diff.Hunk that is a diff hunk between overlapping oldHunks and newHunks, 848 | // related to the same source file. 849 | func mergeOverlappingHunks(oldHunks, newHunks []*diff.Hunk) (*diff.Hunk, error) { 850 | resultHunk, currentOrgI, err := configureResultHunk(oldHunks, newHunks) 851 | 852 | if err != nil { 853 | return nil, fmt.Errorf("configuring result hunk: %w", err) 854 | } 855 | 856 | // Indexes of hunks 857 | currentOldHunkI, currentNewHunkJ := 0, 0 858 | // Indexes of lines in body hunks 859 | // if indexes == -1 -- we don't have relevant hunk, which contains changes nearby currentOrgI 860 | i, j := -1, -1 861 | 862 | // Body of hunks 863 | var newBody []string 864 | var oldHunkBody, newHunkBody []string 865 | 866 | // Iterating through the hunks in the order they're appearing in origin file. 867 | // Using number of line in origin (currentOrgI) as an anchor to process line by line. 868 | // By using currentOrgI as anchor it is easier to see how changes have been applied step by step. 869 | 870 | // Merge, while there are hunks to process 871 | for currentOldHunkI < len(oldHunks) || currentNewHunkJ < len(newHunks) { 872 | 873 | // Entering next hunk in oldHunks 874 | if currentOldHunkI < len(oldHunks) && i == -1 && currentOrgI == oldHunks[currentOldHunkI].OrigStartLine { 875 | i = 0 876 | oldHunkBody = strings.Split(strings.TrimSuffix(string(oldHunks[currentOldHunkI].Body), "\n"), "\n") 877 | } 878 | 879 | // Entering next hunk in newHunks 880 | if currentNewHunkJ < len(newHunks) && j == -1 && currentOrgI == newHunks[currentNewHunkJ].OrigStartLine { 881 | j = 0 882 | newHunkBody = strings.Split(strings.TrimSuffix(string(newHunks[currentNewHunkJ].Body), "\n"), "\n") 883 | } 884 | 885 | switch { 886 | case i == -1 && j == -1: 887 | case i >= 0 && j == -1: 888 | // Changes are only in oldHunk 889 | newBody = append(newBody, revertedLine(oldHunkBody[i])) 890 | // In case current line haven't been added, we have processed anchor line. 891 | if !strings.HasPrefix(oldHunkBody[i], "+") { 892 | // Updating index of anchor line. 893 | currentOrgI++ 894 | } 895 | i++ 896 | 897 | case i == -1 && j >= 0: 898 | // Changes are only in newHunk 899 | newBody = append(newBody, newHunkBody[j]) 900 | // In case current line haven't been added, we have processed anchor line. 901 | if !strings.HasPrefix(newHunkBody[j], "+") { 902 | // Updating index of anchor line. 903 | currentOrgI++ 904 | } 905 | j++ 906 | 907 | default: 908 | // Changes are in old and new hunks. 909 | switch { 910 | // Firstly proceeding added lines, 911 | // because added lines are between previous currentOrgI and currentOrgI. 912 | case strings.HasPrefix(oldHunkBody[i], "+") || strings.HasPrefix(newHunkBody[j], "+"): 913 | newBody = append(newBody, interAddedLines(&i, &j, &oldHunkBody, &newHunkBody)...) 914 | default: 915 | // Checking if original content is the same 916 | if oldHunkBody[i][1:] != newHunkBody[j][1:] { 917 | return nil, fmt.Errorf( 918 | "line in original %d in oldDiff (%q) and newDiff (%q): %w", 919 | currentOrgI, oldHunkBody[i][1:], newHunkBody[j][1:], ErrContentMismatch) 920 | } 921 | switch { 922 | case strings.HasPrefix(oldHunkBody[i], " ") && strings.HasPrefix(newHunkBody[j], " "): 923 | newBody = append(newBody, oldHunkBody[i]) 924 | case strings.HasPrefix(oldHunkBody[i], "-") && strings.HasPrefix(newHunkBody[j], " "): 925 | newBody = append(newBody, revertedLine(oldHunkBody[i])) 926 | case strings.HasPrefix(oldHunkBody[i], " ") && strings.HasPrefix(newHunkBody[j], "-"): 927 | newBody = append(newBody, newHunkBody[j]) 928 | // If both have deleted same line, no need to append it to newBody 929 | } 930 | 931 | // Updating currentOrgI since we have processed anchor line. 932 | currentOrgI++ 933 | i++ 934 | j++ 935 | } 936 | } 937 | 938 | if i >= len(oldHunkBody) { 939 | // Proceed whole oldHunkBody 940 | i = -1 941 | currentOldHunkI++ 942 | } 943 | 944 | if j >= len(newHunkBody) { 945 | // Proceed whole newHunkBody 946 | j = -1 947 | currentNewHunkJ++ 948 | } 949 | } 950 | 951 | resultHunk.Body = []byte(strings.Join(newBody, "\n") + "\n") 952 | 953 | for _, line := range newBody { 954 | if !strings.HasPrefix(line, " ") { 955 | // resultHunkBody contains some changes 956 | return resultHunk, nil 957 | } 958 | } 959 | 960 | return nil, nil 961 | } 962 | 963 | // interAddedLines finds interdiff between added lines in oldHunkBody (after i) and newHunkBody (after j) 964 | func interAddedLines(i, j *int, oldHunkBody, newHunkBody *[]string) []string { 965 | var result, oldAddedLines, newAddedLines []string 966 | // Collect added lines in oldHunkBody 967 | for (*i < len(*oldHunkBody)) && (strings.HasPrefix((*oldHunkBody)[*i], "+")) { 968 | oldAddedLines = append(oldAddedLines, (*oldHunkBody)[*i][1:]) 969 | *i++ 970 | } 971 | // Collect added lines in newHunkBody 972 | for (*j < len(*newHunkBody)) && (strings.HasPrefix((*newHunkBody)[*j], "+")) { 973 | newAddedLines = append(newAddedLines, (*newHunkBody)[*j][1:]) 974 | *j++ 975 | } 976 | 977 | // Difference between collected added lines 978 | chunks := dbd.DiffChunks(oldAddedLines, newAddedLines) 979 | for _, c := range chunks { 980 | // A chunk will not have both added and deleted lines. 981 | for _, line := range c.Added { 982 | result = append(result, "+"+line) 983 | } 984 | for _, line := range c.Deleted { 985 | result = append(result, "-"+line) 986 | } 987 | for _, line := range c.Equal { 988 | result = append(result, " "+line) 989 | } 990 | } 991 | 992 | return result 993 | } 994 | 995 | // configureResultHunk returns a new diff.Hunk (with configured StartLines and NumberLines) 996 | // and currentOrgI (number of anchor line) based on oldHunks and newHunks, for their further merge. 997 | func configureResultHunk(oldHunks, newHunks []*diff.Hunk) (*diff.Hunk, int32, error) { 998 | if len(oldHunks) == 0 || len(newHunks) == 0 { 999 | return nil, 0, errors.New("one of the hunks array is empty") 1000 | } 1001 | 1002 | var currentOrgI int32 1003 | resultHunk := &diff.Hunk{ 1004 | // TODO: Concatenate sections 1005 | Section: "", 1006 | Body: []byte{0}, 1007 | } 1008 | 1009 | firstOldHunk, firstNewHunk := oldHunks[0], newHunks[0] 1010 | lastOldHunk, lastNewHunk := oldHunks[len(oldHunks)-1], newHunks[len(newHunks)-1] 1011 | 1012 | // Calculate StartLine for origin and new in result 1013 | if firstOldHunk.OrigStartLine < firstNewHunk.OrigStartLine { 1014 | // Started with old hunk 1015 | currentOrgI = firstOldHunk.OrigStartLine 1016 | // As we started with this old hunk, OrigStartLine will be same as start line of hunk in old source 1017 | resultHunk.OrigStartLine = firstOldHunk.NewStartLine 1018 | // StartLine in firstNewHunk - number of origin lines between start of firstNewHunk and start of resultHunk 1019 | resultHunk.NewStartLine = currentOrgI + 1020 | firstNewHunk.NewStartLine - firstNewHunk.OrigStartLine 1021 | } else { 1022 | // Started with new hunk 1023 | currentOrgI = firstNewHunk.OrigStartLine 1024 | // StartLine in firstOldHunk - number of origin lines between start of firstOldHunk and start of resultHunk 1025 | resultHunk.OrigStartLine = currentOrgI + 1026 | firstOldHunk.NewStartLine - firstOldHunk.OrigStartLine 1027 | // As we started with this new hunk, NewStartLine will be same as start line of hunk in new source 1028 | resultHunk.NewStartLine = firstNewHunk.NewStartLine 1029 | } 1030 | 1031 | // Calculate NumberLines for origin and new in result 1032 | if lastOldHunk.OrigStartLine+lastOldHunk.OrigLines > 1033 | lastNewHunk.OrigStartLine+lastNewHunk.OrigLines { 1034 | // Finished with old hunk 1035 | // Last line of lastOldHunk - first line of origin in resultHunk 1036 | resultHunk.OrigLines = lastOldHunk.NewStartLine + lastOldHunk.NewLines - resultHunk.OrigStartLine 1037 | // Last line of new in resultHunk - first line of new in resultHunk 1038 | // lastNewHunk.NewStartLine + lastNewHunk.NewLines = last line of lastNewHunk 1039 | resultHunk.NewLines = lastNewHunk.NewStartLine + lastNewHunk.NewLines + 1040 | // + number of origin lines between last line of lastNewHunk and lastOldHunk 1041 | lastOldHunk.OrigStartLine + lastOldHunk.OrigLines - 1042 | lastNewHunk.OrigStartLine - lastNewHunk.OrigLines - 1043 | // - first line of new in resultHunk 1044 | resultHunk.NewStartLine 1045 | } else { 1046 | // Finished with new hunk 1047 | // Last line of old in resultHunk - first line of old in resultHunk 1048 | // lastOldHunk.NewStartLine + lastOldHunk.NewLines = last line of lastOldHunk 1049 | resultHunk.OrigLines = lastOldHunk.NewStartLine + lastOldHunk.NewLines + 1050 | // + number of origin lines between last line of lastOldHunk and lastNewHunk 1051 | lastNewHunk.OrigStartLine + lastNewHunk.OrigLines - 1052 | lastOldHunk.OrigStartLine - lastOldHunk.OrigLines - 1053 | // - first line of old in resultHunk 1054 | resultHunk.OrigStartLine 1055 | // Last line of lastNewHunk - first line of new in resultHunk 1056 | resultHunk.NewLines = lastNewHunk.NewStartLine + lastNewHunk.NewLines - resultHunk.NewStartLine 1057 | } 1058 | 1059 | resultHunk.OrigNoNewlineAt = 0 1060 | resultHunk.StartPosition = firstOldHunk.StartPosition 1061 | 1062 | return resultHunk, currentOrgI, nil 1063 | } 1064 | 1065 | // revertHunks reverts each hunk body in hunks in diffFile 1066 | func revertHunks(diffFile *diff.FileDiff) { 1067 | for k, h := range diffFile.Hunks { 1068 | diffFile.Hunks[k] = revertedHunkBody(h) 1069 | } 1070 | } 1071 | 1072 | // revertedHunkBody returns a copy of hunk with reverted lines of Body. 1073 | func revertedHunkBody(hunk *diff.Hunk) *diff.Hunk { 1074 | var newBody []string 1075 | 1076 | lines := strings.Split(string(hunk.Body), "\n") 1077 | 1078 | for _, line := range lines { 1079 | newBody = append(newBody, revertedLine(line)) 1080 | } 1081 | 1082 | revertedHunk := &diff.Hunk{ 1083 | OrigStartLine: hunk.OrigStartLine, 1084 | OrigLines: hunk.OrigLines, 1085 | OrigNoNewlineAt: hunk.OrigNoNewlineAt, 1086 | NewStartLine: hunk.NewStartLine, 1087 | NewLines: hunk.NewLines, 1088 | Section: hunk.Section, 1089 | StartPosition: hunk.StartPosition, 1090 | Body: []byte(strings.Join(newBody, "\n") + "\n"), 1091 | } 1092 | 1093 | return revertedHunk 1094 | } 1095 | 1096 | // revertedLine returns a reverted line. 1097 | // `+` added lines are marked as `-` deleted and vise versa. 1098 | // ` ` unchanged lines are left as unchanged. 1099 | func revertedLine(line string) string { 1100 | switch { 1101 | case strings.HasPrefix(line, "+"): 1102 | return "-" + line[1:] 1103 | case strings.HasPrefix(line, "-"): 1104 | return "+" + line[1:] 1105 | default: 1106 | return line 1107 | } 1108 | } 1109 | 1110 | // ErrContentMismatch indicates that compared content is not same. 1111 | var ErrContentMismatch = errors.New("content mismatch") 1112 | 1113 | // ErrEmptyDiffFile indicates that provided file doesn't contain any information about changes. 1114 | var ErrEmptyDiffFile = errors.New("empty diff file") 1115 | --------------------------------------------------------------------------------