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 ↕
1: 778bcd8 ! 1: 0f86231 feat: range diff

@@ fixtures/a_b.patch
+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:07:57 -0400
+Subject: [PATCH] chore: add torch and create random tensor
+
+---
+ requirements.txt | 1 +
+ train.py         | 3 +++
+ 2 files changed, 4 insertions(+)
+ create mode 100644 requirements.txt
+
+diff --git a/requirements.txt b/requirements.txt
+new file mode 100644
+index 0000000..4968a39
+--- /dev/null
++++ b/requirements.txt
+@@ -0,0 +1 @@
++torch==2.3.1
+diff --git a/train.py b/train.py
+index 5c027f4..d21dac3 100644
+--- a/train.py
++++ b/train.py
+@@ -1,2 +1,5 @@
++import torch
++
+ if __name__ == "__main__":
+     print("train!")
++    torch.rand(3,6)
+-- 
+2.45.2
+

@@ fixtures/a_b_reorder.patch
+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:07:57 -0400
+Subject: [PATCH 1/2] chore: add torch and create random tensor
+
+---
+ requirements.txt | 1 +
+ train.py         | 3 +++
+ 2 files changed, 4 insertions(+)
+ create mode 100644 requirements.txt
+
+diff --git a/requirements.txt b/requirements.txt
+new file mode 100644
+index 0000000..4968a39
+--- /dev/null
++++ b/requirements.txt
+@@ -0,0 +1 @@
++torch==2.3.1
+diff --git a/train.py b/train.py
+index 5c027f4..d21dac3 100644
+--- a/train.py
++++ b/train.py
+@@ -1,2 +1,5 @@
++import torch
++
+ if __name__ == "__main__":
+     print("train!")
++    torch.rand(3,6)
+-- 
+2.45.2
+
+
+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:14:37 -0400
+Subject: [PATCH 2/2] docs: readme
+
+---
+ README.md | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/README.md b/README.md
+index 8f3a780..3043953 100644
+--- a/README.md
++++ b/README.md
+@@ -1,3 +1,5 @@
+ # Let's build an RNN
+ 
+-This repo demonstrates building an RNN using `pytorch`
++This repo demonstrates building an RNN using `pytorch`.
++
++Here is some more readme information.
+-- 
+2.45.2
+

@@ fixtures/a_c.patch
+From 166848469e0b954c2e14233233f3824a46dcddb8 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:06:00 -0400
+Subject: [PATCH] chore: add torch and create random tensor
+
+---
+ requirements.txt | 1 +
+ train.py         | 3 +++
+ 2 files changed, 4 insertions(+)
+ create mode 100644 requirements.txt
+
+diff --git a/requirements.txt b/requirements.txt
+new file mode 100644
+index 0000000..4968a39
+--- /dev/null
++++ b/requirements.txt
+@@ -0,0 +1 @@
++torch==2.3.1
+diff --git a/train.py b/train.py
+index 5c027f4..d21dac3 100644
+--- a/train.py
++++ b/train.py
+@@ -1,2 +1,5 @@
++import torch
++
+ if __name__ == "__main__":
+     print("train!")
++    torch.rand(3,6)
+
+base-commit: 59456574a0bfee9f71c91c13046173c820152346
+-- 
+2.45.2
+

@@ fixtures/a_c_added_commit.patch
+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:07:57 -0400
+Subject: [PATCH 1/3] chore: add torch and create random tensor
+
+---
+ requirements.txt | 1 +
+ train.py         | 3 +++
+ 2 files changed, 4 insertions(+)
+ create mode 100644 requirements.txt
+
+diff --git a/requirements.txt b/requirements.txt
+new file mode 100644
+index 0000000..4968a39
+--- /dev/null
++++ b/requirements.txt
+@@ -0,0 +1 @@
++torch==2.3.1
+diff --git a/train.py b/train.py
+index 5c027f4..d21dac3 100644
+--- a/train.py
++++ b/train.py
+@@ -1,2 +1,5 @@
++import torch
++
+ if __name__ == "__main__":
+     print("train!")
++    torch.rand(3,6)
+-- 
+2.45.2
+
+
+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:14:37 -0400
+Subject: [PATCH 2/3] docs: readme
+
+---
+ README.md | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/README.md b/README.md
+index 8f3a780..3043953 100644
+--- a/README.md
++++ b/README.md
+@@ -1,3 +1,5 @@
+ # Let's build an RNN
+ 
+-This repo demonstrates building an RNN using `pytorch`
++This repo demonstrates building an RNN using `pytorch`.
++
++Here is some more readme information.
+-- 
+2.45.2
+
+
+From b248060488df529b850060b3c86417bb87d490cc Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:20:44 -0400
+Subject: [PATCH 3/3] chore: make tensor 6x6
+
+---
+ train.py | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/train.py b/train.py
+index d21dac3..8cd47e0 100644
+--- a/train.py
++++ b/train.py
+@@ -2,4 +2,6 @@ import torch
+ 
+ if __name__ == "__main__":
+     print("train!")
+-    torch.rand(3,6)
++    # let's create a 6x6 tensor!
++    tensor = torch.rand(6,6)
++    print(tensor)
+-- 
+2.45.2
+

@@ fixtures/a_c_changed_commit.patch
+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:07:57 -0400
+Subject: [PATCH 1/2] chore: add torch and create random tensor
+
+---
+ requirements.txt | 1 +
+ train.py         | 3 +++
+ 2 files changed, 4 insertions(+)
+ create mode 100644 requirements.txt
+
+diff --git a/requirements.txt b/requirements.txt
+new file mode 100644
+index 0000000..4968a39
+--- /dev/null
++++ b/requirements.txt
+@@ -0,0 +1 @@
++torch==2.3.1
+diff --git a/train.py b/train.py
+index 5c027f4..d21dac3 100644
+--- a/train.py
++++ b/train.py
+@@ -1,2 +1,5 @@
++import torch
++
+ if __name__ == "__main__":
+     print("train!")
++    torch.rand(3,6)
+-- 
+2.45.2
+
+
+From dce20e70280d92aeb88c3d603ad67043ead772fb Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:14:37 -0400
+Subject: [PATCH 2/2] docs: readme
+
+---
+ README.md | 9 ++++++++-
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+diff --git a/README.md b/README.md
+index 8f3a780..ba0293b 100644
+--- a/README.md
++++ b/README.md
+@@ -1,3 +1,10 @@
+ # Let's build an RNN
+ 
+-This repo demonstrates building an RNN using `pytorch`
++This repo demonstrates building an RNN using `pytorch`.
++
++Here is some more readme information.
++
++Here is how to run this project locally:
++
++- install python and pip
++- `pip install -r requirements.txt`
+-- 
+2.45.2
+

@@ fixtures/a_c_reorder.patch
+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:15:23 -0400
+Subject: [PATCH 1/2] docs: readme
+
+---
+ README.md | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/README.md b/README.md
+index 8f3a780..3043953 100644
+--- a/README.md
++++ b/README.md
+@@ -1,3 +1,5 @@
+ # Let's build an RNN
+ 
+-This repo demonstrates building an RNN using `pytorch`
++This repo demonstrates building an RNN using `pytorch`.
++
++Here is some more readme information.
+-- 
+2.45.2
+
+
+From ad175875e2bf320859554bae73743675cc5ce444 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:06:00 -0400
+Subject: [PATCH 2/2] chore: add torch and create random tensor
+
+---
+ requirements.txt | 1 +
+ train.py         | 3 +++
+ 2 files changed, 4 insertions(+)
+ create mode 100644 requirements.txt
+
+diff --git a/requirements.txt b/requirements.txt
+new file mode 100644
+index 0000000..4968a39
+--- /dev/null
++++ b/requirements.txt
+@@ -0,0 +1 @@
++torch==2.3.1
+diff --git a/train.py b/train.py
+index 5c027f4..d21dac3 100644
+--- a/train.py
++++ b/train.py
+@@ -1,2 +1,5 @@
++import torch
++
+ if __name__ == "__main__":
+     print("train!")
++    torch.rand(3,6)
+-- 
+2.45.2
+

@@ fixtures/a_c_rm_commit.patch
+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
+From: Eric Bower <me@erock.io>
+Date: Tue, 23 Jul 2024 10:15:23 -0400
+Subject: [PATCH] docs: readme
+
+---
+ README.md | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/README.md b/README.md
+index 8f3a780..3043953 100644
+--- a/README.md
++++ b/README.md
+@@ -1,3 +1,5 @@
+ # Let's build an RNN
+ 
+-This repo demonstrates building an RNN using `pytorch`
++This repo demonstrates building an RNN using `pytorch`.
++
++Here is some more readme information.
+-- 
+2.45.2
+

@@ fixtures/expected_commit_changed.txt
+1:  33c682a = 1:  33c682a chore: add torch and create random tensor
+2:  22dde12 ! 2:  0185f34 docs: readme
+    @@ README.md
+     +This repo demonstrates building an RNN using `pytorch`.
+     +
+     +Here is some more readme information.
+    ++
+    ++Here is how to run this project locally:
+    ++
+    ++- install python and pip
+    ++- `pip install -r requirements.txt`

@@ go.mod
 	github.com/knadh/koanf/providers/env v0.1.0
 	github.com/knadh/koanf/providers/file v1.0.0
 	github.com/knadh/koanf/v2 v2.1.1
+	github.com/sergi/go-diff v1.1.0
 	github.com/urfave/cli/v2 v2.27.2
 	golang.org/x/crypto v0.21.0
 	modernc.org/sqlite v1.27.0

@@ go.sum
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
-github.com/bluekeyes/go-gitdiff v0.7.2 h1:42jrcVZdjjxXtVsFNYTo/I6T1ZvIiQL+iDDLiH904hw=
-github.com/bluekeyes/go-gitdiff v0.7.2/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190 h1:k6Ep4yQtmsoP/St4bf7ofXyWc6ITB/FyGy9ewaAn5os=
 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
 github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
 github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
 github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
 golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=

@@ pr.go
 		_ = tx.Rollback()
 	}()
 
-	patches, err := parsePatchset(patchset)
+	patches, err := ParsePatchset(patchset)
 	if err != nil {
 		return nil, err
 	}
 		_ = tx.Rollback()
 	}()
 
-	patches, err := parsePatchset(patchset)
+	patches, err := ParsePatchset(patchset)
 	if err != nil {
 		return fin, err
 	}

@@ 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) {}

@@ 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

@@ util_test.go
 	if err != nil {
 		t.Fatalf(err.Error())
 	}
-	actual, err := parsePatchset(file)
+	actual, err := ParsePatchset(file)
 	if err != nil {
 		t.Fatalf(err.Error())
 	}
ps-51 by erock on 2024-10-18T20:36:21Z

feat: range diff

db.go link
+6 -0
 1diff --git a/db.go b/db.go
 2index 291ccc6..7323e8a 100644
 3--- a/db.go
 4+++ b/db.go
 5@@ -6,6 +6,7 @@ import (
 6 	"log/slog"
 7 	"time"
 8 
 9+	"github.com/bluekeyes/go-gitdiff/gitdiff"
10 	"github.com/jmoiron/sqlx"
11 	_ "modernc.org/sqlite"
12 )
13@@ -67,6 +68,11 @@ type Patch struct {
14 	BaseCommitSha sql.NullString `db:"base_commit_sha"`
15 	RawText       string         `db:"raw_text"`
16 	CreatedAt     time.Time      `db:"created_at"`
17+	Files         []*gitdiff.File
18+}
19+
20+func (p *Patch) CalcDiff() string {
21+	return p.RawText
22 }
23 
24 // EventLog is a event log for RSS or other notification systems.
fixtures/a_b.patch link
+31 -0
 1diff --git a/fixtures/a_b.patch b/fixtures/a_b.patch
 2new file mode 100644
 3index 0000000..c2c5167
 4--- /dev/null
 5+++ b/fixtures/a_b.patch
 6@@ -0,0 +1,31 @@
 7+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:07:57 -0400
10+Subject: [PATCH] chore: add torch and create random tensor
11+
12+---
13+ requirements.txt | 1 +
14+ train.py         | 3 +++
15+ 2 files changed, 4 insertions(+)
16+ create mode 100644 requirements.txt
17+
18+diff --git a/requirements.txt b/requirements.txt
19+new file mode 100644
20+index 0000000..4968a39
21+--- /dev/null
22++++ b/requirements.txt
23+@@ -0,0 +1 @@
24++torch==2.3.1
25+diff --git a/train.py b/train.py
26+index 5c027f4..d21dac3 100644
27+--- a/train.py
28++++ b/train.py
29+@@ -1,2 +1,5 @@
30++import torch
31++
32+ if __name__ == "__main__":
33+     print("train!")
34++    torch.rand(3,6)
35+-- 
36+2.45.2
37+
fixtures/a_b_reorder.patch link
+55 -0
 1diff --git a/fixtures/a_b_reorder.patch b/fixtures/a_b_reorder.patch
 2new file mode 100644
 3index 0000000..2524df9
 4--- /dev/null
 5+++ b/fixtures/a_b_reorder.patch
 6@@ -0,0 +1,55 @@
 7+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:07:57 -0400
10+Subject: [PATCH 1/2] chore: add torch and create random tensor
11+
12+---
13+ requirements.txt | 1 +
14+ train.py         | 3 +++
15+ 2 files changed, 4 insertions(+)
16+ create mode 100644 requirements.txt
17+
18+diff --git a/requirements.txt b/requirements.txt
19+new file mode 100644
20+index 0000000..4968a39
21+--- /dev/null
22++++ b/requirements.txt
23+@@ -0,0 +1 @@
24++torch==2.3.1
25+diff --git a/train.py b/train.py
26+index 5c027f4..d21dac3 100644
27+--- a/train.py
28++++ b/train.py
29+@@ -1,2 +1,5 @@
30++import torch
31++
32+ if __name__ == "__main__":
33+     print("train!")
34++    torch.rand(3,6)
35+-- 
36+2.45.2
37+
38+
39+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
40+From: Eric Bower <me@erock.io>
41+Date: Tue, 23 Jul 2024 10:14:37 -0400
42+Subject: [PATCH 2/2] docs: readme
43+
44+---
45+ README.md | 4 +++-
46+ 1 file changed, 3 insertions(+), 1 deletion(-)
47+
48+diff --git a/README.md b/README.md
49+index 8f3a780..3043953 100644
50+--- a/README.md
51++++ b/README.md
52+@@ -1,3 +1,5 @@
53+ # Let's build an RNN
54+ 
55+-This repo demonstrates building an RNN using `pytorch`
56++This repo demonstrates building an RNN using `pytorch`.
57++
58++Here is some more readme information.
59+-- 
60+2.45.2
61+
fixtures/a_c.patch link
+33 -0
 1diff --git a/fixtures/a_c.patch b/fixtures/a_c.patch
 2new file mode 100644
 3index 0000000..4960260
 4--- /dev/null
 5+++ b/fixtures/a_c.patch
 6@@ -0,0 +1,33 @@
 7+From 166848469e0b954c2e14233233f3824a46dcddb8 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:06:00 -0400
10+Subject: [PATCH] chore: add torch and create random tensor
11+
12+---
13+ requirements.txt | 1 +
14+ train.py         | 3 +++
15+ 2 files changed, 4 insertions(+)
16+ create mode 100644 requirements.txt
17+
18+diff --git a/requirements.txt b/requirements.txt
19+new file mode 100644
20+index 0000000..4968a39
21+--- /dev/null
22++++ b/requirements.txt
23+@@ -0,0 +1 @@
24++torch==2.3.1
25+diff --git a/train.py b/train.py
26+index 5c027f4..d21dac3 100644
27+--- a/train.py
28++++ b/train.py
29+@@ -1,2 +1,5 @@
30++import torch
31++
32+ if __name__ == "__main__":
33+     print("train!")
34++    torch.rand(3,6)
35+
36+base-commit: 59456574a0bfee9f71c91c13046173c820152346
37+-- 
38+2.45.2
39+
fixtures/a_c_added_commit.patch link
+80 -0
 1diff --git a/fixtures/a_c_added_commit.patch b/fixtures/a_c_added_commit.patch
 2new file mode 100644
 3index 0000000..f5ce9f3
 4--- /dev/null
 5+++ b/fixtures/a_c_added_commit.patch
 6@@ -0,0 +1,80 @@
 7+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:07:57 -0400
10+Subject: [PATCH 1/3] chore: add torch and create random tensor
11+
12+---
13+ requirements.txt | 1 +
14+ train.py         | 3 +++
15+ 2 files changed, 4 insertions(+)
16+ create mode 100644 requirements.txt
17+
18+diff --git a/requirements.txt b/requirements.txt
19+new file mode 100644
20+index 0000000..4968a39
21+--- /dev/null
22++++ b/requirements.txt
23+@@ -0,0 +1 @@
24++torch==2.3.1
25+diff --git a/train.py b/train.py
26+index 5c027f4..d21dac3 100644
27+--- a/train.py
28++++ b/train.py
29+@@ -1,2 +1,5 @@
30++import torch
31++
32+ if __name__ == "__main__":
33+     print("train!")
34++    torch.rand(3,6)
35+-- 
36+2.45.2
37+
38+
39+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
40+From: Eric Bower <me@erock.io>
41+Date: Tue, 23 Jul 2024 10:14:37 -0400
42+Subject: [PATCH 2/3] docs: readme
43+
44+---
45+ README.md | 4 +++-
46+ 1 file changed, 3 insertions(+), 1 deletion(-)
47+
48+diff --git a/README.md b/README.md
49+index 8f3a780..3043953 100644
50+--- a/README.md
51++++ b/README.md
52+@@ -1,3 +1,5 @@
53+ # Let's build an RNN
54+ 
55+-This repo demonstrates building an RNN using `pytorch`
56++This repo demonstrates building an RNN using `pytorch`.
57++
58++Here is some more readme information.
59+-- 
60+2.45.2
61+
62+
63+From b248060488df529b850060b3c86417bb87d490cc Mon Sep 17 00:00:00 2001
64+From: Eric Bower <me@erock.io>
65+Date: Tue, 23 Jul 2024 10:20:44 -0400
66+Subject: [PATCH 3/3] chore: make tensor 6x6
67+
68+---
69+ train.py | 4 +++-
70+ 1 file changed, 3 insertions(+), 1 deletion(-)
71+
72+diff --git a/train.py b/train.py
73+index d21dac3..8cd47e0 100644
74+--- a/train.py
75++++ b/train.py
76+@@ -2,4 +2,6 @@ import torch
77+ 
78+ if __name__ == "__main__":
79+     print("train!")
80+-    torch.rand(3,6)
81++    # let's create a 6x6 tensor!
82++    tensor = torch.rand(6,6)
83++    print(tensor)
84+-- 
85+2.45.2
86+
fixtures/a_c_changed_commit.patch link
+60 -0
 1diff --git a/fixtures/a_c_changed_commit.patch b/fixtures/a_c_changed_commit.patch
 2new file mode 100644
 3index 0000000..259a434
 4--- /dev/null
 5+++ b/fixtures/a_c_changed_commit.patch
 6@@ -0,0 +1,60 @@
 7+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:07:57 -0400
10+Subject: [PATCH 1/2] chore: add torch and create random tensor
11+
12+---
13+ requirements.txt | 1 +
14+ train.py         | 3 +++
15+ 2 files changed, 4 insertions(+)
16+ create mode 100644 requirements.txt
17+
18+diff --git a/requirements.txt b/requirements.txt
19+new file mode 100644
20+index 0000000..4968a39
21+--- /dev/null
22++++ b/requirements.txt
23+@@ -0,0 +1 @@
24++torch==2.3.1
25+diff --git a/train.py b/train.py
26+index 5c027f4..d21dac3 100644
27+--- a/train.py
28++++ b/train.py
29+@@ -1,2 +1,5 @@
30++import torch
31++
32+ if __name__ == "__main__":
33+     print("train!")
34++    torch.rand(3,6)
35+-- 
36+2.45.2
37+
38+
39+From dce20e70280d92aeb88c3d603ad67043ead772fb Mon Sep 17 00:00:00 2001
40+From: Eric Bower <me@erock.io>
41+Date: Tue, 23 Jul 2024 10:14:37 -0400
42+Subject: [PATCH 2/2] docs: readme
43+
44+---
45+ README.md | 9 ++++++++-
46+ 1 file changed, 8 insertions(+), 1 deletion(-)
47+
48+diff --git a/README.md b/README.md
49+index 8f3a780..ba0293b 100644
50+--- a/README.md
51++++ b/README.md
52+@@ -1,3 +1,10 @@
53+ # Let's build an RNN
54+ 
55+-This repo demonstrates building an RNN using `pytorch`
56++This repo demonstrates building an RNN using `pytorch`.
57++
58++Here is some more readme information.
59++
60++Here is how to run this project locally:
61++
62++- install python and pip
63++- `pip install -r requirements.txt`
64+-- 
65+2.45.2
66+
fixtures/a_c_reorder.patch link
+55 -0
 1diff --git a/fixtures/a_c_reorder.patch b/fixtures/a_c_reorder.patch
 2new file mode 100644
 3index 0000000..bc90f03
 4--- /dev/null
 5+++ b/fixtures/a_c_reorder.patch
 6@@ -0,0 +1,55 @@
 7+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:15:23 -0400
10+Subject: [PATCH 1/2] docs: readme
11+
12+---
13+ README.md | 4 +++-
14+ 1 file changed, 3 insertions(+), 1 deletion(-)
15+
16+diff --git a/README.md b/README.md
17+index 8f3a780..3043953 100644
18+--- a/README.md
19++++ b/README.md
20+@@ -1,3 +1,5 @@
21+ # Let's build an RNN
22+ 
23+-This repo demonstrates building an RNN using `pytorch`
24++This repo demonstrates building an RNN using `pytorch`.
25++
26++Here is some more readme information.
27+-- 
28+2.45.2
29+
30+
31+From ad175875e2bf320859554bae73743675cc5ce444 Mon Sep 17 00:00:00 2001
32+From: Eric Bower <me@erock.io>
33+Date: Tue, 23 Jul 2024 10:06:00 -0400
34+Subject: [PATCH 2/2] chore: add torch and create random tensor
35+
36+---
37+ requirements.txt | 1 +
38+ train.py         | 3 +++
39+ 2 files changed, 4 insertions(+)
40+ create mode 100644 requirements.txt
41+
42+diff --git a/requirements.txt b/requirements.txt
43+new file mode 100644
44+index 0000000..4968a39
45+--- /dev/null
46++++ b/requirements.txt
47+@@ -0,0 +1 @@
48++torch==2.3.1
49+diff --git a/train.py b/train.py
50+index 5c027f4..d21dac3 100644
51+--- a/train.py
52++++ b/train.py
53+@@ -1,2 +1,5 @@
54++import torch
55++
56+ if __name__ == "__main__":
57+     print("train!")
58++    torch.rand(3,6)
59+-- 
60+2.45.2
61+
fixtures/a_c_rm_commit.patch link
+23 -0
 1diff --git a/fixtures/a_c_rm_commit.patch b/fixtures/a_c_rm_commit.patch
 2new file mode 100644
 3index 0000000..c6a4c5e
 4--- /dev/null
 5+++ b/fixtures/a_c_rm_commit.patch
 6@@ -0,0 +1,23 @@
 7+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
 8+From: Eric Bower <me@erock.io>
 9+Date: Tue, 23 Jul 2024 10:15:23 -0400
10+Subject: [PATCH] docs: readme
11+
12+---
13+ README.md | 4 +++-
14+ 1 file changed, 3 insertions(+), 1 deletion(-)
15+
16+diff --git a/README.md b/README.md
17+index 8f3a780..3043953 100644
18+--- a/README.md
19++++ b/README.md
20+@@ -1,3 +1,5 @@
21+ # Let's build an RNN
22+ 
23+-This repo demonstrates building an RNN using `pytorch`
24++This repo demonstrates building an RNN using `pytorch`.
25++
26++Here is some more readme information.
27+-- 
28+2.45.2
29+
fixtures/expected_commit_changed.txt link
+11 -0
 1diff --git a/fixtures/expected_commit_changed.txt b/fixtures/expected_commit_changed.txt
 2new file mode 100644
 3index 0000000..ac4499c
 4--- /dev/null
 5+++ b/fixtures/expected_commit_changed.txt
 6@@ -0,0 +1,11 @@
 7+1:  33c682a = 1:  33c682a chore: add torch and create random tensor
 8+2:  22dde12 ! 2:  0185f34 docs: readme
 9+    @@ README.md
10+     +This repo demonstrates building an RNN using `pytorch`.
11+     +
12+     +Here is some more readme information.
13+    ++
14+    ++Here is how to run this project locally:
15+    ++
16+    ++- install python and pip
17+    ++- `pip install -r requirements.txt`
go.mod link
+1 -0
 1diff --git a/go.mod b/go.mod
 2index 4dc95e6..53504d3 100644
 3--- a/go.mod
 4+++ b/go.mod
 5@@ -14,6 +14,7 @@ require (
 6 	github.com/knadh/koanf/providers/env v0.1.0
 7 	github.com/knadh/koanf/providers/file v1.0.0
 8 	github.com/knadh/koanf/v2 v2.1.1
 9+	github.com/sergi/go-diff v1.1.0
10 	github.com/urfave/cli/v2 v2.27.2
11 	golang.org/x/crypto v0.21.0
12 	modernc.org/sqlite v1.27.0
go.sum link
+13 -2
 1diff --git a/go.sum b/go.sum
 2index 1ec46c6..5bc1160 100644
 3--- a/go.sum
 4+++ b/go.sum
 5@@ -8,8 +8,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
 6 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 7 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 8 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 9-github.com/bluekeyes/go-gitdiff v0.7.2 h1:42jrcVZdjjxXtVsFNYTo/I6T1ZvIiQL+iDDLiH904hw=
10-github.com/bluekeyes/go-gitdiff v0.7.2/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
11 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190 h1:k6Ep4yQtmsoP/St4bf7ofXyWc6ITB/FyGy9ewaAn5os=
12 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
13 github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
14@@ -36,6 +34,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV
15 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
16 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
17 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
18+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
20 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
22@@ -74,8 +73,11 @@ github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcnc
23 github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
24 github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
25 github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
26+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
27 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
28 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
29+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
30+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
31 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
32 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
33 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
34@@ -107,6 +109,7 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo
35 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
36 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
37 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
38+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
39 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
40 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
41 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
42@@ -119,6 +122,10 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
43 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
44 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
45 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
46+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
47+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
48+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
49+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
50 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
51 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
52 github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
53@@ -143,6 +150,10 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
54 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
55 golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
56 golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
57+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
58+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
59+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
60+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
61 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
62 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
63 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
pr.go link
+2 -2
 1diff --git a/pr.go b/pr.go
 2index 9e535a0..984fe92 100644
 3--- a/pr.go
 4+++ b/pr.go
 5@@ -447,7 +447,7 @@ func (cmd PrCmd) SubmitPatchRequest(repoID string, userID int64, patchset io.Rea
 6 		_ = tx.Rollback()
 7 	}()
 8 
 9-	patches, err := parsePatchset(patchset)
10+	patches, err := ParsePatchset(patchset)
11 	if err != nil {
12 		return nil, err
13 	}
14@@ -541,7 +541,7 @@ func (cmd PrCmd) SubmitPatchset(prID int64, userID int64, op PatchsetOp, patchse
15 		_ = tx.Rollback()
16 	}()
17 
18-	patches, err := parsePatchset(patchset)
19+	patches, err := ParsePatchset(patchset)
20 	if err != nil {
21 		return fin, err
22 	}
range_diff.go link
+219 -0
  1diff --git a/range_diff.go b/range_diff.go
  2new file mode 100644
  3index 0000000..0df9b3e
  4--- /dev/null
  5+++ b/range_diff.go
  6@@ -0,0 +1,219 @@
  7+package git
  8+
  9+import (
 10+	"fmt"
 11+	"math"
 12+
 13+	"github.com/sergi/go-diff/diffmatchpatch"
 14+)
 15+
 16+var COST_MAX = 65536
 17+var RANGE_DIFF_CREATION_FACTOR_DEFAULT = 60
 18+
 19+type PatchRange struct {
 20+	*Patch
 21+	Matching int
 22+	Diff     string
 23+	DiffSize int
 24+	Shown    bool
 25+}
 26+
 27+func NewPatchRange(patch *Patch) *PatchRange {
 28+	diff := patch.CalcDiff()
 29+	return &PatchRange{
 30+		Patch:    patch,
 31+		Matching: -1,
 32+		Diff:     diff,
 33+		DiffSize: len(diff),
 34+		Shown:    false,
 35+	}
 36+}
 37+
 38+func output(a []*PatchRange, b []*PatchRange) string {
 39+	out := ""
 40+	for i, patchA := range a {
 41+		if patchA.Matching == -1 {
 42+			out += outputPairHeader(patchA, nil, i+1, -1)
 43+		}
 44+	}
 45+
 46+	for j, patchB := range b {
 47+		if patchB.Matching == -1 {
 48+			out += outputPairHeader(nil, patchB, -1, j+1)
 49+			continue
 50+		}
 51+		patchA := a[patchB.Matching]
 52+		if patchB.ContentSha == patchA.ContentSha {
 53+			out += outputPairHeader(patchA, patchB, patchB.Matching+1, patchA.Matching+1)
 54+		}
 55+	}
 56+	return out
 57+}
 58+
 59+func outputPairHeader(a *PatchRange, b *PatchRange, aIndex, bIndex int) string {
 60+	if a == nil {
 61+		return fmt.Sprintf("-:  ------- > %d:  %s %s\n", bIndex, truncateSha(b.CommitSha), b.Title)
 62+	}
 63+	if b == nil {
 64+		return fmt.Sprintf("%d:  %s < -:  ------- %s\n", aIndex, truncateSha(a.CommitSha), a.Title)
 65+	}
 66+	return fmt.Sprintf("%d:  %s = %d:  %s %s\n", aIndex, truncateSha(a.CommitSha), bIndex, truncateSha(b.CommitSha), a.Title)
 67+}
 68+
 69+func RangeDiff(a []*Patch, b []*Patch) string {
 70+	aPatches := []*PatchRange{}
 71+	for _, patch := range a {
 72+		aPatches = append(aPatches, NewPatchRange(patch))
 73+	}
 74+	bPatches := []*PatchRange{}
 75+	for _, patch := range b {
 76+		bPatches = append(bPatches, NewPatchRange(patch))
 77+	}
 78+	findExactMatches(aPatches, bPatches)
 79+	getCorrespondences(aPatches, bPatches, RANGE_DIFF_CREATION_FACTOR_DEFAULT)
 80+	return output(aPatches, bPatches)
 81+}
 82+
 83+func findExactMatches(a []*PatchRange, b []*PatchRange) {
 84+	for i, patchA := range a {
 85+		for j, patchB := range b {
 86+			if patchA.ContentSha == patchB.ContentSha {
 87+				patchA.Matching = j
 88+				patchB.Matching = i
 89+			}
 90+		}
 91+	}
 92+}
 93+
 94+func createMatrix(rows, cols int) [][]int {
 95+	mat := make([][]int, rows)
 96+	for i := range mat {
 97+		mat[i] = make([]int, cols)
 98+	}
 99+	return mat
100+}
101+
102+func diffsize(a *PatchRange, b *PatchRange) int {
103+	dmp := diffmatchpatch.New()
104+	diffs := dmp.DiffMain(a.Diff, b.Diff, false)
105+	return len(dmp.DiffPrettyText(diffs))
106+}
107+
108+func getCorrespondences(a []*PatchRange, b []*PatchRange, creationFactor int) {
109+	n := len(a) + len(b)
110+	fmt.Println("rows", len(a), "cols", len(b))
111+	cost := createMatrix(n, n)
112+
113+	for i, patchA := range a {
114+		var c int
115+		for j, patchB := range b {
116+			if patchA.Matching == j {
117+				c = 0
118+			} else if patchA.Matching == -1 && patchB.Matching == -1 {
119+				c = diffsize(patchA, patchB)
120+			} else {
121+				c = COST_MAX
122+			}
123+			cost[i][j] = c
124+		}
125+	}
126+
127+	for j, patchB := range b {
128+		creationCost := (patchB.DiffSize * creationFactor) / 100
129+		if patchB.Matching >= 0 {
130+			creationCost = math.MaxInt32
131+		}
132+		for i := len(a); i < n; i++ {
133+			cost[i][j] = creationCost
134+		}
135+	}
136+
137+	for i := len(a); i < n; i++ {
138+		for j := len(b); j < n; j++ {
139+			cost[i][j] = 0
140+		}
141+	}
142+
143+	assignment := computeAssignment(cost, n, n)
144+	for i, j := range assignment {
145+		if i < len(a) && j < len(b) {
146+			a[i].Matching = j
147+			b[j].Matching = i
148+		}
149+	}
150+
151+	fmt.Println("cost", cost, "assignment", assignment)
152+}
153+
154+// computeAssignment assigns patches using the Hungarian algorithm.
155+func computeAssignment(costMatrix [][]int, m, n int) []int {
156+	u := make([]int, m+1) // potential for workers
157+	v := make([]int, n+1) // potential for jobs
158+	p := make([]int, n+1) // job assignment
159+	way := make([]int, n+1)
160+
161+	for i := 1; i <= m; i++ {
162+		links := make([]int, n+1)
163+		minV := make([]int, n+1)
164+		used := make([]bool, n+1)
165+		for j := 0; j <= n; j++ {
166+			minV[j] = math.MaxInt32
167+			used[j] = false
168+		}
169+
170+		j0 := 0
171+		p[0] = i
172+
173+		for {
174+			used[j0] = true
175+			i0 := p[j0]
176+			delta := math.MaxInt32
177+			j1 := 0
178+
179+			for j := 1; j <= n; j++ {
180+				if !used[j] {
181+					cur := costMatrix[i0-1][j-1] - u[i0] - v[j]
182+					if cur < minV[j] {
183+						minV[j] = cur
184+						links[j] = j0
185+					}
186+					if minV[j] < delta {
187+						delta = minV[j]
188+						j1 = j
189+					}
190+				}
191+			}
192+
193+			for j := 0; j <= n; j++ {
194+				if used[j] {
195+					u[p[j]] += delta
196+					v[j] -= delta
197+				} else {
198+					minV[j] -= delta
199+				}
200+			}
201+
202+			j0 = j1
203+			if p[j0] == 0 {
204+				break
205+			}
206+		}
207+
208+		for {
209+			j1 := way[j0]
210+			p[j0] = p[j1]
211+			j0 = j1
212+			if j0 == 0 {
213+				break
214+			}
215+		}
216+	}
217+
218+	assignment := make([]int, m)
219+	for j := 1; j <= n; j++ {
220+		if p[j] > 0 {
221+			assignment[p[j]-1] = j - 1
222+		}
223+	}
224+	return assignment
225+}
range_diff_test.go link
+276 -0
  1diff --git a/range_diff_test.go b/range_diff_test.go
  2new file mode 100644
  3index 0000000..3a67952
  4--- /dev/null
  5+++ b/range_diff_test.go
  6@@ -0,0 +1,276 @@
  7+package git
  8+
  9+import (
 10+	"fmt"
 11+	"testing"
 12+
 13+	"github.com/picosh/git-pr/fixtures"
 14+)
 15+
 16+func bail(err error) {
 17+	if err != nil {
 18+		panic(bail)
 19+	}
 20+}
 21+
 22+func cmp(afile, bfile string) string {
 23+	a, err := fixtures.Fixtures.Open(afile)
 24+	bail(err)
 25+	b, err := fixtures.Fixtures.Open(bfile)
 26+	bail(err)
 27+	aPatches, err := ParsePatchset(a)
 28+	bail(err)
 29+	bPatches, err := ParsePatchset(b)
 30+	bail(err)
 31+	actual := RangeDiff(aPatches, bPatches)
 32+	return actual
 33+}
 34+
 35+func fail(expected, actual string) string {
 36+	return fmt.Sprintf("expected:[%s] actual:[%s]", expected, actual)
 37+}
 38+
 39+// https://git.kernel.org/tree/t/t3206-range-diff.sh?id=d19b6cd2dd72dc811f19df4b32c7ed223256c3ee
 40+
 41+// simple A..B A..C (unmodified)
 42+/*
 43+	1:  $(test_oid t1) = 1:  $(test_oid u1) s/5/A/
 44+	2:  $(test_oid t2) = 2:  $(test_oid u2) s/4/A/
 45+	3:  $(test_oid t3) = 3:  $(test_oid u3) s/11/B/
 46+	4:  $(test_oid t4) = 4:  $(test_oid u4) s/12/B/
 47+*/
 48+func TestRangeDiffUnmodified(t *testing.T) {
 49+	actual := cmp("a_b.patch", "a_c.patch")
 50+	expected := "1:  33c682a = 1:  1668484 chore: add torch and create random tensor\n"
 51+	if expected != actual {
 52+		t.Fatalf(fail(expected, actual))
 53+	}
 54+}
 55+
 56+// trivial reordering
 57+/*
 58+	1:  $(test_oid t1) = 1:  $(test_oid r1) s/5/A/
 59+	3:  $(test_oid t3) = 2:  $(test_oid r2) s/11/B/
 60+	4:  $(test_oid t4) = 3:  $(test_oid r3) s/12/B/
 61+	2:  $(test_oid t2) = 4:  $(test_oid r4) s/4/A/
 62+*/
 63+func TestRangeDiffTrivialReordering(t *testing.T) {
 64+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
 65+	expected := `2:  22dde12 = 1:  7dbb94c docs: readme
 66+1:  33c682a = 2:  ad17587 chore: add torch and create random tensor
 67+`
 68+	if expected != actual {
 69+		t.Fatalf(fail(expected, actual))
 70+	}
 71+}
 72+
 73+// removed commit
 74+/*
 75+	1:  $(test_oid t1) = 1:  $(test_oid d1) s/5/A/
 76+	2:  $(test_oid t2) < -:  $(test_oid __) s/4/A/
 77+	3:  $(test_oid t3) = 2:  $(test_oid d2) s/11/B/
 78+	4:  $(test_oid t4) = 3:  $(test_oid d3) s/12/B/
 79+*/
 80+func TestRangeDiffRemovedCommit(t *testing.T) {
 81+	actual := cmp("a_b_reorder.patch", "a_c_rm_commit.patch")
 82+	expected := `1:  33c682a < -:  ------- chore: add torch and create random tensor
 83+2:  22dde12 = 1:  7dbb94c docs: readme
 84+`
 85+	if expected != actual {
 86+		t.Fatalf(fail(expected, actual))
 87+	}
 88+}
 89+
 90+// added commit
 91+/*
 92+	1:  $(test_oid t1) = 1:  $(test_oid a1) s/5/A/
 93+	2:  $(test_oid t2) = 2:  $(test_oid a2) s/4/A/
 94+	-:  $(test_oid __) > 3:  $(test_oid a3) s/6/A/
 95+	3:  $(test_oid t3) = 4:  $(test_oid a4) s/11/B/
 96+	4:  $(test_oid t4) = 5:  $(test_oid a5) s/12/B/
 97+*/
 98+func TestRangeDiffAddedCommit(t *testing.T) {
 99+	actual := cmp("a_b_reorder.patch", "a_c_added_commit.patch")
100+	expected := `1:  33c682a = 1:  33c682a chore: add torch and create random tensor
101+2:  22dde12 = 2:  22dde12 docs: readme
102+-:  ------- > 3:  b248060 chore: make tensor 6x6
103+`
104+	if expected != actual {
105+		t.Fatalf(fail(expected, actual))
106+	}
107+}
108+
109+// changed commit
110+/*
111+	1:  $(test_oid t1) = 1:  $(test_oid c1) s/5/A/
112+	2:  $(test_oid t2) = 2:  $(test_oid c2) s/4/A/
113+	3:  $(test_oid t3) ! 3:  $(test_oid c3) s/11/B/
114+	    @@ file: A
115+	      9
116+	      10
117+	     -11
118+	    -+B
119+	    ++BB
120+	      12
121+	      13
122+	      14
123+	4:  $(test_oid t4) ! 4:  $(test_oid c4) s/12/B/
124+	    @@ file
125+	     @@ file: A
126+	      9
127+	      10
128+	    - B
129+	    + BB
130+	     -12
131+	     +B
132+	      13
133+*/
134+func TestRangeDiffChangedCommit(t *testing.T) {
135+	actual := cmp("a_b_reorder.patch", "a_c_changed_commit.patch")
136+	fp, err := fixtures.Fixtures.ReadFile("expected_commit_changed.txt")
137+	if err != nil {
138+		t.Fatalf("file not found")
139+	}
140+	expected := string(fp)
141+	if expected != actual {
142+		t.Fatalf(fail(expected, actual))
143+	}
144+}
145+
146+// renamed file
147+/*
148+	1:  $(test_oid t1) = 1:  $(test_oid n1) s/5/A/
149+	2:  $(test_oid t2) ! 2:  $(test_oid n2) s/4/A/
150+	    @@ Metadata
151+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
152+	    Z
153+	    Z ## Commit message ##
154+	    -    s/4/A/
155+	    +    s/4/A/ + rename file
156+	    Z
157+	    - ## file ##
158+	    + ## file => renamed-file ##
159+	    Z@@
160+	    Z 1
161+	    Z 2
162+	3:  $(test_oid t3) ! 3:  $(test_oid n3) s/11/B/
163+	    @@ Metadata
164+	    Z ## Commit message ##
165+	    Z    s/11/B/
166+	    Z
167+	    - ## file ##
168+	    -@@ file: A
169+	    + ## renamed-file ##
170+	    +@@ renamed-file: A
171+	    Z 8
172+	    Z 9
173+	    Z 10
174+	4:  $(test_oid t4) ! 4:  $(test_oid n4) s/12/B/
175+	    @@ Metadata
176+	    Z ## Commit message ##
177+	    Z    s/12/B/
178+	    Z
179+	    - ## file ##
180+	    -@@ file: A
181+	    + ## renamed-file ##
182+	    +@@ renamed-file: A
183+	    Z 9
184+	    Z 10
185+	    Z B
186+*/
187+// func TestRangeDiffRenamedFile(t *testing.T) {}
188+
189+// file with mode only change
190+/*
191+	1:  $(test_oid t2) ! 1:  $(test_oid o1) s/4/A/
192+	    @@ Metadata
193+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
194+	    Z
195+	    Z ## Commit message ##
196+	    -    s/4/A/
197+	    +    s/4/A/ + add other-file
198+	    Z
199+	    Z ## file ##
200+	    Z@@
201+	    @@ file
202+	    Z A
203+	    Z 6
204+	    Z 7
205+	    +
206+	    + ## other-file (new) ##
207+	2:  $(test_oid t3) ! 2:  $(test_oid o2) s/11/B/
208+	    @@ Metadata
209+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
210+	    Z
211+	    Z ## Commit message ##
212+	    -    s/11/B/
213+	    +    s/11/B/ + mode change other-file
214+	    Z
215+	    Z ## file ##
216+	    Z@@ file: A
217+	    @@ file: A
218+	    Z 12
219+	    Z 13
220+	    Z 14
221+	    +
222+	    + ## other-file (mode change 100644 => 100755) ##
223+	3:  $(test_oid t4) = 3:  $(test_oid o3) s/12/B/
224+*/
225+// func TestRangeDiffFileWithModeOnlyChange(t *testing.T) {}
226+
227+// file added and later removed
228+/*
229+	1:  $(test_oid t1) = 1:  $(test_oid s1) s/5/A/
230+	2:  $(test_oid t2) ! 2:  $(test_oid s2) s/4/A/
231+	    @@ Metadata
232+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
233+	    Z
234+	    Z ## Commit message ##
235+	    -    s/4/A/
236+	    +    s/4/A/ + new-file
237+	    Z
238+	    Z ## file ##
239+	    Z@@
240+	    @@ file
241+	    Z A
242+	    Z 6
243+	    Z 7
244+	    +
245+	    + ## new-file (new) ##
246+	3:  $(test_oid t3) ! 3:  $(test_oid s3) s/11/B/
247+	    @@ Metadata
248+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
249+	    Z
250+	    Z ## Commit message ##
251+	    -    s/11/B/
252+	    +    s/11/B/ + remove file
253+	    Z
254+	    Z ## file ##
255+	    Z@@ file: A
256+	    @@ file: A
257+	    Z 12
258+	    Z 13
259+	    Z 14
260+	    +
261+	    + ## new-file (deleted) ##
262+	4:  $(test_oid t4) = 4:  $(test_oid s4) s/12/B/
263+*/
264+// func TestRangeDiffFileAddedThenRemoved(t *testing.T) {}
265+
266+// changed message
267+/*
268+	1:  $(test_oid t1) = 1:  $(test_oid m1) s/5/A/
269+	2:  $(test_oid t2) ! 2:  $(test_oid m2) s/4/A/
270+	    @@ Metadata
271+	    Z ## Commit message ##
272+	    Z    s/4/A/
273+	    Z
274+	    +    Also a silly comment here!
275+	    +
276+	    Z ## file ##
277+	    Z@@
278+	    Z 1
279+	3:  $(test_oid t3) = 3:  $(test_oid m3) s/11/B/
280+	4:  $(test_oid t4) = 4:  $(test_oid m4) s/12/B/
281+*/
282+// func TestRangeDiffChangedMessage(t *testing.T) {}
util.go link
+3 -3
 1diff --git a/util.go b/util.go
 2index 061bb6d..3c10da5 100644
 3--- a/util.go
 4+++ b/util.go
 5@@ -109,7 +109,7 @@ func patchToDiff(patch io.Reader) (string, error) {
 6 	return str[idx:], nil
 7 }
 8 
 9-func parsePatchset(patchset io.Reader) ([]*Patch, error) {
10+func ParsePatchset(patchset io.Reader) ([]*Patch, error) {
11 	patches := []*Patch{}
12 	buf := new(strings.Builder)
13 	_, err := io.Copy(buf, patchset)
14@@ -154,6 +154,7 @@ func parsePatchset(patchset io.Reader) ([]*Patch, error) {
15 			ContentSha:    contentSha,
16 			RawText:       patchStr,
17 			BaseCommitSha: sql.NullString{String: baseCommit},
18+			Files:         diffFiles,
19 		})
20 	}
21 
22@@ -172,12 +173,11 @@ func calcContentSha(diffFiles []*gitdiff.File, header *gitdiff.PatchHeader) stri
23 		authorEmail = header.Author.Email
24 	}
25 	content := fmt.Sprintf(
26-		"%s\n%s\n%s\n%s\n%s\n",
27+		"%s\n%s\n%s\n%s\n",
28 		header.Title,
29 		header.Body,
30 		authorName,
31 		authorEmail,
32-		header.AuthorDate,
33 	)
34 	for _, diff := range diffFiles {
35 		// we need to ignore diffs with base commit because that depends
util_test.go link
+1 -1
 1diff --git a/util_test.go b/util_test.go
 2index eaba33d..e579254 100644
 3--- a/util_test.go
 4+++ b/util_test.go
 5@@ -15,7 +15,7 @@ func TestParsePatchsetWithCover(t *testing.T) {
 6 	if err != nil {
 7 		t.Fatalf(err.Error())
 8 	}
 9-	actual, err := parsePatchset(file)
10+	actual, err := ParsePatchset(file)
11 	if err != nil {
12 		t.Fatalf(err.Error())
13 	}