dashboard / pico / style(prose): update smol.css #19 rss

closed · opened on 2024-09-03T02:15:28Z by erock
Help
# add changes to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add 19
# add review to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add --review 19
# remove patchset
ssh pr.pico.sh ps rm ps-x
# checkout all patches
ssh pr.pico.sh pr print 19 | git am -3
# print a diff between the last two patches in a patch request
ssh pr.pico.sh pr diff 19
# accept PR
ssh pr.pico.sh pr accept 19
# close PR
ssh pr.pico.sh pr close 19

Logs

erock created pr with ps-35 on 2024-08-19T21:12:53Z
erock added ps-51 on 2024-10-18T20:36:21Z

Patchsets

ps-35 by erock on 2024-08-19T21:12:53Z
Range Diff ↕ rd-51
1: 778bcd8 ! 1: 0f86231 feat: range diff
ps-51 by erock on 2024-10-18T20:36:21Z

Range-diff rd-51

title
feat: range diff
description
Patch changed
old #1
778bcd8
new #1
0f86231
Back to top
1: 778bcd8 ! 1: 0f86231 feat: range diff
range_diff.go
+package git
+
+import (
+	"fmt"
+	"math"
+
+	"github.com/sergi/go-diff/diffmatchpatch"
+)
+
+var COST_MAX = 65536
+var RANGE_DIFF_CREATION_FACTOR_DEFAULT = 60
+
+type PatchRange struct {
+	*Patch
+	Matching int
+	Diff     string
+	DiffSize int
+	Shown    bool
+}
+
+func NewPatchRange(patch *Patch) *PatchRange {
+	diff := patch.CalcDiff()
+	return &PatchRange{
+		Patch: patch,
+		Patch:    patch,
+		Matching: -1,
+		Diff:     diff,
+		DiffSize: len(diff),
+		Shown:    false,
+	}
+}
+
+func output(a []*PatchRange, b []*PatchRange) string {
+	out := ""
+	for _, patchB := range b {
+	for i, patchA := range a {
+		if patchA.Matching == -1 {
+			out += outputPairHeader(patchA, nil, i+1, -1)
+		}
+	}
+
+	for j, patchB := range b {
+		if patchB.Matching == -1 {
+			out += outputPairHeader(nil, patchB, -1, j+1)
+			continue
+		}
+		patchA := a[patchB.Matching]
+		if patchB.ContentSha == patchA.ContentSha {
+			out += outputPairHeader(patchA, patchB, patchB.Matching+1, patchA.Matching+1)
+		}
+	}
+	return out
+}
+
+func outputPairHeader(a *PatchRange, b *PatchRange, aIndex, bIndex int) string {
+	if a == nil {
+		return fmt.Sprintf("-:  ------- > %d:  %s %s\n", bIndex, truncateSha(b.CommitSha), b.Title)
+	}
+	if b == nil {
+		return fmt.Sprintf("%d:  %s < -:  ------- %s\n", aIndex, truncateSha(a.CommitSha), a.Title)
+	}
+	return fmt.Sprintf("%d:  %s = %d:  %s %s\n", aIndex, truncateSha(a.CommitSha), bIndex, truncateSha(b.CommitSha), a.Title)
+}
+
+func RangeDiff(a []*Patch, b []*Patch) string {
+	aPatches := []*PatchRange{}
+	for _, patch := range a {
+		aPatches = append(aPatches, NewPatchRange(patch))
+	}
+	bPatches := []*PatchRange{}
+	for _, patch := range b {
+		bPatches = append(bPatches, NewPatchRange(patch))
+	}
+	findExactMatches(aPatches, bPatches)
+	getCorrespondences(aPatches, bPatches, RANGE_DIFF_CREATION_FACTOR_DEFAULT)
+	return output(aPatches, bPatches)
+}
+
+func findExactMatches(a []*PatchRange, b []*PatchRange) {
+	for i, patchA := range a {
+		for j, patchB := range b {
+			if patchA.ContentSha == patchB.ContentSha {
+				patchA.Matching = j
+				patchB.Matching = i
+			}
+		}
+	}
+}
+
+func createMatrix(rows, cols int) [][]int {
+	mat := make([][]int, rows)
+	for i := range mat {
+		mat[i] = make([]int, cols)
+	}
+	return mat
+}
+
+func diffsize(a *PatchRange, b *PatchRange) int {
+	dmp := diffmatchpatch.New()
+	diffs := dmp.DiffMain(a.RawText, b.RawText, false)
+	diffs := dmp.DiffMain(a.Diff, b.Diff, false)
+	return len(dmp.DiffPrettyText(diffs))
+}
+
+func getCorrespondences(a []*PatchRange, b []*PatchRange, creationFactor int) {
+	// n := len(a) + len(b)
+	fmt.Println(len(a), len(b))
+	cost := createMatrix(len(a), len(b))
+	n := len(a) + len(b)
+	fmt.Println("rows", len(a), "cols", len(b))
+	cost := createMatrix(n, n)
+
+	for i, patchA := range a {
+		var c int
+		for j, patchB := range b {
+			if patchA.Matching == j {
+				c = 0
+			} else if patchA.Matching == 0 && patchB.Matching == 0 {
+			} else if patchA.Matching == -1 && patchB.Matching == -1 {
+				c = diffsize(patchA, patchB)
+			} else {
+				c = COST_MAX
+			}
+			cost[i][j] = c
+		}
+	}
+
+	assignment := computeAssignment(cost, len(a), len(b))
+	for i, j := range assignment {
+		if j < len(b) {
+			a[i].Matching = j
+			b[j].Matching = i
+	for j, patchB := range b {
+		creationCost := (patchB.DiffSize * creationFactor) / 100
+		if patchB.Matching >= 0 {
+			creationCost = math.MaxInt32
+		}
+		for i := len(a); i < n; i++ {
+			cost[i][j] = creationCost
+		}
+	}
+
+	fmt.Println(cost, assignment)
+	fmt.Println("A==")
+	for _, patch := range a {
+		fmt.Println("matches", b[patch.Matching].Title)
+	for i := len(a); i < n; i++ {
+		for j := len(b); j < n; j++ {
+			cost[i][j] = 0
+		}
+	}
+
+	fmt.Println("B==")
+	for _, patch := range b {
+		fmt.Println("matches", a[patch.Matching].Title)
+	assignment := computeAssignment(cost, n, n)
+	for i, j := range assignment {
+		if i < len(a) && j < len(b) {
+			a[i].Matching = j
+			b[j].Matching = i
+		}
+	}
+
+	fmt.Println("cost", cost, "assignment", assignment)
+}
+
+// computeAssignment assigns patches using the Hungarian algorithm.
+func computeAssignment(costMatrix [][]int, m, n int) []int {
+	u := make([]int, m+1) // potential for workers
+	v := make([]int, n+1) // potential for jobs
+	p := make([]int, n+1) // job assignment
+	way := make([]int, n+1)
+
+	for i := 1; i <= m; i++ {
+		links := make([]int, n+1)
+		minV := make([]int, n+1)
+		used := make([]bool, n+1)
+		for j := 0; j <= n; j++ {
+			minV[j] = math.MaxInt32
+			used[j] = false
+		}
+
+		j0 := 0
+		p[0] = i
+
+		for {
+			used[j0] = true
+			i0 := p[j0]
+			delta := math.MaxInt32
+			j1 := 0
+
+			for j := 1; j <= n; j++ {
+				if !used[j] {
+					cur := costMatrix[i0-1][j-1] - u[i0] - v[j]
+					if cur < minV[j] {
+						minV[j] = cur
+						links[j] = j0
+					}
+					if minV[j] < delta {
+						delta = minV[j]
+						j1 = j
+					}
+				}
+			}
+
+			for j := 0; j <= n; j++ {
+				if used[j] {
+					u[p[j]] += delta
+					v[j] -= delta
+				} else {
+					minV[j] -= delta
+				}
+			}
+
+			j0 = j1
+			if p[j0] == 0 {
+				break
+			}
+		}
+
+		for {
+			j1 := way[j0]
+			p[j0] = p[j1]
+			j0 = j1
+			if j0 == 0 {
+				break
+			}
+		}
+	}
+
+	assignment := make([]int, m)
+	for j := 1; j <= n; j++ {
+		if p[j] > 0 {
+			assignment[p[j]-1] = j - 1
+		}
+	}
+	return assignment
+}
range_diff.go
+package git
+
+import (
+	"fmt"
+	"math"
+
+	"github.com/sergi/go-diff/diffmatchpatch"
+)
+
+var COST_MAX = 65536
+var RANGE_DIFF_CREATION_FACTOR_DEFAULT = 60
+
+type PatchRange struct {
+	*Patch
+	Matching int
+	Diff     string
+	DiffSize int
+	Shown    bool
+}
+
+func NewPatchRange(patch *Patch) *PatchRange {
+	diff := patch.CalcDiff()
+	return &PatchRange{
+		Patch: patch,
+		Patch:    patch,
+		Matching: -1,
+		Diff:     diff,
+		DiffSize: len(diff),
+		Shown:    false,
+	}
+}
+
+func output(a []*PatchRange, b []*PatchRange) string {
+	out := ""
+	for _, patchB := range b {
+	for i, patchA := range a {
+		if patchA.Matching == -1 {
+			out += outputPairHeader(patchA, nil, i+1, -1)
+		}
+	}
+
+	for j, patchB := range b {
+		if patchB.Matching == -1 {
+			out += outputPairHeader(nil, patchB, -1, j+1)
+			continue
+		}
+		patchA := a[patchB.Matching]
+		if patchB.ContentSha == patchA.ContentSha {
+			out += outputPairHeader(patchA, patchB, patchB.Matching+1, patchA.Matching+1)
+		}
+	}
+	return out
+}
+
+func outputPairHeader(a *PatchRange, b *PatchRange, aIndex, bIndex int) string {
+	if a == nil {
+		return fmt.Sprintf("-:  ------- > %d:  %s %s\n", bIndex, truncateSha(b.CommitSha), b.Title)
+	}
+	if b == nil {
+		return fmt.Sprintf("%d:  %s < -:  ------- %s\n", aIndex, truncateSha(a.CommitSha), a.Title)
+	}
+	return fmt.Sprintf("%d:  %s = %d:  %s %s\n", aIndex, truncateSha(a.CommitSha), bIndex, truncateSha(b.CommitSha), a.Title)
+}
+
+func RangeDiff(a []*Patch, b []*Patch) string {
+	aPatches := []*PatchRange{}
+	for _, patch := range a {
+		aPatches = append(aPatches, NewPatchRange(patch))
+	}
+	bPatches := []*PatchRange{}
+	for _, patch := range b {
+		bPatches = append(bPatches, NewPatchRange(patch))
+	}
+	findExactMatches(aPatches, bPatches)
+	getCorrespondences(aPatches, bPatches, RANGE_DIFF_CREATION_FACTOR_DEFAULT)
+	return output(aPatches, bPatches)
+}
+
+func findExactMatches(a []*PatchRange, b []*PatchRange) {
+	for i, patchA := range a {
+		for j, patchB := range b {
+			if patchA.ContentSha == patchB.ContentSha {
+				patchA.Matching = j
+				patchB.Matching = i
+			}
+		}
+	}
+}
+
+func createMatrix(rows, cols int) [][]int {
+	mat := make([][]int, rows)
+	for i := range mat {
+		mat[i] = make([]int, cols)
+	}
+	return mat
+}
+
+func diffsize(a *PatchRange, b *PatchRange) int {
+	dmp := diffmatchpatch.New()
+	diffs := dmp.DiffMain(a.RawText, b.RawText, false)
+	diffs := dmp.DiffMain(a.Diff, b.Diff, false)
+	return len(dmp.DiffPrettyText(diffs))
+}
+
+func getCorrespondences(a []*PatchRange, b []*PatchRange, creationFactor int) {
+	// n := len(a) + len(b)
+	fmt.Println(len(a), len(b))
+	cost := createMatrix(len(a), len(b))
+	n := len(a) + len(b)
+	fmt.Println("rows", len(a), "cols", len(b))
+	cost := createMatrix(n, n)
+
+	for i, patchA := range a {
+		var c int
+		for j, patchB := range b {
+			if patchA.Matching == j {
+				c = 0
+			} else if patchA.Matching == 0 && patchB.Matching == 0 {
+			} else if patchA.Matching == -1 && patchB.Matching == -1 {
+				c = diffsize(patchA, patchB)
+			} else {
+				c = COST_MAX
+			}
+			cost[i][j] = c
+		}
+	}
+
+	assignment := computeAssignment(cost, len(a), len(b))
+	for i, j := range assignment {
+		if j < len(b) {
+			a[i].Matching = j
+			b[j].Matching = i
+	for j, patchB := range b {
+		creationCost := (patchB.DiffSize * creationFactor) / 100
+		if patchB.Matching >= 0 {
+			creationCost = math.MaxInt32
+		}
+		for i := len(a); i < n; i++ {
+			cost[i][j] = creationCost
+		}
+	}
+
+	fmt.Println(cost, assignment)
+	fmt.Println("A==")
+	for _, patch := range a {
+		fmt.Println("matches", b[patch.Matching].Title)
+	for i := len(a); i < n; i++ {
+		for j := len(b); j < n; j++ {
+			cost[i][j] = 0
+		}
+	}
+
+	fmt.Println("B==")
+	for _, patch := range b {
+		fmt.Println("matches", a[patch.Matching].Title)
+	assignment := computeAssignment(cost, n, n)
+	for i, j := range assignment {
+		if i < len(a) && j < len(b) {
+			a[i].Matching = j
+			b[j].Matching = i
+		}
+	}
+
+	fmt.Println("cost", cost, "assignment", assignment)
+}
+
+// computeAssignment assigns patches using the Hungarian algorithm.
+func computeAssignment(costMatrix [][]int, m, n int) []int {
+	u := make([]int, m+1) // potential for workers
+	v := make([]int, n+1) // potential for jobs
+	p := make([]int, n+1) // job assignment
+	way := make([]int, n+1)
+
+	for i := 1; i <= m; i++ {
+		links := make([]int, n+1)
+		minV := make([]int, n+1)
+		used := make([]bool, n+1)
+		for j := 0; j <= n; j++ {
+			minV[j] = math.MaxInt32
+			used[j] = false
+		}
+
+		j0 := 0
+		p[0] = i
+
+		for {
+			used[j0] = true
+			i0 := p[j0]
+			delta := math.MaxInt32
+			j1 := 0
+
+			for j := 1; j <= n; j++ {
+				if !used[j] {
+					cur := costMatrix[i0-1][j-1] - u[i0] - v[j]
+					if cur < minV[j] {
+						minV[j] = cur
+						links[j] = j0
+					}
+					if minV[j] < delta {
+						delta = minV[j]
+						j1 = j
+					}
+				}
+			}
+
+			for j := 0; j <= n; j++ {
+				if used[j] {
+					u[p[j]] += delta
+					v[j] -= delta
+				} else {
+					minV[j] -= delta
+				}
+			}
+
+			j0 = j1
+			if p[j0] == 0 {
+				break
+			}
+		}
+
+		for {
+			j1 := way[j0]
+			p[j0] = p[j1]
+			j0 = j1
+			if j0 == 0 {
+				break
+			}
+		}
+	}
+
+	assignment := make([]int, m)
+	for j := 1; j <= n; j++ {
+		if p[j] > 0 {
+			assignment[p[j]-1] = j - 1
+		}
+	}
+	return assignment
+}
range_diff_test.go
+package git
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/picosh/git-pr/fixtures"
+)
+
+func bail(err error) {
+	if err != nil {
+		panic(bail)
+	}
+}
+
+func cmp(afile, bfile string) string {
+	a, err := fixtures.Fixtures.Open(afile)
+	bail(err)
+	b, err := fixtures.Fixtures.Open(bfile)
+	bail(err)
+	aPatches, err := ParsePatchset(a)
+	bail(err)
+	bPatches, err := ParsePatchset(b)
+	bail(err)
+	actual := RangeDiff(aPatches, bPatches)
+	return actual
+}
+
+func fail(expected, actual string) string {
+	return fmt.Sprintf("expected:[%s] actual:[%s]", expected, actual)
+}
+
+// https://git.kernel.org/tree/t/t3206-range-diff.sh?id=d19b6cd2dd72dc811f19df4b32c7ed223256c3ee
+
+// simple A..B A..C (unmodified)
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid u1) s/5/A/
+	2:  $(test_oid t2) = 2:  $(test_oid u2) s/4/A/
+	3:  $(test_oid t3) = 3:  $(test_oid u3) s/11/B/
+	4:  $(test_oid t4) = 4:  $(test_oid u4) s/12/B/
+*/
+func TestRangeDiffUnmodified(t *testing.T) {
+	actual := cmp("a_b.patch", "a_c.patch")
+	expected := "1:  33c682a = 1:  1668484 chore: add torch and create random tensor\n"
+	if expected != actual {
+		t.Fatalf(fail(expected, actual))
+	}
+}
+
+// trivial reordering
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid r1) s/5/A/
+	3:  $(test_oid t3) = 2:  $(test_oid r2) s/11/B/
+	4:  $(test_oid t4) = 3:  $(test_oid r3) s/12/B/
+	2:  $(test_oid t2) = 4:  $(test_oid r4) s/4/A/
+*/
+func TestRangeDiffTrivialReordering(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
+	expected := `2:  22dde12 = 1:  7dbb94c docs: readme
+1:  33c682a = 2:  ad17587 chore: add torch and create random tensor
+`
+	if expected != actual {
+		t.Fatalf(fail(expected, actual))
+	}
+}
+
+// removed commit
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid d1) s/5/A/
+	2:  $(test_oid t2) < -:  $(test_oid __) s/4/A/
+	3:  $(test_oid t3) = 2:  $(test_oid d2) s/11/B/
+	4:  $(test_oid t4) = 3:  $(test_oid d3) s/12/B/
+*/
+func TestRangeDiffRemovedCommit(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
+	actual := cmp("a_b_reorder.patch", "a_c_rm_commit.patch")
+	expected := `1:  33c682a < -:  ------- chore: add torch and create random tensor
+2:  22dde12 = 1:  7dbb94c docs: readme`
+2:  22dde12 = 1:  7dbb94c docs: readme
+`
+	if expected != actual {
+		t.Fatalf(fail(expected, actual))
+	}
+}
+
+// added commit
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid a1) s/5/A/
+	2:  $(test_oid t2) = 2:  $(test_oid a2) s/4/A/
+	-:  $(test_oid __) > 3:  $(test_oid a3) s/6/A/
+	3:  $(test_oid t3) = 4:  $(test_oid a4) s/11/B/
+	4:  $(test_oid t4) = 5:  $(test_oid a5) s/12/B/
+*/
+/* func TestRangeDiffAddedCommit(t *testing.T) {
+	actual := ""
+func TestRangeDiffAddedCommit(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_added_commit.patch")
+	expected := `1:  33c682a = 1:  33c682a chore: add torch and create random tensor
+2:  22dde12 = 2:  22dde12 docs: readme
+-:  ------- > 3:  b248060 chore: make tensor 6x6`
+-:  ------- > 3:  b248060 chore: make tensor 6x6
+`
+	if expected != actual {
+		t.Fatalf("expected:%s actual:%s", expected, actual)
+		t.Fatalf(fail(expected, actual))
+	}
+} */
+}
+
+// changed commit
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid c1) s/5/A/
+	2:  $(test_oid t2) = 2:  $(test_oid c2) s/4/A/
+	3:  $(test_oid t3) ! 3:  $(test_oid c3) s/11/B/
+	    @@ file: A
+	      9
+	      10
+	     -11
+	    -+B
+	    ++BB
+	      12
+	      13
+	      14
+	4:  $(test_oid t4) ! 4:  $(test_oid c4) s/12/B/
+	    @@ file
+	     @@ file: A
+	      9
+	      10
+	    - B
+	    + BB
+	     -12
+	     +B
+	      13
+*/
+/* func TestRangeDiffChangedCommit(t *testing.T) {
+	actual := ""
+	fp, err := fixtures.Fixtures.ReadFile("extected_commit_changed.txt")
+func TestRangeDiffChangedCommit(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_changed_commit.patch")
+	fp, err := fixtures.Fixtures.ReadFile("expected_commit_changed.txt")
+	if err != nil {
+		t.Fatalf("file not found")
+	}
+	expected := string(fp)
+	if expected != actual {
+		t.Fatalf("expected:%s actual:%s", expected, actual)
+		t.Fatalf(fail(expected, actual))
+	}
+} */
+}
+
+// renamed file
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid n1) s/5/A/
+	2:  $(test_oid t2) ! 2:  $(test_oid n2) s/4/A/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/4/A/
+	    +    s/4/A/ + rename file
+	    Z
+	    - ## file ##
+	    + ## file => renamed-file ##
+	    Z@@
+	    Z 1
+	    Z 2
+	3:  $(test_oid t3) ! 3:  $(test_oid n3) s/11/B/
+	    @@ Metadata
+	    Z ## Commit message ##
+	    Z    s/11/B/
+	    Z
+	    - ## file ##
+	    -@@ file: A
+	    + ## renamed-file ##
+	    +@@ renamed-file: A
+	    Z 8
+	    Z 9
+	    Z 10
+	4:  $(test_oid t4) ! 4:  $(test_oid n4) s/12/B/
+	    @@ Metadata
+	    Z ## Commit message ##
+	    Z    s/12/B/
+	    Z
+	    - ## file ##
+	    -@@ file: A
+	    + ## renamed-file ##
+	    +@@ renamed-file: A
+	    Z 9
+	    Z 10
+	    Z B
+*/
+// func TestRangeDiffRenamedFile(t *testing.T) {}
+
+// file with mode only change
+/*
+	1:  $(test_oid t2) ! 1:  $(test_oid o1) s/4/A/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/4/A/
+	    +    s/4/A/ + add other-file
+	    Z
+	    Z ## file ##
+	    Z@@
+	    @@ file
+	    Z A
+	    Z 6
+	    Z 7
+	    +
+	    + ## other-file (new) ##
+	2:  $(test_oid t3) ! 2:  $(test_oid o2) s/11/B/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/11/B/
+	    +    s/11/B/ + mode change other-file
+	    Z
+	    Z ## file ##
+	    Z@@ file: A
+	    @@ file: A
+	    Z 12
+	    Z 13
+	    Z 14
+	    +
+	    + ## other-file (mode change 100644 => 100755) ##
+	3:  $(test_oid t4) = 3:  $(test_oid o3) s/12/B/
+*/
+// func TestRangeDiffFileWithModeOnlyChange(t *testing.T) {}
+
+// file added and later removed
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid s1) s/5/A/
+	2:  $(test_oid t2) ! 2:  $(test_oid s2) s/4/A/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/4/A/
+	    +    s/4/A/ + new-file
+	    Z
+	    Z ## file ##
+	    Z@@
+	    @@ file
+	    Z A
+	    Z 6
+	    Z 7
+	    +
+	    + ## new-file (new) ##
+	3:  $(test_oid t3) ! 3:  $(test_oid s3) s/11/B/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/11/B/
+	    +    s/11/B/ + remove file
+	    Z
+	    Z ## file ##
+	    Z@@ file: A
+	    @@ file: A
+	    Z 12
+	    Z 13
+	    Z 14
+	    +
+	    + ## new-file (deleted) ##
+	4:  $(test_oid t4) = 4:  $(test_oid s4) s/12/B/
+*/
+// func TestRangeDiffFileAddedThenRemoved(t *testing.T) {}
+
+// changed message
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid m1) s/5/A/
+	2:  $(test_oid t2) ! 2:  $(test_oid m2) s/4/A/
+	    @@ Metadata
+	    Z ## Commit message ##
+	    Z    s/4/A/
+	    Z
+	    +    Also a silly comment here!
+	    +
+	    Z ## file ##
+	    Z@@
+	    Z 1
+	3:  $(test_oid t3) = 3:  $(test_oid m3) s/11/B/
+	4:  $(test_oid t4) = 4:  $(test_oid m4) s/12/B/
+*/
+// func TestRangeDiffChangedMessage(t *testing.T) {}
range_diff_test.go
+package git
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/picosh/git-pr/fixtures"
+)
+
+func bail(err error) {
+	if err != nil {
+		panic(bail)
+	}
+}
+
+func cmp(afile, bfile string) string {
+	a, err := fixtures.Fixtures.Open(afile)
+	bail(err)
+	b, err := fixtures.Fixtures.Open(bfile)
+	bail(err)
+	aPatches, err := ParsePatchset(a)
+	bail(err)
+	bPatches, err := ParsePatchset(b)
+	bail(err)
+	actual := RangeDiff(aPatches, bPatches)
+	return actual
+}
+
+func fail(expected, actual string) string {
+	return fmt.Sprintf("expected:[%s] actual:[%s]", expected, actual)
+}
+
+// https://git.kernel.org/tree/t/t3206-range-diff.sh?id=d19b6cd2dd72dc811f19df4b32c7ed223256c3ee
+
+// simple A..B A..C (unmodified)
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid u1) s/5/A/
+	2:  $(test_oid t2) = 2:  $(test_oid u2) s/4/A/
+	3:  $(test_oid t3) = 3:  $(test_oid u3) s/11/B/
+	4:  $(test_oid t4) = 4:  $(test_oid u4) s/12/B/
+*/
+func TestRangeDiffUnmodified(t *testing.T) {
+	actual := cmp("a_b.patch", "a_c.patch")
+	expected := "1:  33c682a = 1:  1668484 chore: add torch and create random tensor\n"
+	if expected != actual {
+		t.Fatalf(fail(expected, actual))
+	}
+}
+
+// trivial reordering
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid r1) s/5/A/
+	3:  $(test_oid t3) = 2:  $(test_oid r2) s/11/B/
+	4:  $(test_oid t4) = 3:  $(test_oid r3) s/12/B/
+	2:  $(test_oid t2) = 4:  $(test_oid r4) s/4/A/
+*/
+func TestRangeDiffTrivialReordering(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
+	expected := `2:  22dde12 = 1:  7dbb94c docs: readme
+1:  33c682a = 2:  ad17587 chore: add torch and create random tensor
+`
+	if expected != actual {
+		t.Fatalf(fail(expected, actual))
+	}
+}
+
+// removed commit
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid d1) s/5/A/
+	2:  $(test_oid t2) < -:  $(test_oid __) s/4/A/
+	3:  $(test_oid t3) = 2:  $(test_oid d2) s/11/B/
+	4:  $(test_oid t4) = 3:  $(test_oid d3) s/12/B/
+*/
+func TestRangeDiffRemovedCommit(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
+	actual := cmp("a_b_reorder.patch", "a_c_rm_commit.patch")
+	expected := `1:  33c682a < -:  ------- chore: add torch and create random tensor
+2:  22dde12 = 1:  7dbb94c docs: readme`
+2:  22dde12 = 1:  7dbb94c docs: readme
+`
+	if expected != actual {
+		t.Fatalf(fail(expected, actual))
+	}
+}
+
+// added commit
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid a1) s/5/A/
+	2:  $(test_oid t2) = 2:  $(test_oid a2) s/4/A/
+	-:  $(test_oid __) > 3:  $(test_oid a3) s/6/A/
+	3:  $(test_oid t3) = 4:  $(test_oid a4) s/11/B/
+	4:  $(test_oid t4) = 5:  $(test_oid a5) s/12/B/
+*/
+/* func TestRangeDiffAddedCommit(t *testing.T) {
+	actual := ""
+func TestRangeDiffAddedCommit(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_added_commit.patch")
+	expected := `1:  33c682a = 1:  33c682a chore: add torch and create random tensor
+2:  22dde12 = 2:  22dde12 docs: readme
+-:  ------- > 3:  b248060 chore: make tensor 6x6`
+-:  ------- > 3:  b248060 chore: make tensor 6x6
+`
+	if expected != actual {
+		t.Fatalf("expected:%s actual:%s", expected, actual)
+		t.Fatalf(fail(expected, actual))
+	}
+} */
+}
+
+// changed commit
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid c1) s/5/A/
+	2:  $(test_oid t2) = 2:  $(test_oid c2) s/4/A/
+	3:  $(test_oid t3) ! 3:  $(test_oid c3) s/11/B/
+	    @@ file: A
+	      9
+	      10
+	     -11
+	    -+B
+	    ++BB
+	      12
+	      13
+	      14
+	4:  $(test_oid t4) ! 4:  $(test_oid c4) s/12/B/
+	    @@ file
+	     @@ file: A
+	      9
+	      10
+	    - B
+	    + BB
+	     -12
+	     +B
+	      13
+*/
+/* func TestRangeDiffChangedCommit(t *testing.T) {
+	actual := ""
+	fp, err := fixtures.Fixtures.ReadFile("extected_commit_changed.txt")
+func TestRangeDiffChangedCommit(t *testing.T) {
+	actual := cmp("a_b_reorder.patch", "a_c_changed_commit.patch")
+	fp, err := fixtures.Fixtures.ReadFile("expected_commit_changed.txt")
+	if err != nil {
+		t.Fatalf("file not found")
+	}
+	expected := string(fp)
+	if expected != actual {
+		t.Fatalf("expected:%s actual:%s", expected, actual)
+		t.Fatalf(fail(expected, actual))
+	}
+} */
+}
+
+// renamed file
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid n1) s/5/A/
+	2:  $(test_oid t2) ! 2:  $(test_oid n2) s/4/A/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/4/A/
+	    +    s/4/A/ + rename file
+	    Z
+	    - ## file ##
+	    + ## file => renamed-file ##
+	    Z@@
+	    Z 1
+	    Z 2
+	3:  $(test_oid t3) ! 3:  $(test_oid n3) s/11/B/
+	    @@ Metadata
+	    Z ## Commit message ##
+	    Z    s/11/B/
+	    Z
+	    - ## file ##
+	    -@@ file: A
+	    + ## renamed-file ##
+	    +@@ renamed-file: A
+	    Z 8
+	    Z 9
+	    Z 10
+	4:  $(test_oid t4) ! 4:  $(test_oid n4) s/12/B/
+	    @@ Metadata
+	    Z ## Commit message ##
+	    Z    s/12/B/
+	    Z
+	    - ## file ##
+	    -@@ file: A
+	    + ## renamed-file ##
+	    +@@ renamed-file: A
+	    Z 9
+	    Z 10
+	    Z B
+*/
+// func TestRangeDiffRenamedFile(t *testing.T) {}
+
+// file with mode only change
+/*
+	1:  $(test_oid t2) ! 1:  $(test_oid o1) s/4/A/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/4/A/
+	    +    s/4/A/ + add other-file
+	    Z
+	    Z ## file ##
+	    Z@@
+	    @@ file
+	    Z A
+	    Z 6
+	    Z 7
+	    +
+	    + ## other-file (new) ##
+	2:  $(test_oid t3) ! 2:  $(test_oid o2) s/11/B/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/11/B/
+	    +    s/11/B/ + mode change other-file
+	    Z
+	    Z ## file ##
+	    Z@@ file: A
+	    @@ file: A
+	    Z 12
+	    Z 13
+	    Z 14
+	    +
+	    + ## other-file (mode change 100644 => 100755) ##
+	3:  $(test_oid t4) = 3:  $(test_oid o3) s/12/B/
+*/
+// func TestRangeDiffFileWithModeOnlyChange(t *testing.T) {}
+
+// file added and later removed
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid s1) s/5/A/
+	2:  $(test_oid t2) ! 2:  $(test_oid s2) s/4/A/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/4/A/
+	    +    s/4/A/ + new-file
+	    Z
+	    Z ## file ##
+	    Z@@
+	    @@ file
+	    Z A
+	    Z 6
+	    Z 7
+	    +
+	    + ## new-file (new) ##
+	3:  $(test_oid t3) ! 3:  $(test_oid s3) s/11/B/
+	    @@ Metadata
+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
+	    Z
+	    Z ## Commit message ##
+	    -    s/11/B/
+	    +    s/11/B/ + remove file
+	    Z
+	    Z ## file ##
+	    Z@@ file: A
+	    @@ file: A
+	    Z 12
+	    Z 13
+	    Z 14
+	    +
+	    + ## new-file (deleted) ##
+	4:  $(test_oid t4) = 4:  $(test_oid s4) s/12/B/
+*/
+// func TestRangeDiffFileAddedThenRemoved(t *testing.T) {}
+
+// changed message
+/*
+	1:  $(test_oid t1) = 1:  $(test_oid m1) s/5/A/
+	2:  $(test_oid t2) ! 2:  $(test_oid m2) s/4/A/
+	    @@ Metadata
+	    Z ## Commit message ##
+	    Z    s/4/A/
+	    Z
+	    +    Also a silly comment here!
+	    +
+	    Z ## file ##
+	    Z@@
+	    Z 1
+	3:  $(test_oid t3) = 3:  $(test_oid m3) s/11/B/
+	4:  $(test_oid t4) = 4:  $(test_oid m4) s/12/B/
+*/
+// func TestRangeDiffChangedMessage(t *testing.T) {}
util.go
 	return str[idx:], nil
 }
 
-func parsePatchset(patchset io.Reader) ([]*Patch, error) {
+func ParsePatchset(patchset io.Reader) ([]*Patch, error) {
 	patches := []*Patch{}
 	buf := new(strings.Builder)
 	_, err := io.Copy(buf, patchset)
+			Files:         diffFiles,
 		authorEmail = header.Author.Email
 	}
 	content := fmt.Sprintf(
-		"%s\n%s\n%s\n%s\n%s\n",
+		"%s\n%s\n%s\n%s\n",
 		header.Title,
 		header.Body,
 		authorName,
 		authorEmail,
-		header.AuthorDate,
 	)
 	for _, diff := range diffFiles {
 		// we need to ignore diffs with base commit because that depends
util.go
 	return str[idx:], nil
 }
 
-func parsePatchset(patchset io.Reader) ([]*Patch, error) {
+func ParsePatchset(patchset io.Reader) ([]*Patch, error) {
 	patches := []*Patch{}
 	buf := new(strings.Builder)
 	_, err := io.Copy(buf, patchset)
 			ContentSha:    contentSha,
 			RawText:       patchStr,
 			BaseCommitSha: sql.NullString{String: baseCommit},
+			Files:         diffFiles,
 		})
 	}
 
 		authorEmail = header.Author.Email
 	}
 	content := fmt.Sprintf(
-		"%s\n%s\n%s\n%s\n%s\n",
+		"%s\n%s\n%s\n%s\n",
 		header.Title,
 		header.Body,
 		authorName,
 		authorEmail,
-		header.AuthorDate,
 	)
 	for _, diff := range diffFiles {
 		// we need to ignore diffs with base commit because that depends