git-pr / feat: range diff #19

open · opened on 2024-08-19T21:12:53Z 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

Patchsets

Diff ↕

feat: range diff

Eric Bower <me@erock.io>
 fixtures/a_b.patch                   |  31 +++
 fixtures/a_b_reorder.patch           |  55 ++++++
 fixtures/a_c.patch                   |  33 ++++
 fixtures/a_c_added_commit.patch      |  80 ++++++++
 fixtures/a_c_changed_commit.patch    |  60 ++++++
 fixtures/a_c_reorder.patch           |  55 ++++++
 fixtures/a_c_rm_commit.patch         |  23 +++
 fixtures/expected_commit_changed.txt |  11 ++
 go.mod                               |   1 +
 go.sum                               |  15 +-
 pr.go                                |   4 +-
 range_diff.go                        | 188 ++++++++++++++++++
 range_diff_test.go                   | 274 +++++++++++++++++++++++++++
 util.go                              |   5 +-
 util_test.go                         |   2 +-
 15 files changed, 829 insertions(+), 8 deletions(-)
 create mode 100644 fixtures/a_b.patch
 create mode 100644 fixtures/a_b_reorder.patch
 create mode 100644 fixtures/a_c.patch
 create mode 100644 fixtures/a_c_added_commit.patch
 create mode 100644 fixtures/a_c_changed_commit.patch
 create mode 100644 fixtures/a_c_reorder.patch
 create mode 100644 fixtures/a_c_rm_commit.patch
 create mode 100644 fixtures/expected_commit_changed.txt
 create mode 100644 range_diff.go
 create mode 100644 range_diff_test.go
   1From 778bcd8223bbe29732a5d8c0379bc9717d93f087 Mon Sep 17 00:00:00 2001
   2From: Eric Bower <me@erock.io>
   3Date: Tue, 23 Jul 2024 11:59:25 -0400
   4Subject: [PATCH] feat: range diff
   5
   6---
   7 fixtures/a_b.patch                   |  31 +++
   8 fixtures/a_b_reorder.patch           |  55 ++++++
   9 fixtures/a_c.patch                   |  33 ++++
  10 fixtures/a_c_added_commit.patch      |  80 ++++++++
  11 fixtures/a_c_changed_commit.patch    |  60 ++++++
  12 fixtures/a_c_reorder.patch           |  55 ++++++
  13 fixtures/a_c_rm_commit.patch         |  23 +++
  14 fixtures/expected_commit_changed.txt |  11 ++
  15 go.mod                               |   1 +
  16 go.sum                               |  15 +-
  17 pr.go                                |   4 +-
  18 range_diff.go                        | 188 ++++++++++++++++++
  19 range_diff_test.go                   | 274 +++++++++++++++++++++++++++
  20 util.go                              |   5 +-
  21 util_test.go                         |   2 +-
  22 15 files changed, 829 insertions(+), 8 deletions(-)
  23 create mode 100644 fixtures/a_b.patch
  24 create mode 100644 fixtures/a_b_reorder.patch
  25 create mode 100644 fixtures/a_c.patch
  26 create mode 100644 fixtures/a_c_added_commit.patch
  27 create mode 100644 fixtures/a_c_changed_commit.patch
  28 create mode 100644 fixtures/a_c_reorder.patch
  29 create mode 100644 fixtures/a_c_rm_commit.patch
  30 create mode 100644 fixtures/expected_commit_changed.txt
  31 create mode 100644 range_diff.go
  32 create mode 100644 range_diff_test.go
  33
  34diff --git a/fixtures/a_b.patch b/fixtures/a_b.patch
  35new file mode 100644
  36index 0000000..c2c5167
  37--- /dev/null
  38+++ b/fixtures/a_b.patch
  39@@ -0,0 +1,31 @@
  40+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
  41+From: Eric Bower <me@erock.io>
  42+Date: Tue, 23 Jul 2024 10:07:57 -0400
  43+Subject: [PATCH] chore: add torch and create random tensor
  44+
  45+---
  46+ requirements.txt | 1 +
  47+ train.py         | 3 +++
  48+ 2 files changed, 4 insertions(+)
  49+ create mode 100644 requirements.txt
  50+
  51+diff --git a/requirements.txt b/requirements.txt
  52+new file mode 100644
  53+index 0000000..4968a39
  54+--- /dev/null
  55++++ b/requirements.txt
  56+@@ -0,0 +1 @@
  57++torch==2.3.1
  58+diff --git a/train.py b/train.py
  59+index 5c027f4..d21dac3 100644
  60+--- a/train.py
  61++++ b/train.py
  62+@@ -1,2 +1,5 @@
  63++import torch
  64++
  65+ if __name__ == "__main__":
  66+     print("train!")
  67++    torch.rand(3,6)
  68+-- 
  69+2.45.2
  70+
  71diff --git a/fixtures/a_b_reorder.patch b/fixtures/a_b_reorder.patch
  72new file mode 100644
  73index 0000000..2524df9
  74--- /dev/null
  75+++ b/fixtures/a_b_reorder.patch
  76@@ -0,0 +1,55 @@
  77+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
  78+From: Eric Bower <me@erock.io>
  79+Date: Tue, 23 Jul 2024 10:07:57 -0400
  80+Subject: [PATCH 1/2] chore: add torch and create random tensor
  81+
  82+---
  83+ requirements.txt | 1 +
  84+ train.py         | 3 +++
  85+ 2 files changed, 4 insertions(+)
  86+ create mode 100644 requirements.txt
  87+
  88+diff --git a/requirements.txt b/requirements.txt
  89+new file mode 100644
  90+index 0000000..4968a39
  91+--- /dev/null
  92++++ b/requirements.txt
  93+@@ -0,0 +1 @@
  94++torch==2.3.1
  95+diff --git a/train.py b/train.py
  96+index 5c027f4..d21dac3 100644
  97+--- a/train.py
  98++++ b/train.py
  99+@@ -1,2 +1,5 @@
 100++import torch
 101++
 102+ if __name__ == "__main__":
 103+     print("train!")
 104++    torch.rand(3,6)
 105+-- 
 106+2.45.2
 107+
 108+
 109+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
 110+From: Eric Bower <me@erock.io>
 111+Date: Tue, 23 Jul 2024 10:14:37 -0400
 112+Subject: [PATCH 2/2] docs: readme
 113+
 114+---
 115+ README.md | 4 +++-
 116+ 1 file changed, 3 insertions(+), 1 deletion(-)
 117+
 118+diff --git a/README.md b/README.md
 119+index 8f3a780..3043953 100644
 120+--- a/README.md
 121++++ b/README.md
 122+@@ -1,3 +1,5 @@
 123+ # Let's build an RNN
 124+ 
 125+-This repo demonstrates building an RNN using `pytorch`
 126++This repo demonstrates building an RNN using `pytorch`.
 127++
 128++Here is some more readme information.
 129+-- 
 130+2.45.2
 131+
 132diff --git a/fixtures/a_c.patch b/fixtures/a_c.patch
 133new file mode 100644
 134index 0000000..4960260
 135--- /dev/null
 136+++ b/fixtures/a_c.patch
 137@@ -0,0 +1,33 @@
 138+From 166848469e0b954c2e14233233f3824a46dcddb8 Mon Sep 17 00:00:00 2001
 139+From: Eric Bower <me@erock.io>
 140+Date: Tue, 23 Jul 2024 10:06:00 -0400
 141+Subject: [PATCH] chore: add torch and create random tensor
 142+
 143+---
 144+ requirements.txt | 1 +
 145+ train.py         | 3 +++
 146+ 2 files changed, 4 insertions(+)
 147+ create mode 100644 requirements.txt
 148+
 149+diff --git a/requirements.txt b/requirements.txt
 150+new file mode 100644
 151+index 0000000..4968a39
 152+--- /dev/null
 153++++ b/requirements.txt
 154+@@ -0,0 +1 @@
 155++torch==2.3.1
 156+diff --git a/train.py b/train.py
 157+index 5c027f4..d21dac3 100644
 158+--- a/train.py
 159++++ b/train.py
 160+@@ -1,2 +1,5 @@
 161++import torch
 162++
 163+ if __name__ == "__main__":
 164+     print("train!")
 165++    torch.rand(3,6)
 166+
 167+base-commit: 59456574a0bfee9f71c91c13046173c820152346
 168+-- 
 169+2.45.2
 170+
 171diff --git a/fixtures/a_c_added_commit.patch b/fixtures/a_c_added_commit.patch
 172new file mode 100644
 173index 0000000..f5ce9f3
 174--- /dev/null
 175+++ b/fixtures/a_c_added_commit.patch
 176@@ -0,0 +1,80 @@
 177+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 178+From: Eric Bower <me@erock.io>
 179+Date: Tue, 23 Jul 2024 10:07:57 -0400
 180+Subject: [PATCH 1/3] chore: add torch and create random tensor
 181+
 182+---
 183+ requirements.txt | 1 +
 184+ train.py         | 3 +++
 185+ 2 files changed, 4 insertions(+)
 186+ create mode 100644 requirements.txt
 187+
 188+diff --git a/requirements.txt b/requirements.txt
 189+new file mode 100644
 190+index 0000000..4968a39
 191+--- /dev/null
 192++++ b/requirements.txt
 193+@@ -0,0 +1 @@
 194++torch==2.3.1
 195+diff --git a/train.py b/train.py
 196+index 5c027f4..d21dac3 100644
 197+--- a/train.py
 198++++ b/train.py
 199+@@ -1,2 +1,5 @@
 200++import torch
 201++
 202+ if __name__ == "__main__":
 203+     print("train!")
 204++    torch.rand(3,6)
 205+-- 
 206+2.45.2
 207+
 208+
 209+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
 210+From: Eric Bower <me@erock.io>
 211+Date: Tue, 23 Jul 2024 10:14:37 -0400
 212+Subject: [PATCH 2/3] docs: readme
 213+
 214+---
 215+ README.md | 4 +++-
 216+ 1 file changed, 3 insertions(+), 1 deletion(-)
 217+
 218+diff --git a/README.md b/README.md
 219+index 8f3a780..3043953 100644
 220+--- a/README.md
 221++++ b/README.md
 222+@@ -1,3 +1,5 @@
 223+ # Let's build an RNN
 224+ 
 225+-This repo demonstrates building an RNN using `pytorch`
 226++This repo demonstrates building an RNN using `pytorch`.
 227++
 228++Here is some more readme information.
 229+-- 
 230+2.45.2
 231+
 232+
 233+From b248060488df529b850060b3c86417bb87d490cc Mon Sep 17 00:00:00 2001
 234+From: Eric Bower <me@erock.io>
 235+Date: Tue, 23 Jul 2024 10:20:44 -0400
 236+Subject: [PATCH 3/3] chore: make tensor 6x6
 237+
 238+---
 239+ train.py | 4 +++-
 240+ 1 file changed, 3 insertions(+), 1 deletion(-)
 241+
 242+diff --git a/train.py b/train.py
 243+index d21dac3..8cd47e0 100644
 244+--- a/train.py
 245++++ b/train.py
 246+@@ -2,4 +2,6 @@ import torch
 247+ 
 248+ if __name__ == "__main__":
 249+     print("train!")
 250+-    torch.rand(3,6)
 251++    # let's create a 6x6 tensor!
 252++    tensor = torch.rand(6,6)
 253++    print(tensor)
 254+-- 
 255+2.45.2
 256+
 257diff --git a/fixtures/a_c_changed_commit.patch b/fixtures/a_c_changed_commit.patch
 258new file mode 100644
 259index 0000000..259a434
 260--- /dev/null
 261+++ b/fixtures/a_c_changed_commit.patch
 262@@ -0,0 +1,60 @@
 263+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 264+From: Eric Bower <me@erock.io>
 265+Date: Tue, 23 Jul 2024 10:07:57 -0400
 266+Subject: [PATCH 1/2] chore: add torch and create random tensor
 267+
 268+---
 269+ requirements.txt | 1 +
 270+ train.py         | 3 +++
 271+ 2 files changed, 4 insertions(+)
 272+ create mode 100644 requirements.txt
 273+
 274+diff --git a/requirements.txt b/requirements.txt
 275+new file mode 100644
 276+index 0000000..4968a39
 277+--- /dev/null
 278++++ b/requirements.txt
 279+@@ -0,0 +1 @@
 280++torch==2.3.1
 281+diff --git a/train.py b/train.py
 282+index 5c027f4..d21dac3 100644
 283+--- a/train.py
 284++++ b/train.py
 285+@@ -1,2 +1,5 @@
 286++import torch
 287++
 288+ if __name__ == "__main__":
 289+     print("train!")
 290++    torch.rand(3,6)
 291+-- 
 292+2.45.2
 293+
 294+
 295+From dce20e70280d92aeb88c3d603ad67043ead772fb Mon Sep 17 00:00:00 2001
 296+From: Eric Bower <me@erock.io>
 297+Date: Tue, 23 Jul 2024 10:14:37 -0400
 298+Subject: [PATCH 2/2] docs: readme
 299+
 300+---
 301+ README.md | 9 ++++++++-
 302+ 1 file changed, 8 insertions(+), 1 deletion(-)
 303+
 304+diff --git a/README.md b/README.md
 305+index 8f3a780..ba0293b 100644
 306+--- a/README.md
 307++++ b/README.md
 308+@@ -1,3 +1,10 @@
 309+ # Let's build an RNN
 310+ 
 311+-This repo demonstrates building an RNN using `pytorch`
 312++This repo demonstrates building an RNN using `pytorch`.
 313++
 314++Here is some more readme information.
 315++
 316++Here is how to run this project locally:
 317++
 318++- install python and pip
 319++- `pip install -r requirements.txt`
 320+-- 
 321+2.45.2
 322+
 323diff --git a/fixtures/a_c_reorder.patch b/fixtures/a_c_reorder.patch
 324new file mode 100644
 325index 0000000..bc90f03
 326--- /dev/null
 327+++ b/fixtures/a_c_reorder.patch
 328@@ -0,0 +1,55 @@
 329+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
 330+From: Eric Bower <me@erock.io>
 331+Date: Tue, 23 Jul 2024 10:15:23 -0400
 332+Subject: [PATCH 1/2] docs: readme
 333+
 334+---
 335+ README.md | 4 +++-
 336+ 1 file changed, 3 insertions(+), 1 deletion(-)
 337+
 338+diff --git a/README.md b/README.md
 339+index 8f3a780..3043953 100644
 340+--- a/README.md
 341++++ b/README.md
 342+@@ -1,3 +1,5 @@
 343+ # Let's build an RNN
 344+ 
 345+-This repo demonstrates building an RNN using `pytorch`
 346++This repo demonstrates building an RNN using `pytorch`.
 347++
 348++Here is some more readme information.
 349+-- 
 350+2.45.2
 351+
 352+
 353+From ad175875e2bf320859554bae73743675cc5ce444 Mon Sep 17 00:00:00 2001
 354+From: Eric Bower <me@erock.io>
 355+Date: Tue, 23 Jul 2024 10:06:00 -0400
 356+Subject: [PATCH 2/2] chore: add torch and create random tensor
 357+
 358+---
 359+ requirements.txt | 1 +
 360+ train.py         | 3 +++
 361+ 2 files changed, 4 insertions(+)
 362+ create mode 100644 requirements.txt
 363+
 364+diff --git a/requirements.txt b/requirements.txt
 365+new file mode 100644
 366+index 0000000..4968a39
 367+--- /dev/null
 368++++ b/requirements.txt
 369+@@ -0,0 +1 @@
 370++torch==2.3.1
 371+diff --git a/train.py b/train.py
 372+index 5c027f4..d21dac3 100644
 373+--- a/train.py
 374++++ b/train.py
 375+@@ -1,2 +1,5 @@
 376++import torch
 377++
 378+ if __name__ == "__main__":
 379+     print("train!")
 380++    torch.rand(3,6)
 381+-- 
 382+2.45.2
 383+
 384diff --git a/fixtures/a_c_rm_commit.patch b/fixtures/a_c_rm_commit.patch
 385new file mode 100644
 386index 0000000..c6a4c5e
 387--- /dev/null
 388+++ b/fixtures/a_c_rm_commit.patch
 389@@ -0,0 +1,23 @@
 390+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
 391+From: Eric Bower <me@erock.io>
 392+Date: Tue, 23 Jul 2024 10:15:23 -0400
 393+Subject: [PATCH] docs: readme
 394+
 395+---
 396+ README.md | 4 +++-
 397+ 1 file changed, 3 insertions(+), 1 deletion(-)
 398+
 399+diff --git a/README.md b/README.md
 400+index 8f3a780..3043953 100644
 401+--- a/README.md
 402++++ b/README.md
 403+@@ -1,3 +1,5 @@
 404+ # Let's build an RNN
 405+ 
 406+-This repo demonstrates building an RNN using `pytorch`
 407++This repo demonstrates building an RNN using `pytorch`.
 408++
 409++Here is some more readme information.
 410+-- 
 411+2.45.2
 412+
 413diff --git a/fixtures/expected_commit_changed.txt b/fixtures/expected_commit_changed.txt
 414new file mode 100644
 415index 0000000..ac4499c
 416--- /dev/null
 417+++ b/fixtures/expected_commit_changed.txt
 418@@ -0,0 +1,11 @@
 419+1:  33c682a = 1:  33c682a chore: add torch and create random tensor
 420+2:  22dde12 ! 2:  0185f34 docs: readme
 421+    @@ README.md
 422+     +This repo demonstrates building an RNN using `pytorch`.
 423+     +
 424+     +Here is some more readme information.
 425+    ++
 426+    ++Here is how to run this project locally:
 427+    ++
 428+    ++- install python and pip
 429+    ++- `pip install -r requirements.txt`
 430diff --git a/go.mod b/go.mod
 431index 4dc95e6..53504d3 100644
 432--- a/go.mod
 433+++ b/go.mod
 434@@ -14,6 +14,7 @@ require (
 435 	github.com/knadh/koanf/providers/env v0.1.0
 436 	github.com/knadh/koanf/providers/file v1.0.0
 437 	github.com/knadh/koanf/v2 v2.1.1
 438+	github.com/sergi/go-diff v1.1.0
 439 	github.com/urfave/cli/v2 v2.27.2
 440 	golang.org/x/crypto v0.21.0
 441 	modernc.org/sqlite v1.27.0
 442diff --git a/go.sum b/go.sum
 443index 1ec46c6..5bc1160 100644
 444--- a/go.sum
 445+++ b/go.sum
 446@@ -8,8 +8,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
 447 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 448 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 449 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 450-github.com/bluekeyes/go-gitdiff v0.7.2 h1:42jrcVZdjjxXtVsFNYTo/I6T1ZvIiQL+iDDLiH904hw=
 451-github.com/bluekeyes/go-gitdiff v0.7.2/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
 452 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190 h1:k6Ep4yQtmsoP/St4bf7ofXyWc6ITB/FyGy9ewaAn5os=
 453 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
 454 github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
 455@@ -36,6 +34,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV
 456 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 457 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
 458 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 459+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 460 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 461 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 462 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 463@@ -74,8 +73,11 @@ github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcnc
 464 github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
 465 github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
 466 github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
 467+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 468 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 469 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 470+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 471+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 472 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 473 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 474 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 475@@ -107,6 +109,7 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo
 476 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 477 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 478 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 479+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 480 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 481 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 482 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 483@@ -119,6 +122,10 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
 484 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 485 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 486 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 487+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 488+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 489+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 490+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 491 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 492 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 493 github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
 494@@ -143,6 +150,10 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 495 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 496 golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
 497 golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
 498+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 499+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 500+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 501+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 502 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 503 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 504 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 505diff --git a/pr.go b/pr.go
 506index 9e535a0..984fe92 100644
 507--- a/pr.go
 508+++ b/pr.go
 509@@ -447,7 +447,7 @@ func (cmd PrCmd) SubmitPatchRequest(repoID string, userID int64, patchset io.Rea
 510 		_ = tx.Rollback()
 511 	}()
 512 
 513-	patches, err := parsePatchset(patchset)
 514+	patches, err := ParsePatchset(patchset)
 515 	if err != nil {
 516 		return nil, err
 517 	}
 518@@ -541,7 +541,7 @@ func (cmd PrCmd) SubmitPatchset(prID int64, userID int64, op PatchsetOp, patchse
 519 		_ = tx.Rollback()
 520 	}()
 521 
 522-	patches, err := parsePatchset(patchset)
 523+	patches, err := ParsePatchset(patchset)
 524 	if err != nil {
 525 		return fin, err
 526 	}
 527diff --git a/range_diff.go b/range_diff.go
 528new file mode 100644
 529index 0000000..28cfaef
 530--- /dev/null
 531+++ b/range_diff.go
 532@@ -0,0 +1,188 @@
 533+package git
 534+
 535+import (
 536+	"fmt"
 537+	"math"
 538+
 539+	"github.com/sergi/go-diff/diffmatchpatch"
 540+)
 541+
 542+var COST_MAX = 65536
 543+var RANGE_DIFF_CREATION_FACTOR_DEFAULT = 60
 544+
 545+type PatchRange struct {
 546+	*Patch
 547+	Matching int
 548+}
 549+
 550+func NewPatchRange(patch *Patch) *PatchRange {
 551+	return &PatchRange{
 552+		Patch: patch,
 553+	}
 554+}
 555+
 556+func output(a []*PatchRange, b []*PatchRange) string {
 557+	out := ""
 558+	for _, patchB := range b {
 559+		patchA := a[patchB.Matching]
 560+		if patchB.ContentSha == patchA.ContentSha {
 561+			out += outputPairHeader(patchA, patchB, patchB.Matching+1, patchA.Matching+1)
 562+		}
 563+	}
 564+	return out
 565+}
 566+
 567+func outputPairHeader(a *PatchRange, b *PatchRange, aIndex, bIndex int) string {
 568+	return fmt.Sprintf("%d:  %s = %d:  %s %s\n", aIndex, truncateSha(a.CommitSha), bIndex, truncateSha(b.CommitSha), a.Title)
 569+}
 570+
 571+func RangeDiff(a []*Patch, b []*Patch) string {
 572+	aPatches := []*PatchRange{}
 573+	for _, patch := range a {
 574+		aPatches = append(aPatches, NewPatchRange(patch))
 575+	}
 576+	bPatches := []*PatchRange{}
 577+	for _, patch := range b {
 578+		bPatches = append(bPatches, NewPatchRange(patch))
 579+	}
 580+	findExactMatches(aPatches, bPatches)
 581+	getCorrespondences(aPatches, bPatches, RANGE_DIFF_CREATION_FACTOR_DEFAULT)
 582+	return output(aPatches, bPatches)
 583+}
 584+
 585+func findExactMatches(a []*PatchRange, b []*PatchRange) {
 586+	for i, patchA := range a {
 587+		for j, patchB := range b {
 588+			if patchA.ContentSha == patchB.ContentSha {
 589+				patchA.Matching = j
 590+				patchB.Matching = i
 591+			}
 592+		}
 593+	}
 594+}
 595+
 596+func createMatrix(rows, cols int) [][]int {
 597+	mat := make([][]int, rows)
 598+	for i := range mat {
 599+		mat[i] = make([]int, cols)
 600+	}
 601+	return mat
 602+}
 603+
 604+func diffsize(a *PatchRange, b *PatchRange) int {
 605+	dmp := diffmatchpatch.New()
 606+	diffs := dmp.DiffMain(a.RawText, b.RawText, false)
 607+	return len(dmp.DiffPrettyText(diffs))
 608+}
 609+
 610+func getCorrespondences(a []*PatchRange, b []*PatchRange, creationFactor int) {
 611+	// n := len(a) + len(b)
 612+	fmt.Println(len(a), len(b))
 613+	cost := createMatrix(len(a), len(b))
 614+
 615+	for i, patchA := range a {
 616+		var c int
 617+		for j, patchB := range b {
 618+			if patchA.Matching == j {
 619+				c = 0
 620+			} else if patchA.Matching == 0 && patchB.Matching == 0 {
 621+				c = diffsize(patchA, patchB)
 622+			} else {
 623+				c = COST_MAX
 624+			}
 625+			cost[i][j] = c
 626+		}
 627+	}
 628+
 629+	assignment := computeAssignment(cost, len(a), len(b))
 630+	for i, j := range assignment {
 631+		if j < len(b) {
 632+			a[i].Matching = j
 633+			b[j].Matching = i
 634+		}
 635+	}
 636+
 637+	fmt.Println(cost, assignment)
 638+	fmt.Println("A==")
 639+	for _, patch := range a {
 640+		fmt.Println("matches", b[patch.Matching].Title)
 641+	}
 642+
 643+	fmt.Println("B==")
 644+	for _, patch := range b {
 645+		fmt.Println("matches", a[patch.Matching].Title)
 646+	}
 647+}
 648+
 649+// computeAssignment assigns patches using the Hungarian algorithm.
 650+func computeAssignment(costMatrix [][]int, m, n int) []int {
 651+	u := make([]int, m+1) // potential for workers
 652+	v := make([]int, n+1) // potential for jobs
 653+	p := make([]int, n+1) // job assignment
 654+	way := make([]int, n+1)
 655+
 656+	for i := 1; i <= m; i++ {
 657+		links := make([]int, n+1)
 658+		minV := make([]int, n+1)
 659+		used := make([]bool, n+1)
 660+		for j := 0; j <= n; j++ {
 661+			minV[j] = math.MaxInt32
 662+			used[j] = false
 663+		}
 664+
 665+		j0 := 0
 666+		p[0] = i
 667+
 668+		for {
 669+			used[j0] = true
 670+			i0 := p[j0]
 671+			delta := math.MaxInt32
 672+			j1 := 0
 673+
 674+			for j := 1; j <= n; j++ {
 675+				if !used[j] {
 676+					cur := costMatrix[i0-1][j-1] - u[i0] - v[j]
 677+					if cur < minV[j] {
 678+						minV[j] = cur
 679+						links[j] = j0
 680+					}
 681+					if minV[j] < delta {
 682+						delta = minV[j]
 683+						j1 = j
 684+					}
 685+				}
 686+			}
 687+
 688+			for j := 0; j <= n; j++ {
 689+				if used[j] {
 690+					u[p[j]] += delta
 691+					v[j] -= delta
 692+				} else {
 693+					minV[j] -= delta
 694+				}
 695+			}
 696+
 697+			j0 = j1
 698+			if p[j0] == 0 {
 699+				break
 700+			}
 701+		}
 702+
 703+		for {
 704+			j1 := way[j0]
 705+			p[j0] = p[j1]
 706+			j0 = j1
 707+			if j0 == 0 {
 708+				break
 709+			}
 710+		}
 711+	}
 712+
 713+	assignment := make([]int, m)
 714+	for j := 1; j <= n; j++ {
 715+		if p[j] > 0 {
 716+			assignment[p[j]-1] = j - 1
 717+		}
 718+	}
 719+	return assignment
 720+}
 721diff --git a/range_diff_test.go b/range_diff_test.go
 722new file mode 100644
 723index 0000000..8705bff
 724--- /dev/null
 725+++ b/range_diff_test.go
 726@@ -0,0 +1,274 @@
 727+package git
 728+
 729+import (
 730+	"fmt"
 731+	"testing"
 732+
 733+	"github.com/picosh/git-pr/fixtures"
 734+)
 735+
 736+func bail(err error) {
 737+	if err != nil {
 738+		panic(bail)
 739+	}
 740+}
 741+
 742+func cmp(afile, bfile string) string {
 743+	a, err := fixtures.Fixtures.Open(afile)
 744+	bail(err)
 745+	b, err := fixtures.Fixtures.Open(bfile)
 746+	bail(err)
 747+	aPatches, err := ParsePatchset(a)
 748+	bail(err)
 749+	bPatches, err := ParsePatchset(b)
 750+	bail(err)
 751+	actual := RangeDiff(aPatches, bPatches)
 752+	return actual
 753+}
 754+
 755+func fail(expected, actual string) string {
 756+	return fmt.Sprintf("expected:[%s] actual:[%s]", expected, actual)
 757+}
 758+
 759+// https://git.kernel.org/tree/t/t3206-range-diff.sh?id=d19b6cd2dd72dc811f19df4b32c7ed223256c3ee
 760+
 761+// simple A..B A..C (unmodified)
 762+/*
 763+	1:  $(test_oid t1) = 1:  $(test_oid u1) s/5/A/
 764+	2:  $(test_oid t2) = 2:  $(test_oid u2) s/4/A/
 765+	3:  $(test_oid t3) = 3:  $(test_oid u3) s/11/B/
 766+	4:  $(test_oid t4) = 4:  $(test_oid u4) s/12/B/
 767+*/
 768+func TestRangeDiffUnmodified(t *testing.T) {
 769+	actual := cmp("a_b.patch", "a_c.patch")
 770+	expected := "1:  33c682a = 1:  1668484 chore: add torch and create random tensor\n"
 771+	if expected != actual {
 772+		t.Fatalf(fail(expected, actual))
 773+	}
 774+}
 775+
 776+// trivial reordering
 777+/*
 778+	1:  $(test_oid t1) = 1:  $(test_oid r1) s/5/A/
 779+	3:  $(test_oid t3) = 2:  $(test_oid r2) s/11/B/
 780+	4:  $(test_oid t4) = 3:  $(test_oid r3) s/12/B/
 781+	2:  $(test_oid t2) = 4:  $(test_oid r4) s/4/A/
 782+*/
 783+func TestRangeDiffTrivialReordering(t *testing.T) {
 784+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
 785+	expected := `2:  22dde12 = 1:  7dbb94c docs: readme
 786+1:  33c682a = 2:  ad17587 chore: add torch and create random tensor
 787+`
 788+	if expected != actual {
 789+		t.Fatalf(fail(expected, actual))
 790+	}
 791+}
 792+
 793+// removed commit
 794+/*
 795+	1:  $(test_oid t1) = 1:  $(test_oid d1) s/5/A/
 796+	2:  $(test_oid t2) < -:  $(test_oid __) s/4/A/
 797+	3:  $(test_oid t3) = 2:  $(test_oid d2) s/11/B/
 798+	4:  $(test_oid t4) = 3:  $(test_oid d3) s/12/B/
 799+*/
 800+func TestRangeDiffRemovedCommit(t *testing.T) {
 801+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
 802+	expected := `1:  33c682a < -:  ------- chore: add torch and create random tensor
 803+2:  22dde12 = 1:  7dbb94c docs: readme`
 804+	if expected != actual {
 805+		t.Fatalf(fail(expected, actual))
 806+	}
 807+}
 808+
 809+// added commit
 810+/*
 811+	1:  $(test_oid t1) = 1:  $(test_oid a1) s/5/A/
 812+	2:  $(test_oid t2) = 2:  $(test_oid a2) s/4/A/
 813+	-:  $(test_oid __) > 3:  $(test_oid a3) s/6/A/
 814+	3:  $(test_oid t3) = 4:  $(test_oid a4) s/11/B/
 815+	4:  $(test_oid t4) = 5:  $(test_oid a5) s/12/B/
 816+*/
 817+/* func TestRangeDiffAddedCommit(t *testing.T) {
 818+	actual := ""
 819+	expected := `1:  33c682a = 1:  33c682a chore: add torch and create random tensor
 820+2:  22dde12 = 2:  22dde12 docs: readme
 821+-:  ------- > 3:  b248060 chore: make tensor 6x6`
 822+	if expected != actual {
 823+		t.Fatalf("expected:%s actual:%s", expected, actual)
 824+	}
 825+} */
 826+
 827+// changed commit
 828+/*
 829+	1:  $(test_oid t1) = 1:  $(test_oid c1) s/5/A/
 830+	2:  $(test_oid t2) = 2:  $(test_oid c2) s/4/A/
 831+	3:  $(test_oid t3) ! 3:  $(test_oid c3) s/11/B/
 832+	    @@ file: A
 833+	      9
 834+	      10
 835+	     -11
 836+	    -+B
 837+	    ++BB
 838+	      12
 839+	      13
 840+	      14
 841+	4:  $(test_oid t4) ! 4:  $(test_oid c4) s/12/B/
 842+	    @@ file
 843+	     @@ file: A
 844+	      9
 845+	      10
 846+	    - B
 847+	    + BB
 848+	     -12
 849+	     +B
 850+	      13
 851+*/
 852+/* func TestRangeDiffChangedCommit(t *testing.T) {
 853+	actual := ""
 854+	fp, err := fixtures.Fixtures.ReadFile("extected_commit_changed.txt")
 855+	if err != nil {
 856+		t.Fatalf("file not found")
 857+	}
 858+	expected := string(fp)
 859+	if expected != actual {
 860+		t.Fatalf("expected:%s actual:%s", expected, actual)
 861+	}
 862+} */
 863+
 864+// renamed file
 865+/*
 866+	1:  $(test_oid t1) = 1:  $(test_oid n1) s/5/A/
 867+	2:  $(test_oid t2) ! 2:  $(test_oid n2) s/4/A/
 868+	    @@ Metadata
 869+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 870+	    Z
 871+	    Z ## Commit message ##
 872+	    -    s/4/A/
 873+	    +    s/4/A/ + rename file
 874+	    Z
 875+	    - ## file ##
 876+	    + ## file => renamed-file ##
 877+	    Z@@
 878+	    Z 1
 879+	    Z 2
 880+	3:  $(test_oid t3) ! 3:  $(test_oid n3) s/11/B/
 881+	    @@ Metadata
 882+	    Z ## Commit message ##
 883+	    Z    s/11/B/
 884+	    Z
 885+	    - ## file ##
 886+	    -@@ file: A
 887+	    + ## renamed-file ##
 888+	    +@@ renamed-file: A
 889+	    Z 8
 890+	    Z 9
 891+	    Z 10
 892+	4:  $(test_oid t4) ! 4:  $(test_oid n4) s/12/B/
 893+	    @@ Metadata
 894+	    Z ## Commit message ##
 895+	    Z    s/12/B/
 896+	    Z
 897+	    - ## file ##
 898+	    -@@ file: A
 899+	    + ## renamed-file ##
 900+	    +@@ renamed-file: A
 901+	    Z 9
 902+	    Z 10
 903+	    Z B
 904+*/
 905+// func TestRangeDiffRenamedFile(t *testing.T) {}
 906+
 907+// file with mode only change
 908+/*
 909+	1:  $(test_oid t2) ! 1:  $(test_oid o1) s/4/A/
 910+	    @@ Metadata
 911+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 912+	    Z
 913+	    Z ## Commit message ##
 914+	    -    s/4/A/
 915+	    +    s/4/A/ + add other-file
 916+	    Z
 917+	    Z ## file ##
 918+	    Z@@
 919+	    @@ file
 920+	    Z A
 921+	    Z 6
 922+	    Z 7
 923+	    +
 924+	    + ## other-file (new) ##
 925+	2:  $(test_oid t3) ! 2:  $(test_oid o2) s/11/B/
 926+	    @@ Metadata
 927+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 928+	    Z
 929+	    Z ## Commit message ##
 930+	    -    s/11/B/
 931+	    +    s/11/B/ + mode change other-file
 932+	    Z
 933+	    Z ## file ##
 934+	    Z@@ file: A
 935+	    @@ file: A
 936+	    Z 12
 937+	    Z 13
 938+	    Z 14
 939+	    +
 940+	    + ## other-file (mode change 100644 => 100755) ##
 941+	3:  $(test_oid t4) = 3:  $(test_oid o3) s/12/B/
 942+*/
 943+// func TestRangeDiffFileWithModeOnlyChange(t *testing.T) {}
 944+
 945+// file added and later removed
 946+/*
 947+	1:  $(test_oid t1) = 1:  $(test_oid s1) s/5/A/
 948+	2:  $(test_oid t2) ! 2:  $(test_oid s2) s/4/A/
 949+	    @@ Metadata
 950+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 951+	    Z
 952+	    Z ## Commit message ##
 953+	    -    s/4/A/
 954+	    +    s/4/A/ + new-file
 955+	    Z
 956+	    Z ## file ##
 957+	    Z@@
 958+	    @@ file
 959+	    Z A
 960+	    Z 6
 961+	    Z 7
 962+	    +
 963+	    + ## new-file (new) ##
 964+	3:  $(test_oid t3) ! 3:  $(test_oid s3) s/11/B/
 965+	    @@ Metadata
 966+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 967+	    Z
 968+	    Z ## Commit message ##
 969+	    -    s/11/B/
 970+	    +    s/11/B/ + remove file
 971+	    Z
 972+	    Z ## file ##
 973+	    Z@@ file: A
 974+	    @@ file: A
 975+	    Z 12
 976+	    Z 13
 977+	    Z 14
 978+	    +
 979+	    + ## new-file (deleted) ##
 980+	4:  $(test_oid t4) = 4:  $(test_oid s4) s/12/B/
 981+*/
 982+// func TestRangeDiffFileAddedThenRemoved(t *testing.T) {}
 983+
 984+// changed message
 985+/*
 986+	1:  $(test_oid t1) = 1:  $(test_oid m1) s/5/A/
 987+	2:  $(test_oid t2) ! 2:  $(test_oid m2) s/4/A/
 988+	    @@ Metadata
 989+	    Z ## Commit message ##
 990+	    Z    s/4/A/
 991+	    Z
 992+	    +    Also a silly comment here!
 993+	    +
 994+	    Z ## file ##
 995+	    Z@@
 996+	    Z 1
 997+	3:  $(test_oid t3) = 3:  $(test_oid m3) s/11/B/
 998+	4:  $(test_oid t4) = 4:  $(test_oid m4) s/12/B/
 999+*/
1000+// func TestRangeDiffChangedMessage(t *testing.T) {}
1001diff --git a/util.go b/util.go
1002index 061bb6d..da69a60 100644
1003--- a/util.go
1004+++ b/util.go
1005@@ -109,7 +109,7 @@ func patchToDiff(patch io.Reader) (string, error) {
1006 	return str[idx:], nil
1007 }
1008 
1009-func parsePatchset(patchset io.Reader) ([]*Patch, error) {
1010+func ParsePatchset(patchset io.Reader) ([]*Patch, error) {
1011 	patches := []*Patch{}
1012 	buf := new(strings.Builder)
1013 	_, err := io.Copy(buf, patchset)
1014@@ -172,12 +172,11 @@ func calcContentSha(diffFiles []*gitdiff.File, header *gitdiff.PatchHeader) stri
1015 		authorEmail = header.Author.Email
1016 	}
1017 	content := fmt.Sprintf(
1018-		"%s\n%s\n%s\n%s\n%s\n",
1019+		"%s\n%s\n%s\n%s\n",
1020 		header.Title,
1021 		header.Body,
1022 		authorName,
1023 		authorEmail,
1024-		header.AuthorDate,
1025 	)
1026 	for _, diff := range diffFiles {
1027 		// we need to ignore diffs with base commit because that depends
1028diff --git a/util_test.go b/util_test.go
1029index eaba33d..e579254 100644
1030--- a/util_test.go
1031+++ b/util_test.go
1032@@ -15,7 +15,7 @@ func TestParsePatchsetWithCover(t *testing.T) {
1033 	if err != nil {
1034 		t.Fatalf(err.Error())
1035 	}
1036-	actual, err := parsePatchset(file)
1037+	actual, err := ParsePatchset(file)
1038 	if err != nil {
1039 		t.Fatalf(err.Error())
1040 	}
1041-- 
10422.45.2
1043
ps-35 by erock on 2024-08-19T21:12:53Z

feat: range diff

Eric Bower <me@erock.io> 2024-07-23T15:59:25Z
 fixtures/a_b.patch                   |  31 +++
 fixtures/a_b_reorder.patch           |  55 ++++++
 fixtures/a_c.patch                   |  33 ++++
 fixtures/a_c_added_commit.patch      |  80 ++++++++
 fixtures/a_c_changed_commit.patch    |  60 ++++++
 fixtures/a_c_reorder.patch           |  55 ++++++
 fixtures/a_c_rm_commit.patch         |  23 +++
 fixtures/expected_commit_changed.txt |  11 ++
 go.mod                               |   1 +
 go.sum                               |  15 +-
 pr.go                                |   4 +-
 range_diff.go                        | 188 ++++++++++++++++++
 range_diff_test.go                   | 274 +++++++++++++++++++++++++++
 util.go                              |   5 +-
 util_test.go                         |   2 +-
 15 files changed, 829 insertions(+), 8 deletions(-)
 create mode 100644 fixtures/a_b.patch
 create mode 100644 fixtures/a_b_reorder.patch
 create mode 100644 fixtures/a_c.patch
 create mode 100644 fixtures/a_c_added_commit.patch
 create mode 100644 fixtures/a_c_changed_commit.patch
 create mode 100644 fixtures/a_c_reorder.patch
 create mode 100644 fixtures/a_c_rm_commit.patch
 create mode 100644 fixtures/expected_commit_changed.txt
 create mode 100644 range_diff.go
 create mode 100644 range_diff_test.go
   1From 778bcd8223bbe29732a5d8c0379bc9717d93f087 Mon Sep 17 00:00:00 2001
   2From: Eric Bower <me@erock.io>
   3Date: Tue, 23 Jul 2024 11:59:25 -0400
   4Subject: [PATCH] feat: range diff
   5
   6---
   7 fixtures/a_b.patch                   |  31 +++
   8 fixtures/a_b_reorder.patch           |  55 ++++++
   9 fixtures/a_c.patch                   |  33 ++++
  10 fixtures/a_c_added_commit.patch      |  80 ++++++++
  11 fixtures/a_c_changed_commit.patch    |  60 ++++++
  12 fixtures/a_c_reorder.patch           |  55 ++++++
  13 fixtures/a_c_rm_commit.patch         |  23 +++
  14 fixtures/expected_commit_changed.txt |  11 ++
  15 go.mod                               |   1 +
  16 go.sum                               |  15 +-
  17 pr.go                                |   4 +-
  18 range_diff.go                        | 188 ++++++++++++++++++
  19 range_diff_test.go                   | 274 +++++++++++++++++++++++++++
  20 util.go                              |   5 +-
  21 util_test.go                         |   2 +-
  22 15 files changed, 829 insertions(+), 8 deletions(-)
  23 create mode 100644 fixtures/a_b.patch
  24 create mode 100644 fixtures/a_b_reorder.patch
  25 create mode 100644 fixtures/a_c.patch
  26 create mode 100644 fixtures/a_c_added_commit.patch
  27 create mode 100644 fixtures/a_c_changed_commit.patch
  28 create mode 100644 fixtures/a_c_reorder.patch
  29 create mode 100644 fixtures/a_c_rm_commit.patch
  30 create mode 100644 fixtures/expected_commit_changed.txt
  31 create mode 100644 range_diff.go
  32 create mode 100644 range_diff_test.go
  33
  34diff --git a/fixtures/a_b.patch b/fixtures/a_b.patch
  35new file mode 100644
  36index 0000000..c2c5167
  37--- /dev/null
  38+++ b/fixtures/a_b.patch
  39@@ -0,0 +1,31 @@
  40+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
  41+From: Eric Bower <me@erock.io>
  42+Date: Tue, 23 Jul 2024 10:07:57 -0400
  43+Subject: [PATCH] chore: add torch and create random tensor
  44+
  45+---
  46+ requirements.txt | 1 +
  47+ train.py         | 3 +++
  48+ 2 files changed, 4 insertions(+)
  49+ create mode 100644 requirements.txt
  50+
  51+diff --git a/requirements.txt b/requirements.txt
  52+new file mode 100644
  53+index 0000000..4968a39
  54+--- /dev/null
  55++++ b/requirements.txt
  56+@@ -0,0 +1 @@
  57++torch==2.3.1
  58+diff --git a/train.py b/train.py
  59+index 5c027f4..d21dac3 100644
  60+--- a/train.py
  61++++ b/train.py
  62+@@ -1,2 +1,5 @@
  63++import torch
  64++
  65+ if __name__ == "__main__":
  66+     print("train!")
  67++    torch.rand(3,6)
  68+-- 
  69+2.45.2
  70+
  71diff --git a/fixtures/a_b_reorder.patch b/fixtures/a_b_reorder.patch
  72new file mode 100644
  73index 0000000..2524df9
  74--- /dev/null
  75+++ b/fixtures/a_b_reorder.patch
  76@@ -0,0 +1,55 @@
  77+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
  78+From: Eric Bower <me@erock.io>
  79+Date: Tue, 23 Jul 2024 10:07:57 -0400
  80+Subject: [PATCH 1/2] chore: add torch and create random tensor
  81+
  82+---
  83+ requirements.txt | 1 +
  84+ train.py         | 3 +++
  85+ 2 files changed, 4 insertions(+)
  86+ create mode 100644 requirements.txt
  87+
  88+diff --git a/requirements.txt b/requirements.txt
  89+new file mode 100644
  90+index 0000000..4968a39
  91+--- /dev/null
  92++++ b/requirements.txt
  93+@@ -0,0 +1 @@
  94++torch==2.3.1
  95+diff --git a/train.py b/train.py
  96+index 5c027f4..d21dac3 100644
  97+--- a/train.py
  98++++ b/train.py
  99+@@ -1,2 +1,5 @@
 100++import torch
 101++
 102+ if __name__ == "__main__":
 103+     print("train!")
 104++    torch.rand(3,6)
 105+-- 
 106+2.45.2
 107+
 108+
 109+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
 110+From: Eric Bower <me@erock.io>
 111+Date: Tue, 23 Jul 2024 10:14:37 -0400
 112+Subject: [PATCH 2/2] docs: readme
 113+
 114+---
 115+ README.md | 4 +++-
 116+ 1 file changed, 3 insertions(+), 1 deletion(-)
 117+
 118+diff --git a/README.md b/README.md
 119+index 8f3a780..3043953 100644
 120+--- a/README.md
 121++++ b/README.md
 122+@@ -1,3 +1,5 @@
 123+ # Let's build an RNN
 124+ 
 125+-This repo demonstrates building an RNN using `pytorch`
 126++This repo demonstrates building an RNN using `pytorch`.
 127++
 128++Here is some more readme information.
 129+-- 
 130+2.45.2
 131+
 132diff --git a/fixtures/a_c.patch b/fixtures/a_c.patch
 133new file mode 100644
 134index 0000000..4960260
 135--- /dev/null
 136+++ b/fixtures/a_c.patch
 137@@ -0,0 +1,33 @@
 138+From 166848469e0b954c2e14233233f3824a46dcddb8 Mon Sep 17 00:00:00 2001
 139+From: Eric Bower <me@erock.io>
 140+Date: Tue, 23 Jul 2024 10:06:00 -0400
 141+Subject: [PATCH] chore: add torch and create random tensor
 142+
 143+---
 144+ requirements.txt | 1 +
 145+ train.py         | 3 +++
 146+ 2 files changed, 4 insertions(+)
 147+ create mode 100644 requirements.txt
 148+
 149+diff --git a/requirements.txt b/requirements.txt
 150+new file mode 100644
 151+index 0000000..4968a39
 152+--- /dev/null
 153++++ b/requirements.txt
 154+@@ -0,0 +1 @@
 155++torch==2.3.1
 156+diff --git a/train.py b/train.py
 157+index 5c027f4..d21dac3 100644
 158+--- a/train.py
 159++++ b/train.py
 160+@@ -1,2 +1,5 @@
 161++import torch
 162++
 163+ if __name__ == "__main__":
 164+     print("train!")
 165++    torch.rand(3,6)
 166+
 167+base-commit: 59456574a0bfee9f71c91c13046173c820152346
 168+-- 
 169+2.45.2
 170+
 171diff --git a/fixtures/a_c_added_commit.patch b/fixtures/a_c_added_commit.patch
 172new file mode 100644
 173index 0000000..f5ce9f3
 174--- /dev/null
 175+++ b/fixtures/a_c_added_commit.patch
 176@@ -0,0 +1,80 @@
 177+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 178+From: Eric Bower <me@erock.io>
 179+Date: Tue, 23 Jul 2024 10:07:57 -0400
 180+Subject: [PATCH 1/3] chore: add torch and create random tensor
 181+
 182+---
 183+ requirements.txt | 1 +
 184+ train.py         | 3 +++
 185+ 2 files changed, 4 insertions(+)
 186+ create mode 100644 requirements.txt
 187+
 188+diff --git a/requirements.txt b/requirements.txt
 189+new file mode 100644
 190+index 0000000..4968a39
 191+--- /dev/null
 192++++ b/requirements.txt
 193+@@ -0,0 +1 @@
 194++torch==2.3.1
 195+diff --git a/train.py b/train.py
 196+index 5c027f4..d21dac3 100644
 197+--- a/train.py
 198++++ b/train.py
 199+@@ -1,2 +1,5 @@
 200++import torch
 201++
 202+ if __name__ == "__main__":
 203+     print("train!")
 204++    torch.rand(3,6)
 205+-- 
 206+2.45.2
 207+
 208+
 209+From 22dde1259c34a166d5a9335ebe5236e79541cc63 Mon Sep 17 00:00:00 2001
 210+From: Eric Bower <me@erock.io>
 211+Date: Tue, 23 Jul 2024 10:14:37 -0400
 212+Subject: [PATCH 2/3] docs: readme
 213+
 214+---
 215+ README.md | 4 +++-
 216+ 1 file changed, 3 insertions(+), 1 deletion(-)
 217+
 218+diff --git a/README.md b/README.md
 219+index 8f3a780..3043953 100644
 220+--- a/README.md
 221++++ b/README.md
 222+@@ -1,3 +1,5 @@
 223+ # Let's build an RNN
 224+ 
 225+-This repo demonstrates building an RNN using `pytorch`
 226++This repo demonstrates building an RNN using `pytorch`.
 227++
 228++Here is some more readme information.
 229+-- 
 230+2.45.2
 231+
 232+
 233+From b248060488df529b850060b3c86417bb87d490cc Mon Sep 17 00:00:00 2001
 234+From: Eric Bower <me@erock.io>
 235+Date: Tue, 23 Jul 2024 10:20:44 -0400
 236+Subject: [PATCH 3/3] chore: make tensor 6x6
 237+
 238+---
 239+ train.py | 4 +++-
 240+ 1 file changed, 3 insertions(+), 1 deletion(-)
 241+
 242+diff --git a/train.py b/train.py
 243+index d21dac3..8cd47e0 100644
 244+--- a/train.py
 245++++ b/train.py
 246+@@ -2,4 +2,6 @@ import torch
 247+ 
 248+ if __name__ == "__main__":
 249+     print("train!")
 250+-    torch.rand(3,6)
 251++    # let's create a 6x6 tensor!
 252++    tensor = torch.rand(6,6)
 253++    print(tensor)
 254+-- 
 255+2.45.2
 256+
 257diff --git a/fixtures/a_c_changed_commit.patch b/fixtures/a_c_changed_commit.patch
 258new file mode 100644
 259index 0000000..259a434
 260--- /dev/null
 261+++ b/fixtures/a_c_changed_commit.patch
 262@@ -0,0 +1,60 @@
 263+From 33c682ac27479f501924cf159d0a75ad91deb589 Mon Sep 17 00:00:00 2001
 264+From: Eric Bower <me@erock.io>
 265+Date: Tue, 23 Jul 2024 10:07:57 -0400
 266+Subject: [PATCH 1/2] chore: add torch and create random tensor
 267+
 268+---
 269+ requirements.txt | 1 +
 270+ train.py         | 3 +++
 271+ 2 files changed, 4 insertions(+)
 272+ create mode 100644 requirements.txt
 273+
 274+diff --git a/requirements.txt b/requirements.txt
 275+new file mode 100644
 276+index 0000000..4968a39
 277+--- /dev/null
 278++++ b/requirements.txt
 279+@@ -0,0 +1 @@
 280++torch==2.3.1
 281+diff --git a/train.py b/train.py
 282+index 5c027f4..d21dac3 100644
 283+--- a/train.py
 284++++ b/train.py
 285+@@ -1,2 +1,5 @@
 286++import torch
 287++
 288+ if __name__ == "__main__":
 289+     print("train!")
 290++    torch.rand(3,6)
 291+-- 
 292+2.45.2
 293+
 294+
 295+From dce20e70280d92aeb88c3d603ad67043ead772fb Mon Sep 17 00:00:00 2001
 296+From: Eric Bower <me@erock.io>
 297+Date: Tue, 23 Jul 2024 10:14:37 -0400
 298+Subject: [PATCH 2/2] docs: readme
 299+
 300+---
 301+ README.md | 9 ++++++++-
 302+ 1 file changed, 8 insertions(+), 1 deletion(-)
 303+
 304+diff --git a/README.md b/README.md
 305+index 8f3a780..ba0293b 100644
 306+--- a/README.md
 307++++ b/README.md
 308+@@ -1,3 +1,10 @@
 309+ # Let's build an RNN
 310+ 
 311+-This repo demonstrates building an RNN using `pytorch`
 312++This repo demonstrates building an RNN using `pytorch`.
 313++
 314++Here is some more readme information.
 315++
 316++Here is how to run this project locally:
 317++
 318++- install python and pip
 319++- `pip install -r requirements.txt`
 320+-- 
 321+2.45.2
 322+
 323diff --git a/fixtures/a_c_reorder.patch b/fixtures/a_c_reorder.patch
 324new file mode 100644
 325index 0000000..bc90f03
 326--- /dev/null
 327+++ b/fixtures/a_c_reorder.patch
 328@@ -0,0 +1,55 @@
 329+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
 330+From: Eric Bower <me@erock.io>
 331+Date: Tue, 23 Jul 2024 10:15:23 -0400
 332+Subject: [PATCH 1/2] docs: readme
 333+
 334+---
 335+ README.md | 4 +++-
 336+ 1 file changed, 3 insertions(+), 1 deletion(-)
 337+
 338+diff --git a/README.md b/README.md
 339+index 8f3a780..3043953 100644
 340+--- a/README.md
 341++++ b/README.md
 342+@@ -1,3 +1,5 @@
 343+ # Let's build an RNN
 344+ 
 345+-This repo demonstrates building an RNN using `pytorch`
 346++This repo demonstrates building an RNN using `pytorch`.
 347++
 348++Here is some more readme information.
 349+-- 
 350+2.45.2
 351+
 352+
 353+From ad175875e2bf320859554bae73743675cc5ce444 Mon Sep 17 00:00:00 2001
 354+From: Eric Bower <me@erock.io>
 355+Date: Tue, 23 Jul 2024 10:06:00 -0400
 356+Subject: [PATCH 2/2] chore: add torch and create random tensor
 357+
 358+---
 359+ requirements.txt | 1 +
 360+ train.py         | 3 +++
 361+ 2 files changed, 4 insertions(+)
 362+ create mode 100644 requirements.txt
 363+
 364+diff --git a/requirements.txt b/requirements.txt
 365+new file mode 100644
 366+index 0000000..4968a39
 367+--- /dev/null
 368++++ b/requirements.txt
 369+@@ -0,0 +1 @@
 370++torch==2.3.1
 371+diff --git a/train.py b/train.py
 372+index 5c027f4..d21dac3 100644
 373+--- a/train.py
 374++++ b/train.py
 375+@@ -1,2 +1,5 @@
 376++import torch
 377++
 378+ if __name__ == "__main__":
 379+     print("train!")
 380++    torch.rand(3,6)
 381+-- 
 382+2.45.2
 383+
 384diff --git a/fixtures/a_c_rm_commit.patch b/fixtures/a_c_rm_commit.patch
 385new file mode 100644
 386index 0000000..c6a4c5e
 387--- /dev/null
 388+++ b/fixtures/a_c_rm_commit.patch
 389@@ -0,0 +1,23 @@
 390+From 7dbb94ca1bc8cadf1ce17dacb89172217d88de07 Mon Sep 17 00:00:00 2001
 391+From: Eric Bower <me@erock.io>
 392+Date: Tue, 23 Jul 2024 10:15:23 -0400
 393+Subject: [PATCH] docs: readme
 394+
 395+---
 396+ README.md | 4 +++-
 397+ 1 file changed, 3 insertions(+), 1 deletion(-)
 398+
 399+diff --git a/README.md b/README.md
 400+index 8f3a780..3043953 100644
 401+--- a/README.md
 402++++ b/README.md
 403+@@ -1,3 +1,5 @@
 404+ # Let's build an RNN
 405+ 
 406+-This repo demonstrates building an RNN using `pytorch`
 407++This repo demonstrates building an RNN using `pytorch`.
 408++
 409++Here is some more readme information.
 410+-- 
 411+2.45.2
 412+
 413diff --git a/fixtures/expected_commit_changed.txt b/fixtures/expected_commit_changed.txt
 414new file mode 100644
 415index 0000000..ac4499c
 416--- /dev/null
 417+++ b/fixtures/expected_commit_changed.txt
 418@@ -0,0 +1,11 @@
 419+1:  33c682a = 1:  33c682a chore: add torch and create random tensor
 420+2:  22dde12 ! 2:  0185f34 docs: readme
 421+    @@ README.md
 422+     +This repo demonstrates building an RNN using `pytorch`.
 423+     +
 424+     +Here is some more readme information.
 425+    ++
 426+    ++Here is how to run this project locally:
 427+    ++
 428+    ++- install python and pip
 429+    ++- `pip install -r requirements.txt`
 430diff --git a/go.mod b/go.mod
 431index 4dc95e6..53504d3 100644
 432--- a/go.mod
 433+++ b/go.mod
 434@@ -14,6 +14,7 @@ require (
 435 	github.com/knadh/koanf/providers/env v0.1.0
 436 	github.com/knadh/koanf/providers/file v1.0.0
 437 	github.com/knadh/koanf/v2 v2.1.1
 438+	github.com/sergi/go-diff v1.1.0
 439 	github.com/urfave/cli/v2 v2.27.2
 440 	golang.org/x/crypto v0.21.0
 441 	modernc.org/sqlite v1.27.0
 442diff --git a/go.sum b/go.sum
 443index 1ec46c6..5bc1160 100644
 444--- a/go.sum
 445+++ b/go.sum
 446@@ -8,8 +8,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
 447 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 448 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 449 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 450-github.com/bluekeyes/go-gitdiff v0.7.2 h1:42jrcVZdjjxXtVsFNYTo/I6T1ZvIiQL+iDDLiH904hw=
 451-github.com/bluekeyes/go-gitdiff v0.7.2/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
 452 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190 h1:k6Ep4yQtmsoP/St4bf7ofXyWc6ITB/FyGy9ewaAn5os=
 453 github.com/bluekeyes/go-gitdiff v0.7.4-0.20240715034416-0a4e55f9a190/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
 454 github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
 455@@ -36,6 +34,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV
 456 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 457 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
 458 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 459+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 460 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 461 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 462 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 463@@ -74,8 +73,11 @@ github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcnc
 464 github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
 465 github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
 466 github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
 467+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 468 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 469 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 470+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 471+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 472 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 473 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 474 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 475@@ -107,6 +109,7 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo
 476 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 477 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 478 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 479+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 480 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 481 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 482 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 483@@ -119,6 +122,10 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
 484 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 485 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 486 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 487+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 488+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 489+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 490+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 491 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 492 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 493 github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
 494@@ -143,6 +150,10 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 495 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 496 golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
 497 golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
 498+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 499+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 500+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 501+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 502 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 503 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 504 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 505diff --git a/pr.go b/pr.go
 506index 9e535a0..984fe92 100644
 507--- a/pr.go
 508+++ b/pr.go
 509@@ -447,7 +447,7 @@ func (cmd PrCmd) SubmitPatchRequest(repoID string, userID int64, patchset io.Rea
 510 		_ = tx.Rollback()
 511 	}()
 512 
 513-	patches, err := parsePatchset(patchset)
 514+	patches, err := ParsePatchset(patchset)
 515 	if err != nil {
 516 		return nil, err
 517 	}
 518@@ -541,7 +541,7 @@ func (cmd PrCmd) SubmitPatchset(prID int64, userID int64, op PatchsetOp, patchse
 519 		_ = tx.Rollback()
 520 	}()
 521 
 522-	patches, err := parsePatchset(patchset)
 523+	patches, err := ParsePatchset(patchset)
 524 	if err != nil {
 525 		return fin, err
 526 	}
 527diff --git a/range_diff.go b/range_diff.go
 528new file mode 100644
 529index 0000000..28cfaef
 530--- /dev/null
 531+++ b/range_diff.go
 532@@ -0,0 +1,188 @@
 533+package git
 534+
 535+import (
 536+	"fmt"
 537+	"math"
 538+
 539+	"github.com/sergi/go-diff/diffmatchpatch"
 540+)
 541+
 542+var COST_MAX = 65536
 543+var RANGE_DIFF_CREATION_FACTOR_DEFAULT = 60
 544+
 545+type PatchRange struct {
 546+	*Patch
 547+	Matching int
 548+}
 549+
 550+func NewPatchRange(patch *Patch) *PatchRange {
 551+	return &PatchRange{
 552+		Patch: patch,
 553+	}
 554+}
 555+
 556+func output(a []*PatchRange, b []*PatchRange) string {
 557+	out := ""
 558+	for _, patchB := range b {
 559+		patchA := a[patchB.Matching]
 560+		if patchB.ContentSha == patchA.ContentSha {
 561+			out += outputPairHeader(patchA, patchB, patchB.Matching+1, patchA.Matching+1)
 562+		}
 563+	}
 564+	return out
 565+}
 566+
 567+func outputPairHeader(a *PatchRange, b *PatchRange, aIndex, bIndex int) string {
 568+	return fmt.Sprintf("%d:  %s = %d:  %s %s\n", aIndex, truncateSha(a.CommitSha), bIndex, truncateSha(b.CommitSha), a.Title)
 569+}
 570+
 571+func RangeDiff(a []*Patch, b []*Patch) string {
 572+	aPatches := []*PatchRange{}
 573+	for _, patch := range a {
 574+		aPatches = append(aPatches, NewPatchRange(patch))
 575+	}
 576+	bPatches := []*PatchRange{}
 577+	for _, patch := range b {
 578+		bPatches = append(bPatches, NewPatchRange(patch))
 579+	}
 580+	findExactMatches(aPatches, bPatches)
 581+	getCorrespondences(aPatches, bPatches, RANGE_DIFF_CREATION_FACTOR_DEFAULT)
 582+	return output(aPatches, bPatches)
 583+}
 584+
 585+func findExactMatches(a []*PatchRange, b []*PatchRange) {
 586+	for i, patchA := range a {
 587+		for j, patchB := range b {
 588+			if patchA.ContentSha == patchB.ContentSha {
 589+				patchA.Matching = j
 590+				patchB.Matching = i
 591+			}
 592+		}
 593+	}
 594+}
 595+
 596+func createMatrix(rows, cols int) [][]int {
 597+	mat := make([][]int, rows)
 598+	for i := range mat {
 599+		mat[i] = make([]int, cols)
 600+	}
 601+	return mat
 602+}
 603+
 604+func diffsize(a *PatchRange, b *PatchRange) int {
 605+	dmp := diffmatchpatch.New()
 606+	diffs := dmp.DiffMain(a.RawText, b.RawText, false)
 607+	return len(dmp.DiffPrettyText(diffs))
 608+}
 609+
 610+func getCorrespondences(a []*PatchRange, b []*PatchRange, creationFactor int) {
 611+	// n := len(a) + len(b)
 612+	fmt.Println(len(a), len(b))
 613+	cost := createMatrix(len(a), len(b))
 614+
 615+	for i, patchA := range a {
 616+		var c int
 617+		for j, patchB := range b {
 618+			if patchA.Matching == j {
 619+				c = 0
 620+			} else if patchA.Matching == 0 && patchB.Matching == 0 {
 621+				c = diffsize(patchA, patchB)
 622+			} else {
 623+				c = COST_MAX
 624+			}
 625+			cost[i][j] = c
 626+		}
 627+	}
 628+
 629+	assignment := computeAssignment(cost, len(a), len(b))
 630+	for i, j := range assignment {
 631+		if j < len(b) {
 632+			a[i].Matching = j
 633+			b[j].Matching = i
 634+		}
 635+	}
 636+
 637+	fmt.Println(cost, assignment)
 638+	fmt.Println("A==")
 639+	for _, patch := range a {
 640+		fmt.Println("matches", b[patch.Matching].Title)
 641+	}
 642+
 643+	fmt.Println("B==")
 644+	for _, patch := range b {
 645+		fmt.Println("matches", a[patch.Matching].Title)
 646+	}
 647+}
 648+
 649+// computeAssignment assigns patches using the Hungarian algorithm.
 650+func computeAssignment(costMatrix [][]int, m, n int) []int {
 651+	u := make([]int, m+1) // potential for workers
 652+	v := make([]int, n+1) // potential for jobs
 653+	p := make([]int, n+1) // job assignment
 654+	way := make([]int, n+1)
 655+
 656+	for i := 1; i <= m; i++ {
 657+		links := make([]int, n+1)
 658+		minV := make([]int, n+1)
 659+		used := make([]bool, n+1)
 660+		for j := 0; j <= n; j++ {
 661+			minV[j] = math.MaxInt32
 662+			used[j] = false
 663+		}
 664+
 665+		j0 := 0
 666+		p[0] = i
 667+
 668+		for {
 669+			used[j0] = true
 670+			i0 := p[j0]
 671+			delta := math.MaxInt32
 672+			j1 := 0
 673+
 674+			for j := 1; j <= n; j++ {
 675+				if !used[j] {
 676+					cur := costMatrix[i0-1][j-1] - u[i0] - v[j]
 677+					if cur < minV[j] {
 678+						minV[j] = cur
 679+						links[j] = j0
 680+					}
 681+					if minV[j] < delta {
 682+						delta = minV[j]
 683+						j1 = j
 684+					}
 685+				}
 686+			}
 687+
 688+			for j := 0; j <= n; j++ {
 689+				if used[j] {
 690+					u[p[j]] += delta
 691+					v[j] -= delta
 692+				} else {
 693+					minV[j] -= delta
 694+				}
 695+			}
 696+
 697+			j0 = j1
 698+			if p[j0] == 0 {
 699+				break
 700+			}
 701+		}
 702+
 703+		for {
 704+			j1 := way[j0]
 705+			p[j0] = p[j1]
 706+			j0 = j1
 707+			if j0 == 0 {
 708+				break
 709+			}
 710+		}
 711+	}
 712+
 713+	assignment := make([]int, m)
 714+	for j := 1; j <= n; j++ {
 715+		if p[j] > 0 {
 716+			assignment[p[j]-1] = j - 1
 717+		}
 718+	}
 719+	return assignment
 720+}
 721diff --git a/range_diff_test.go b/range_diff_test.go
 722new file mode 100644
 723index 0000000..8705bff
 724--- /dev/null
 725+++ b/range_diff_test.go
 726@@ -0,0 +1,274 @@
 727+package git
 728+
 729+import (
 730+	"fmt"
 731+	"testing"
 732+
 733+	"github.com/picosh/git-pr/fixtures"
 734+)
 735+
 736+func bail(err error) {
 737+	if err != nil {
 738+		panic(bail)
 739+	}
 740+}
 741+
 742+func cmp(afile, bfile string) string {
 743+	a, err := fixtures.Fixtures.Open(afile)
 744+	bail(err)
 745+	b, err := fixtures.Fixtures.Open(bfile)
 746+	bail(err)
 747+	aPatches, err := ParsePatchset(a)
 748+	bail(err)
 749+	bPatches, err := ParsePatchset(b)
 750+	bail(err)
 751+	actual := RangeDiff(aPatches, bPatches)
 752+	return actual
 753+}
 754+
 755+func fail(expected, actual string) string {
 756+	return fmt.Sprintf("expected:[%s] actual:[%s]", expected, actual)
 757+}
 758+
 759+// https://git.kernel.org/tree/t/t3206-range-diff.sh?id=d19b6cd2dd72dc811f19df4b32c7ed223256c3ee
 760+
 761+// simple A..B A..C (unmodified)
 762+/*
 763+	1:  $(test_oid t1) = 1:  $(test_oid u1) s/5/A/
 764+	2:  $(test_oid t2) = 2:  $(test_oid u2) s/4/A/
 765+	3:  $(test_oid t3) = 3:  $(test_oid u3) s/11/B/
 766+	4:  $(test_oid t4) = 4:  $(test_oid u4) s/12/B/
 767+*/
 768+func TestRangeDiffUnmodified(t *testing.T) {
 769+	actual := cmp("a_b.patch", "a_c.patch")
 770+	expected := "1:  33c682a = 1:  1668484 chore: add torch and create random tensor\n"
 771+	if expected != actual {
 772+		t.Fatalf(fail(expected, actual))
 773+	}
 774+}
 775+
 776+// trivial reordering
 777+/*
 778+	1:  $(test_oid t1) = 1:  $(test_oid r1) s/5/A/
 779+	3:  $(test_oid t3) = 2:  $(test_oid r2) s/11/B/
 780+	4:  $(test_oid t4) = 3:  $(test_oid r3) s/12/B/
 781+	2:  $(test_oid t2) = 4:  $(test_oid r4) s/4/A/
 782+*/
 783+func TestRangeDiffTrivialReordering(t *testing.T) {
 784+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
 785+	expected := `2:  22dde12 = 1:  7dbb94c docs: readme
 786+1:  33c682a = 2:  ad17587 chore: add torch and create random tensor
 787+`
 788+	if expected != actual {
 789+		t.Fatalf(fail(expected, actual))
 790+	}
 791+}
 792+
 793+// removed commit
 794+/*
 795+	1:  $(test_oid t1) = 1:  $(test_oid d1) s/5/A/
 796+	2:  $(test_oid t2) < -:  $(test_oid __) s/4/A/
 797+	3:  $(test_oid t3) = 2:  $(test_oid d2) s/11/B/
 798+	4:  $(test_oid t4) = 3:  $(test_oid d3) s/12/B/
 799+*/
 800+func TestRangeDiffRemovedCommit(t *testing.T) {
 801+	actual := cmp("a_b_reorder.patch", "a_c_reorder.patch")
 802+	expected := `1:  33c682a < -:  ------- chore: add torch and create random tensor
 803+2:  22dde12 = 1:  7dbb94c docs: readme`
 804+	if expected != actual {
 805+		t.Fatalf(fail(expected, actual))
 806+	}
 807+}
 808+
 809+// added commit
 810+/*
 811+	1:  $(test_oid t1) = 1:  $(test_oid a1) s/5/A/
 812+	2:  $(test_oid t2) = 2:  $(test_oid a2) s/4/A/
 813+	-:  $(test_oid __) > 3:  $(test_oid a3) s/6/A/
 814+	3:  $(test_oid t3) = 4:  $(test_oid a4) s/11/B/
 815+	4:  $(test_oid t4) = 5:  $(test_oid a5) s/12/B/
 816+*/
 817+/* func TestRangeDiffAddedCommit(t *testing.T) {
 818+	actual := ""
 819+	expected := `1:  33c682a = 1:  33c682a chore: add torch and create random tensor
 820+2:  22dde12 = 2:  22dde12 docs: readme
 821+-:  ------- > 3:  b248060 chore: make tensor 6x6`
 822+	if expected != actual {
 823+		t.Fatalf("expected:%s actual:%s", expected, actual)
 824+	}
 825+} */
 826+
 827+// changed commit
 828+/*
 829+	1:  $(test_oid t1) = 1:  $(test_oid c1) s/5/A/
 830+	2:  $(test_oid t2) = 2:  $(test_oid c2) s/4/A/
 831+	3:  $(test_oid t3) ! 3:  $(test_oid c3) s/11/B/
 832+	    @@ file: A
 833+	      9
 834+	      10
 835+	     -11
 836+	    -+B
 837+	    ++BB
 838+	      12
 839+	      13
 840+	      14
 841+	4:  $(test_oid t4) ! 4:  $(test_oid c4) s/12/B/
 842+	    @@ file
 843+	     @@ file: A
 844+	      9
 845+	      10
 846+	    - B
 847+	    + BB
 848+	     -12
 849+	     +B
 850+	      13
 851+*/
 852+/* func TestRangeDiffChangedCommit(t *testing.T) {
 853+	actual := ""
 854+	fp, err := fixtures.Fixtures.ReadFile("extected_commit_changed.txt")
 855+	if err != nil {
 856+		t.Fatalf("file not found")
 857+	}
 858+	expected := string(fp)
 859+	if expected != actual {
 860+		t.Fatalf("expected:%s actual:%s", expected, actual)
 861+	}
 862+} */
 863+
 864+// renamed file
 865+/*
 866+	1:  $(test_oid t1) = 1:  $(test_oid n1) s/5/A/
 867+	2:  $(test_oid t2) ! 2:  $(test_oid n2) s/4/A/
 868+	    @@ Metadata
 869+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 870+	    Z
 871+	    Z ## Commit message ##
 872+	    -    s/4/A/
 873+	    +    s/4/A/ + rename file
 874+	    Z
 875+	    - ## file ##
 876+	    + ## file => renamed-file ##
 877+	    Z@@
 878+	    Z 1
 879+	    Z 2
 880+	3:  $(test_oid t3) ! 3:  $(test_oid n3) s/11/B/
 881+	    @@ Metadata
 882+	    Z ## Commit message ##
 883+	    Z    s/11/B/
 884+	    Z
 885+	    - ## file ##
 886+	    -@@ file: A
 887+	    + ## renamed-file ##
 888+	    +@@ renamed-file: A
 889+	    Z 8
 890+	    Z 9
 891+	    Z 10
 892+	4:  $(test_oid t4) ! 4:  $(test_oid n4) s/12/B/
 893+	    @@ Metadata
 894+	    Z ## Commit message ##
 895+	    Z    s/12/B/
 896+	    Z
 897+	    - ## file ##
 898+	    -@@ file: A
 899+	    + ## renamed-file ##
 900+	    +@@ renamed-file: A
 901+	    Z 9
 902+	    Z 10
 903+	    Z B
 904+*/
 905+// func TestRangeDiffRenamedFile(t *testing.T) {}
 906+
 907+// file with mode only change
 908+/*
 909+	1:  $(test_oid t2) ! 1:  $(test_oid o1) s/4/A/
 910+	    @@ Metadata
 911+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 912+	    Z
 913+	    Z ## Commit message ##
 914+	    -    s/4/A/
 915+	    +    s/4/A/ + add other-file
 916+	    Z
 917+	    Z ## file ##
 918+	    Z@@
 919+	    @@ file
 920+	    Z A
 921+	    Z 6
 922+	    Z 7
 923+	    +
 924+	    + ## other-file (new) ##
 925+	2:  $(test_oid t3) ! 2:  $(test_oid o2) s/11/B/
 926+	    @@ Metadata
 927+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 928+	    Z
 929+	    Z ## Commit message ##
 930+	    -    s/11/B/
 931+	    +    s/11/B/ + mode change other-file
 932+	    Z
 933+	    Z ## file ##
 934+	    Z@@ file: A
 935+	    @@ file: A
 936+	    Z 12
 937+	    Z 13
 938+	    Z 14
 939+	    +
 940+	    + ## other-file (mode change 100644 => 100755) ##
 941+	3:  $(test_oid t4) = 3:  $(test_oid o3) s/12/B/
 942+*/
 943+// func TestRangeDiffFileWithModeOnlyChange(t *testing.T) {}
 944+
 945+// file added and later removed
 946+/*
 947+	1:  $(test_oid t1) = 1:  $(test_oid s1) s/5/A/
 948+	2:  $(test_oid t2) ! 2:  $(test_oid s2) s/4/A/
 949+	    @@ Metadata
 950+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 951+	    Z
 952+	    Z ## Commit message ##
 953+	    -    s/4/A/
 954+	    +    s/4/A/ + new-file
 955+	    Z
 956+	    Z ## file ##
 957+	    Z@@
 958+	    @@ file
 959+	    Z A
 960+	    Z 6
 961+	    Z 7
 962+	    +
 963+	    + ## new-file (new) ##
 964+	3:  $(test_oid t3) ! 3:  $(test_oid s3) s/11/B/
 965+	    @@ Metadata
 966+	    ZAuthor: Thomas Rast <trast@inf.ethz.ch>
 967+	    Z
 968+	    Z ## Commit message ##
 969+	    -    s/11/B/
 970+	    +    s/11/B/ + remove file
 971+	    Z
 972+	    Z ## file ##
 973+	    Z@@ file: A
 974+	    @@ file: A
 975+	    Z 12
 976+	    Z 13
 977+	    Z 14
 978+	    +
 979+	    + ## new-file (deleted) ##
 980+	4:  $(test_oid t4) = 4:  $(test_oid s4) s/12/B/
 981+*/
 982+// func TestRangeDiffFileAddedThenRemoved(t *testing.T) {}
 983+
 984+// changed message
 985+/*
 986+	1:  $(test_oid t1) = 1:  $(test_oid m1) s/5/A/
 987+	2:  $(test_oid t2) ! 2:  $(test_oid m2) s/4/A/
 988+	    @@ Metadata
 989+	    Z ## Commit message ##
 990+	    Z    s/4/A/
 991+	    Z
 992+	    +    Also a silly comment here!
 993+	    +
 994+	    Z ## file ##
 995+	    Z@@
 996+	    Z 1
 997+	3:  $(test_oid t3) = 3:  $(test_oid m3) s/11/B/
 998+	4:  $(test_oid t4) = 4:  $(test_oid m4) s/12/B/
 999+*/
1000+// func TestRangeDiffChangedMessage(t *testing.T) {}
1001diff --git a/util.go b/util.go
1002index 061bb6d..da69a60 100644
1003--- a/util.go
1004+++ b/util.go
1005@@ -109,7 +109,7 @@ func patchToDiff(patch io.Reader) (string, error) {
1006 	return str[idx:], nil
1007 }
1008 
1009-func parsePatchset(patchset io.Reader) ([]*Patch, error) {
1010+func ParsePatchset(patchset io.Reader) ([]*Patch, error) {
1011 	patches := []*Patch{}
1012 	buf := new(strings.Builder)
1013 	_, err := io.Copy(buf, patchset)
1014@@ -172,12 +172,11 @@ func calcContentSha(diffFiles []*gitdiff.File, header *gitdiff.PatchHeader) stri
1015 		authorEmail = header.Author.Email
1016 	}
1017 	content := fmt.Sprintf(
1018-		"%s\n%s\n%s\n%s\n%s\n",
1019+		"%s\n%s\n%s\n%s\n",
1020 		header.Title,
1021 		header.Body,
1022 		authorName,
1023 		authorEmail,
1024-		header.AuthorDate,
1025 	)
1026 	for _, diff := range diffFiles {
1027 		// we need to ignore diffs with base commit because that depends
1028diff --git a/util_test.go b/util_test.go
1029index eaba33d..e579254 100644
1030--- a/util_test.go
1031+++ b/util_test.go
1032@@ -15,7 +15,7 @@ func TestParsePatchsetWithCover(t *testing.T) {
1033 	if err != nil {
1034 		t.Fatalf(err.Error())
1035 	}
1036-	actual, err := parsePatchset(file)
1037+	actual, err := ParsePatchset(file)
1038 	if err != nil {
1039 		t.Fatalf(err.Error())
1040 	}
1041-- 
10422.45.2
1043