This opens doors for many features like following renames, no run-time
dependencies, clean isolation, extra history features (like rich diffs),
and even an embedded git server! It should also handle errors better,
because now we're in control of every git operation executed, unlike
with shelling out to the git CLI.
---
Review with care! There are probably a lot of edge cases that have bugs
categories/categories.go | 26 +-
go.mod | 17 +-
go.sum | 81 +++++-
help/help.go | 3 +-
history/diff.go | 59 ++++
history/feed.go | 56 ++--
history/history.go | 81 ++----
history/histweb/histview.go | 15 +-
history/histweb/view_primitive_diff.html | 2 +-
history/histweb/view_recent_changes.html | 36 +--
history/operations.go | 77 +++--
history/revision.go | 346 ++++++++++-------------
history/revlog.go | 142 ++++++++++
history/view.qtpl | 17 +-
history/view.qtpl.go | 273 +++++++++---------
l18n/l18n.go | 1 +
main.go | 6 +-
migration/migration.go | 4 +-
shroom/upload.go | 15 +-
util/util.go | 3 +-
20 files changed, 747 insertions(+), 513 deletions(-)
create mode 100644 history/diff.go
create mode 100644 history/revlog.go
diff --git a/categories/categories.go b/categories/categories.go
index 4a093f5..d49ee3d 100644
--- a/categories/categories.go
+++ b/categories/categories.go
@@ -3,22 +3,22 @@
// As per the long pondering, this is how categories (cats for short)
// work in Mycorrhiza:
//
-// - Cats are not hyphae. Cats are separate entities. This is not as
-// vibeful as I would have wanted, but seems to be more practical
-// due to //the reasons//.
-// - Cats are stored outside of git. Instead, they are stored in a
-// JSON file, path to which is determined by files.CategoriesJSON.
-// - Due to not being stored in git, no cat history is tracked, and
-// cat operations are not mentioned on the recent changes page.
-// - For cat A, if there are 0 hyphae in the cat, cat A does not
-// exist. If there are 1 or more hyphae in the cat, cat A exists.
+// - Cats are not hyphae. Cats are separate entities. This is not as
+// vibeful as I would have wanted, but seems to be more practical
+// due to //the reasons//.
+// - Cats are stored outside of git. Instead, they are stored in a
+// JSON file, path to which is determined by files.CategoriesJSON.
+// - Due to not being stored in git, no cat history is tracked, and
+// cat operations are not mentioned on the recent changes page.
+// - For cat A, if there are 0 hyphae in the cat, cat A does not
+// exist. If there are 1 or more hyphae in the cat, cat A exists.
//
// List of things to do with categories later:
//
-// - Forbid / in cat names.
-// - Rename categories.
-// - Delete categories.
-// - Bind hyphae.
+// - Forbid / in cat names.
+// - Rename categories.
+// - Delete categories.
+// - Bind hyphae.
package categories
import "sync"
diff --git a/go.mod b/go.mod
index 79779d7..1dca575 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.18
require (
github.com/bouncepaw/mycomarkup/v5 v5.1.2
+ github.com/go-git/go-git/v5 v5.4.2
github.com/go-ini/ini v1.63.2
github.com/gorilla/feeds v1.1.1
github.com/gorilla/mux v1.8.0
@@ -15,10 +16,22 @@ require (
)
require (
- github.com/kr/pretty v0.2.1 // indirect
- github.com/stretchr/testify v1.7.0 // indirect
+ github.com/Microsoft/go-winio v0.4.16 // indirect
+ github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
+ github.com/acomagu/bufpipe v1.0.3 // indirect
+ github.com/emirpasic/gods v1.12.0 // indirect
+ github.com/go-git/gcfg v1.5.0 // indirect
+ github.com/go-git/go-billy/v5 v5.3.1 // indirect
+ github.com/imdario/mergo v0.3.12 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/sergi/go-diff v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/xanzy/ssh-agent v0.3.0 // indirect
+ golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
)
// Use this trick to test local Mycomarkup changes, replace the path with yours,
diff --git a/go.sum b/go.sum
index 06428ae..bc23362 100644
--- a/go.sum
+++ b/go.sum
@@ -1,26 +1,79 @@
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
+github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
+github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
+github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bouncepaw/mycomarkup/v5 v5.1.2 h1:0ARUfHMRCWygp/ATq8yjdM7Mxf3beXA1XYV9oB3YW9A=
github.com/bouncepaw/mycomarkup/v5 v5.1.2/go.mod h1:jyB/vxKe3X8SsN7FjjPf24IZwFM/H1C4LNvQ5UyXwjU=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
+github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
+github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
+github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
+github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
+github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
github.com/go-ini/ini v1.63.2 h1:kwN3umicd2HF3Tgvap4um1ZG52/WyKT9GGdPx0CJk6Y=
github.com/go-ini/ini v1.63.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
+github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
+github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+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/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -29,15 +82,29 @@ github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
+github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd h1:zVFyTKZN/Q7mNRWSs1GOYnHM9NiFSJ54YVRsD0rNWT4=
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
+golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 h1:T6tyxxvHMj2L1R2kZg0uNMpS8ZhB9lRa9XRGTCSA65w=
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -50,5 +117,15 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+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.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/help/help.go b/help/help.go
index 057d3c1..5091c6e 100644
--- a/help/help.go
+++ b/help/help.go
@@ -9,7 +9,8 @@ import (
var fs embed.FS
// Get determines what help text you need and returns it. The path is a substring from URL, it follows this form:
-// <language>/<topic>
+//
+// <language>/<topic>
func Get(path string) ([]byte, error) {
if path == "" {
return Get("en")
diff --git a/history/diff.go b/history/diff.go
new file mode 100644
index 0000000..45ac22e
--- /dev/null
+++ b/history/diff.go
@@ -0,0 +1,59 @@
+package history
+
+import (
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing"
+
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath
+// at the commit with the given hash. It may return an error if git fails.
+func PrimitiveDiffAtRevision(filepath, hash string) (*Revision, string, error) {
+ filepath = util.ShorterPath(filepath)
+ commit, err := repo.CommitObject(plumbing.NewHash(hash))
+ if err != nil {
+ return nil, "", err
+ }
+ rev := parseRevision(commit)
+ oldTree, newTree, err := getDiffTrees(rev.commit)
+ if err != nil {
+ return rev, "", err
+ }
+ patch, err := oldTree.Patch(newTree)
+ if err != nil {
+ return rev, "", err
+ }
+ origPatch := patch.String()
+ encodedPatch := origPatch
+ origPatch = "Failed to find the hypha. Displaying the full diff.\n\n" + origPatch
+ for {
+ idx := strings.Index(encodedPatch, "diff ")
+ if idx == -1 {
+ return rev, origPatch, nil
+ }
+ encodedPatch = encodedPatch[idx:]
+ header := encodedPatch
+ idx = strings.Index(header, "\n")
+ if idx == -1 {
+ return rev, origPatch, nil
+ }
+ header = header[:idx]
+ parts := strings.Split(header, " ")
+ idx = strings.Index(encodedPatch[len(header):], "diff ")
+ if parts[3] == "b/"+filepath {
+ if idx != -1 {
+ encodedPatch = encodedPatch[:idx]
+ }
+ return rev, encodedPatch, nil
+ } else {
+ if idx != -1 {
+ encodedPatch = encodedPatch[idx:]
+ } else {
+ return rev, origPatch, nil
+ }
+ }
+ }
+ return rev, encodedPatch, nil
+}
diff --git a/history/feed.go b/history/feed.go
index d48c5a1..2621eb5 100644
--- a/history/feed.go
+++ b/history/feed.go
@@ -21,7 +21,7 @@ func recentChangesFeed(opts FeedOptions) *feeds.Feed {
Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize),
Updated: time.Now(),
}
- revs := newRecentChangesStream()
+ revs := NewRevLogStream()
groups := groupRevisions(revs, opts)
for _, grp := range groups {
item := grp.feedItem(opts)
@@ -46,29 +46,29 @@ func RecentChangesJSON(opts FeedOptions) (string, error) {
}
// revisionGroup is a slice of revisions, ordered most recent first.
-type revisionGroup []Revision
+type revisionGroup []*Revision
-func newRevisionGroup(rev Revision) revisionGroup {
- return []Revision{rev}
+func newRevisionGroup(revs ...*Revision) revisionGroup {
+ return revs
}
-func (grp *revisionGroup) addRevision(rev Revision) {
+func (grp *revisionGroup) addRevision(rev *Revision) {
*grp = append(*grp, rev)
}
// orderedIndex returns the ith revision in the group following the given order.
-func (grp *revisionGroup) orderedIndex(i int, order feedGroupOrder) *Revision {
+func (grp revisionGroup) orderedIndex(i int, order feedGroupOrder) *Revision {
switch order {
case newToOld:
- return &(*grp)[i]
+ return grp[i]
case oldToNew:
- return &(*grp)[len(*grp)-1-i]
+ return grp[len(grp)-1-i]
}
// unreachable
return nil
}
-func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
+func groupRevisionsByMonth(revs []*Revision) (res []revisionGroup) {
var (
currentYear int
currentMonth time.Month
@@ -88,27 +88,25 @@ func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
// groupRevisions groups revisions for a feed.
// It returns the first changeGroupMaxSize (30) groups.
// The grouping parameter determines when two revisions will be grouped.
-func groupRevisions(revs recentChangesStream, opts FeedOptions) (res []revisionGroup) {
- nextRev := revs.iterator()
- rev, empty := nextRev()
- if empty {
- return res
- }
-
- currGroup := newRevisionGroup(rev)
- for rev, done := nextRev(); !done; rev, done = nextRev() {
+func groupRevisions(revs RevLogStream, opts FeedOptions) (res []revisionGroup) {
+ currGroup := newRevisionGroup()
+ revs.ForEach(func(rev *Revision) error {
if opts.canGroup(currGroup, rev) {
currGroup.addRevision(rev)
} else {
res = append(res, currGroup)
+ currGroup = newRevisionGroup()
if len(res) == changeGroupMaxSize {
- return res
+ return errors.New("stop")
}
- currGroup = newRevisionGroup(rev)
+ currGroup.addRevision(rev)
}
+ return nil
+ })
+ if len(currGroup) > 0 {
+ res = append(res, currGroup)
}
- // no more revisions, haven't added the last group yet
- return append(res, currGroup)
+ return res
}
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
@@ -232,14 +230,8 @@ func (parser *feedOptionParserState) parseFeedGroupingSame(query url.Values) err
for _, sameCond := range same {
switch sameCond {
case "author":
- if cond.author {
- return errors.New("set same=author twice")
- }
cond.author = true
case "message":
- if cond.message {
- return errors.New("set same=message twice")
- }
cond.message = true
default:
return errors.New("unknown same option " + sameCond)
@@ -281,7 +273,7 @@ func (parser *feedOptionParserState) parseFeedGroupingOrder(query url.Values) er
}
// canGroup determines whether a revision can be added to a group.
-func (opts FeedOptions) canGroup(grp revisionGroup, rev Revision) bool {
+func (opts FeedOptions) canGroup(grp revisionGroup, rev *Revision) bool {
if len(opts.conds) == 0 {
return false
}
@@ -295,7 +287,7 @@ func (opts FeedOptions) canGroup(grp revisionGroup, rev Revision) bool {
}
type groupingCondition interface {
- canGroup(grp revisionGroup, rev Revision) bool
+ canGroup(grp revisionGroup, rev *Revision) bool
}
// periodGroupingCondition will group two revisions if they are within period of each other.
@@ -303,7 +295,7 @@ type periodGroupingCondition struct {
period time.Duration
}
-func (cond periodGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
+func (cond periodGroupingCondition) canGroup(grp revisionGroup, rev *Revision) bool {
return grp[len(grp)-1].Time.Sub(rev.Time) < cond.period
}
@@ -312,7 +304,7 @@ type sameGroupingCondition struct {
message bool
}
-func (c sameGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
+func (c sameGroupingCondition) canGroup(grp revisionGroup, rev *Revision) bool {
return (!c.author || grp[0].Username == rev.Username) &&
(!c.message || grp[0].Message == rev.Message)
}
diff --git a/history/history.go b/history/history.go
index 559d9d5..029557e 100644
--- a/history/history.go
+++ b/history/history.go
@@ -2,82 +2,33 @@
package history
import (
- "bytes"
- "fmt"
+ "errors"
"log"
- "os/exec"
- "path/filepath"
"regexp"
+ "github.com/go-git/go-git/v5"
+
"github.com/bouncepaw/mycorrhiza/files"
- "github.com/bouncepaw/mycorrhiza/util"
)
-// Path to git executable. Set at init()
-var gitpath string
-
+var repo *git.Repository
+var worktree *git.Worktree
var renameMsgPattern = regexp.MustCompile(`^Rename ā(.*)ā to ā.*ā`)
-var gitEnv = []string{"GIT_COMMITTER_NAME=wikimind", "GIT_COMMITTER_EMAIL=wikimind@mycorrhiza"}
-
-// Start finds git and initializes git credentials.
-func Start() {
- path, err := exec.LookPath("git")
- if err != nil {
- log.Fatal("Could not find the git executable. Check your $PATH.")
- }
- gitpath = path
-}
-
-// InitGitRepo checks a Git repository and initializes it if necessary.
-func InitGitRepo() {
- // Detect if the Git repo directory is a Git repository
- isGitRepo := true
- buf, err := silentGitsh("rev-parse", "--git-dir")
- if err != nil {
- isGitRepo = false
- }
- if isGitRepo {
- gitDir := buf.String()
- if filepath.IsAbs(gitDir) && !filepath.HasPrefix(gitDir, files.HyphaeDir()) {
- isGitRepo = false
+// Init initializes the history subsystem.
+func Init() {
+ var err error
+ repo, err = git.PlainOpen(files.GitRepo())
+ if errors.Is(err, git.ErrRepositoryNotExists) {
+ repo, err = git.PlainInit(files.GitRepo(), false)
+ if err != nil {
+ log.Fatal(err)
}
}
- if !isGitRepo {
- log.Println("Initializing Git repo at", files.HyphaeDir())
- gitsh("init")
- gitsh("config", "core.quotePath", "false")
- }
-}
-// I pronounce it as [gÉŖtĶ”Ź].
-// gitsh is async-safe, therefore all other git-related functions in this module are too.
-func gitsh(args ...string) (out bytes.Buffer, err error) {
- fmt.Printf("$ %v\n", args)
- cmd := exec.Command(gitpath, args...)
- cmd.Dir = files.HyphaeDir()
- cmd.Env = gitEnv
-
- b, err := cmd.CombinedOutput()
+ worktree, err = repo.Worktree()
if err != nil {
- log.Println("gitsh:", err)
+ // Assertion: non-bare repositories must have a worktree
+ panic(err)
}
- return *bytes.NewBuffer(b), err
-}
-
-// silentGitsh is like gitsh, except it writes less to the stdout.
-func silentGitsh(args ...string) (out bytes.Buffer, err error) {
- cmd := exec.Command(gitpath, args...)
- cmd.Dir = files.HyphaeDir()
- cmd.Env = gitEnv
-
- b, err := cmd.CombinedOutput()
- return *bytes.NewBuffer(b), err
-}
-
-// Rename renames from `from` to `to` using `git mv`.
-func Rename(from, to string) error {
- log.Println(util.ShorterPath(from), util.ShorterPath(to))
- _, err := gitsh("mv", "--force", from, to)
- return err
}
diff --git a/history/histweb/histview.go b/history/histweb/histview.go
index 4467b44..6a42ea5 100644
--- a/history/histweb/histview.go
+++ b/history/histweb/histview.go
@@ -61,12 +61,12 @@ func handlerPrimitiveDiff(w http.ResponseWriter, rq *http.Request) {
case *hyphae.EmptyHypha:
mycoFilePath = filepath.Join(files.HyphaeDir(), h.CanonicalName()+".myco")
}
- text, err := history.PrimitiveDiffAtRevision(mycoFilePath, revHash)
+ rev, text, err := history.PrimitiveDiffAtRevision(mycoFilePath, revHash)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- primitiveDiff(viewutil.MetaFrom(w, rq), h, revHash, text)
+ primitiveDiff(viewutil.MetaFrom(w, rq), h, rev.ShortHash(), text)
}
// handlerRecentChanges displays the /recent-changes/ page.
@@ -82,13 +82,10 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
// handlerHistory lists all revisions of a hypha.
func handlerHistory(w http.ResponseWriter, rq *http.Request) {
hyphaName := util.HyphaNameFromRq(rq, "history")
- var list string
// History can be found for files that do not exist anymore.
- revs, err := history.Revisions(hyphaName)
- if err == nil {
- list = history.WithRevisions(hyphaName, revs)
- }
+ revs := history.Revisions(hyphaName)
+ list := history.WithRevisions(hyphaName, revs)
log.Println("Found", len(revs), "revisions for", hyphaName)
historyView(viewutil.MetaFrom(w, rq), hyphaName, list)
@@ -148,12 +145,12 @@ var (
type recentChangesData struct {
*viewutil.BaseData
EditCount int
- Changes []history.Revision
+ Changes []*history.Revision
UserHypha string
Stops []int
}
-func recentChanges(meta viewutil.Meta, editCount int, changes []history.Revision) {
+func recentChanges(meta viewutil.Meta, editCount int, changes []*history.Revision) {
viewutil.ExecutePage(meta, chainRecentChanges, recentChangesData{
BaseData: &viewutil.BaseData{},
EditCount: editCount,
diff --git a/history/histweb/view_primitive_diff.html b/history/histweb/view_primitive_diff.html
index c2025a0..2fd2a7d 100644
--- a/history/histweb/view_primitive_diff.html
+++ b/history/histweb/view_primitive_diff.html
@@ -8,4 +8,4 @@
<pre class="codeblock"><code>{{.Text}}</code></pre>
</article>
</main>
-{{end}}
\ No newline at end of file
+{{end}}
diff --git a/history/histweb/view_recent_changes.html b/history/histweb/view_recent_changes.html
index 02842ff..67cecb3 100644
--- a/history/histweb/view_recent_changes.html
+++ b/history/histweb/view_recent_changes.html
@@ -22,14 +22,14 @@
<div class="recent-changes__entry">
<div>
<time class="recent-changes__entry__time">
- {{ $time.Format "15:04 UTC" }}
+ {{ $time.Format "15:04 UTC" }}
</time>
- <span class="recent-changes__entry__message">{{$entry.Hash}}</span>
- {{ if $entry.Username | ne "anon" }}
+ <span class="recent-changes__entry__message">{{$entry.ShortHash}}</span>
+ {{ if $entry.Username | ne "anon" }}
<span class="recent-changes__entry__author">
— <a href="/hypha/{{$userHypha}}/{{$entry.Username}}" rel="author">{{$entry.Username}}</a>
</span>
- {{end}}
+ {{end}}
</div>
<div>
<span class="recent-changes__entry__links">
@@ -46,24 +46,24 @@
</section>
<p class="recent-changes__count">
- {{block "count pre" .}}See{{end}}
- {{ $editCount := .EditCount }}
- {{range $i, $m := .Stops }}
- {{if gt $i 0}}
- <span aria-hidden="true">|</span>
- {{end}}
- {{if $m | eq $editCount}}
- <b>{{$m}}</b>
- {{else}}
- <a href="/recent-changes/{{$m}}">{{$m}}</a>
- {{end}}
- {{end}}
- {{block "count post" .}}recent changes{{end}}
+ {{block "count pre" .}}See{{end}}
+ {{ $editCount := .EditCount }}
+ {{range $i, $m := .Stops }}
+ {{if gt $i 0}}
+ <span aria-hidden="true">|</span>
+ {{end}}
+ {{if $m | eq $editCount}}
+ <b>{{$m}}</b>
+ {{else}}
+ <a href="/recent-changes/{{$m}}">{{$m}}</a>
+ {{end}}
+ {{end}}
+ {{block "count post" .}}recent changes{{end}}
</p>
<p>
<img class="icon" width="20" height="20" src="/static/icon/feed.svg" aria-hidden="true" alt="RSS icon">
- {{block "subscribe via" .}}Subscribe via <a href="/recent-changes-rss">RSS</a>, <a href="/recent-changes-atom">Atom</a> or <a href="/recent-changes-json">JSON feed</a>.{{end}}
+ {{block "subscribe via" .}}Subscribe via <a href="/recent-changes-rss">RSS</a>, <a href="/recent-changes-atom">Atom</a> or <a href="/recent-changes-json">JSON feed</a>.{{end}}
</p>
</main>
{{end}}
diff --git a/history/operations.go b/history/operations.go
index 1081017..5d40800 100644
--- a/history/operations.go
+++ b/history/operations.go
@@ -3,15 +3,23 @@ package history
// history/operations.go
// Things related to writing history.
import (
- "fmt"
+ "errors"
"os"
"path/filepath"
+ "strings"
"sync"
+ "time"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
+const committerName = "wikimind"
+const committerEmail = "wikimind@mycorrhiza"
+
// gitMutex is used for blocking git operations to avoid clashes.
var gitMutex = sync.Mutex{}
@@ -57,16 +65,6 @@ func Operation(opType OpType) *Op {
return hop
}
-// git operation maker helper
-func (hop *Op) gitop(args ...string) *Op {
- out, err := gitsh(args...)
- if err != nil {
- fmt.Println("out:", out.String())
- hop.Errs = append(hop.Errs, err)
- }
- return hop
-}
-
// withErr appends the `err` to the list of errors.
func (hop *Op) withErr(err error) *Op {
hop.Errs = append(hop.Errs, err)
@@ -80,13 +78,13 @@ func (hop *Op) WithErrAbort(err error) *Op {
// WithFilesRemoved git-rm-s all passed `paths`. Paths can be rooted or not. Paths that are empty strings are ignored.
func (hop *Op) WithFilesRemoved(paths ...string) *Op {
- args := []string{"rm", "--quiet", "--"}
for _, path := range paths {
- if path != "" {
- args = append(args, path)
+ _, err := worktree.Remove(path)
+ if err != nil {
+ hop = hop.withErr(err)
}
}
- return hop.gitop(args...)
+ return hop
}
// WithFilesRenamed git-mv-s all passed keys of `pairs` to values of `pairs`. Paths can be rooted ot not. Empty keys are ignored.
@@ -94,10 +92,17 @@ func (hop *Op) WithFilesRenamed(pairs map[string]string) *Op {
for from, to := range pairs {
if from != "" {
if err := os.MkdirAll(filepath.Dir(to), 0777); err != nil {
- hop.Errs = append(hop.Errs, err)
+ hop = hop.withErr(err)
continue
}
- hop.gitop("mv", "--force", from, to)
+ if err := os.Remove(to); err != nil && !errors.Is(err, os.ErrNotExist) {
+ hop = hop.withErr(err)
+ continue
+ }
+ _, err := worktree.Move(from, to)
+ if err != nil {
+ hop = hop.withErr(err)
+ }
}
}
return hop
@@ -105,20 +110,31 @@ func (hop *Op) WithFilesRenamed(pairs map[string]string) *Op {
// WithFiles stages all passed `paths`. Paths can be rooted or not.
func (hop *Op) WithFiles(paths ...string) *Op {
- for i, path := range paths {
- paths[i] = util.ShorterPath(path)
+ for _, path := range paths {
+ path = util.ShorterPath(path)
+ _, err := worktree.Add(path)
+ if err != nil {
+ hop = hop.withErr(err)
+ }
}
- // 1 git operation is more effective than n operations.
- return hop.gitop(append([]string{"add"}, paths...)...)
+ return hop
}
// Apply applies history operation by doing the commit. You do not need to call Abort afterwards.
func (hop *Op) Apply() *Op {
- hop.gitop(
- "commit",
- "--author='"+hop.name+" <"+hop.email+">'",
- "--message="+hop.userMsg,
- )
+ now := time.Now()
+ worktree.Commit(hop.userMsg, &git.CommitOptions{
+ Author: &object.Signature{
+ Name: hop.name,
+ Email: hop.email,
+ When: now,
+ },
+ Committer: &object.Signature{
+ Name: committerName,
+ Email: committerEmail,
+ When: now,
+ },
+ })
gitMutex.Unlock()
return hop
}
@@ -131,12 +147,11 @@ func (hop *Op) Abort() *Op {
// WithMsg sets what message will be used for the future commit. If user message exceeds one line, it is stripped down.
func (hop *Op) WithMsg(userMsg string) *Op {
- for _, ch := range userMsg {
- if ch == '\r' || ch == '\n' {
- break
- }
- hop.userMsg += string(ch)
+ idx := strings.IndexAny(userMsg, "\r\n")
+ if idx >= 0 {
+ userMsg = userMsg[:idx]
}
+ hop.userMsg = userMsg
return hop
}
diff --git a/history/revision.go b/history/revision.go
index 4817aa1..89df41b 100644
--- a/history/revision.go
+++ b/history/revision.go
@@ -1,254 +1,216 @@
package history
import (
+ "errors"
"fmt"
- "log"
- "regexp"
- "strconv"
"strings"
"time"
- "github.com/bouncepaw/mycorrhiza/files"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
)
-// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
+// Revision represents a revision.
type Revision struct {
- Hash string
- Username string
- Time time.Time
- Message string
- filesAffectedBuf []string
- hyphaeAffectedBuf []string
-}
-
-// gitLog calls `git log` and parses the results.
-func gitLog(args ...string) ([]Revision, error) {
- args = append([]string{
- "log", "--abbrev-commit", "--no-merges",
- "--pretty=format:%h\t%ae\t%at\t%s",
- }, args...)
- out, err := silentGitsh(args...)
- if err != nil {
- return nil, err
- }
-
- outStr := out.String()
- if outStr == "" {
- // if there are no commits to return
- return nil, nil
- }
-
- var revs []Revision
- for _, line := range strings.Split(outStr, "\n") {
- revs = append(revs, parseRevisionLine(line))
- }
- return revs, nil
-}
-
-type recentChangesStream struct {
- currHash string
-}
-
-func newRecentChangesStream() recentChangesStream {
- // next returns the next n revisions from the stream, ordered most recent first.
- // If there are less than n revisions remaining, it will return only those.
- return recentChangesStream{currHash: ""}
-}
-
-func (stream *recentChangesStream) next(n int) []Revision {
- args := []string{"--max-count=" + strconv.Itoa(n)}
- if stream.currHash == "" {
- args = append(args, "HEAD")
- } else {
- // currHash is the last revision from the last call, so skip it
- args = append(args, "--skip=1", stream.currHash)
- }
- // I don't think this can fail, so ignore the error
- res, _ := gitLog(args...)
- if len(res) != 0 {
- stream.currHash = res[len(res)-1].Hash
- }
- return res
-}
-
-// recentChangesIterator returns a function that returns successive revisions from the stream.
-// It buffers revisions to avoid calling git every time.
-func (stream recentChangesStream) iterator() func() (Revision, bool) {
- var buf []Revision
- return func() (Revision, bool) {
- if len(buf) == 0 {
- // no real reason to choose 30, just needs some large number
- buf = stream.next(30)
- if len(buf) == 0 {
- // revs has no revisions left
- return Revision{}, true
+ Hash string
+ Username string
+ Time time.Time
+ Message string
+ HyphaName string
+
+ commit *object.Commit
+
+ // cached properties
+ changesVal object.Changes
+ changesErr error
+ filesAffectedVal []*object.File
+ filesAffectedErr error
+ hyphaeAffectedVal []string
+ hyphaeAffectedErr error
+}
+
+// parseRevision converts a git commit into a revision.
+func parseRevision(commit *object.Commit) *Revision {
+ r := &Revision{
+ Hash: commit.Hash.String(),
+ Username: commit.Author.Name,
+ Time: commit.Author.When,
+ Message: commit.Message,
+ commit: commit,
+ }
+ changes, err := r.changes()
+ if err == nil && len(changes) > 0 {
+ name := changes[0].To.Name
+ if name == "" {
+ name = changes[0].From.Name
+ }
+ if name != "" {
+ first, _, found := strings.Cut(name, ".")
+ if found {
+ r.HyphaName = first
}
}
- rev := buf[0]
- buf = buf[1:]
- return rev, false
}
+ return r
}
-// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice, ordered most recent first.
-func RecentChanges(n int) []Revision {
- stream := newRecentChangesStream()
- revs := stream.next(n)
- log.Printf("Found %d recent changes", len(revs))
- return revs
-}
-
-// Revisions returns slice of revisions for the given hypha name, ordered most recent first.
-func Revisions(hyphaName string) ([]Revision, error) {
- revs, err := gitLog("--", hyphaName+".*")
- log.Printf("Found %d revisions for ā%sā\n", len(revs), hyphaName)
- return revs, err
+// ShortHash returns a shorter version of the revision hash for display.
+func (r *Revision) ShortHash() string {
+ if len(r.Hash) <= 7 {
+ return r.Hash
+ }
+ return r.Hash[:7]
}
-// FileChanged tells you if the file has been changed since the last commit.
-func FileChanged(path string) bool {
- _, err := gitsh("diff", "--exit-code", path)
- return err != nil
+// TimeString returns a human readable time representation.
+func (rev Revision) TimeString() string {
+ return rev.Time.Format(time.RFC822)
}
-// Return time like dd ā 13:42
+// timeToDisplay formats time in the format "dd ā 13:42".
func (rev *Revision) timeToDisplay() string {
D := rev.Time.Day()
h, m, _ := rev.Time.Clock()
return fmt.Sprintf("%02d ā %02d:%02d", D, h, m)
}
-var revisionLinePattern = regexp.MustCompile("(.*)\t(.*)@.*\t(.*)\t(.*)")
-
-// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
-func unixTimestampAsTime(ts string) *time.Time {
- i, err := strconv.ParseInt(ts, 10, 64)
- if err != nil {
- return nil
+// changes computes the changes between this and the parent revision.
+func (rev *Revision) changes() (object.Changes, error) {
+ if rev.changesVal == nil && rev.changesErr == nil {
+ rev.changesVal, rev.changesErr = rev.findChanges()
}
- tm := time.Unix(i, 0)
- return &tm
+ return rev.changesVal, rev.changesErr
}
-func parseRevisionLine(line string) Revision {
- results := revisionLinePattern.FindStringSubmatch(line)
- return Revision{
- Hash: results[1],
- Username: results[2],
- Time: *unixTimestampAsTime(results[3]),
- Message: results[4],
+func (rev *Revision) findChanges() (object.Changes, error) {
+ oldTree, newTree, err := getDiffTrees(rev.commit)
+ if err != nil {
+ return nil, err
}
+ return oldTree.Diff(newTree)
}
// filesAffected tells what files have been affected by the revision.
-func (rev *Revision) filesAffected() (filenames []string) {
- if nil != rev.filesAffectedBuf {
- return rev.filesAffectedBuf
+func (rev *Revision) filesAffected() ([]*object.File, error) {
+ if rev.filesAffectedVal == nil && rev.filesAffectedErr == nil {
+ rev.filesAffectedVal, rev.filesAffectedErr = rev.findFilesAffected()
}
- // List of files affected by this revision, one per line.
- out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
- // There's an error? Well, whatever, let's just assign an empty slice, who cares.
- if err != nil {
- rev.filesAffectedBuf = []string{}
- } else {
- rev.filesAffectedBuf = strings.Split(out.String(), "\n")
- }
- return rev.filesAffectedBuf
+ return rev.filesAffectedVal, rev.filesAffectedErr
}
-// determine what hyphae were affected by this revision
-func (rev *Revision) hyphaeAffected() (hyphae []string) {
- if nil != rev.hyphaeAffectedBuf {
- return rev.hyphaeAffectedBuf
- }
- hyphae = make([]string, 0)
- var (
- // set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
- set = make(map[string]bool)
- isNewName = func(hyphaName string) bool {
- if _, present := set[hyphaName]; present {
- return false
- }
- set[hyphaName] = true
- return true
+func (rev *Revision) findFilesAffected() ([]*object.File, error) {
+ changes, err := rev.changes()
+ if err != nil {
+ return nil, err
+ }
+ var files []*object.File
+ for _, change := range changes {
+ from, to, err := change.Files()
+ if err != nil {
+ continue
}
- filesAffected = rev.filesAffected()
- )
- for _, filename := range filesAffected {
- if strings.ContainsRune(filename, '.') {
- dotPos := strings.LastIndexByte(filename, '.')
- hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
- if isNewName(hyphaName) {
- hyphae = append(hyphae, hyphaName)
- }
+ if from != nil && to != nil && from.Name == to.Name {
+ to = nil
+ }
+ if from != nil {
+ files = append(files, from)
+ }
+ if to != nil {
+ files = append(files, to)
}
}
- rev.hyphaeAffectedBuf = hyphae
- return hyphae
+ return files, nil
}
-// TimeString returns a human readable time representation.
-func (rev Revision) TimeString() string {
- return rev.Time.Format(time.RFC822)
+// hyphaeAffected tells what hyphae were affected by this revision.
+func (rev *Revision) hyphaeAffected() ([]string, error) {
+ if rev.hyphaeAffectedVal == nil && rev.hyphaeAffectedErr == nil {
+ rev.hyphaeAffectedVal, rev.hyphaeAffectedErr = rev.findHyphaeAffected()
+ }
+ return rev.hyphaeAffectedVal, rev.hyphaeAffectedErr
}
-// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
-func (rev *Revision) textDiff() (diff string) {
- filenames, ok := rev.mycoFiles()
- if !ok {
- return "No text changes"
+func (rev *Revision) findHyphaeAffected() ([]string, error) {
+ files, err := rev.filesAffected()
+ if err != nil {
+ return nil, err
+ }
+
+ var hyphae []string
+ set := make(map[string]bool)
+ isPresent := func(hyphaName string) bool {
+ present := set[hyphaName]
+ set[hyphaName] = true
+ return present
}
- for _, filename := range filenames {
- text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
- if err != nil {
- diff += "\nAn error has occurred with " + filename + "\n"
+ for _, file := range files {
+ hyphaName, _, found := strings.Cut(file.Name, ".")
+ if found && !isPresent(hyphaName) {
+ hyphae = append(hyphae, hyphaName)
}
- diff += text + "\n"
}
- return diff
+ return hyphae, nil
}
-// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
-func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
- filenames = []string{}
- for _, filename := range rev.filesAffected() {
- if strings.HasSuffix(filename, ".myco") {
- filenames = append(filenames, filename)
+// textDiff generates a diff used in web feeds.
+func (rev *Revision) textDiff() string {
+ parent, err := rev.commit.Parent(0)
+ if errors.Is(err, object.ErrParentNotFound) {
+ return "Root commit"
+ } else if err != nil {
+ return "Failed to get the parent commit"
+ }
+ patch, err := rev.commit.Patch(parent)
+ if err != nil {
+ return "Failed to get a diff"
+ }
+ return patch.String()
+}
+
+// mycoFiles returns filenames of .myco file. It is not ok if there are no myco
+// files.
+func (rev *Revision) mycoFiles() ([]*object.File, bool) {
+ var files []*object.File
+ allFiles, err := rev.filesAffected()
+ if err != nil {
+ // XXX: error "casted" to bool without any context
+ return nil, false
+ }
+ for _, file := range allFiles {
+ if strings.HasSuffix(file.Name, ".myco") {
+ files = append(files, file)
}
}
- return filenames, len(filenames) > 0
+ return files, len(files) > 0
}
// Try and guess what link is the most important by looking at the message.
func (rev *Revision) bestLink() string {
- var (
- revs = rev.hyphaeAffected()
- renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
- )
- switch {
- case renameRes != nil:
- return "/hypha/" + renameRes[1]
- case len(revs) == 0:
+ hyphae, err := rev.hyphaeAffected()
+ if err != nil || len(hyphae) == 0 {
return ""
- default:
- return "/hypha/" + revs[0]
}
+ if r := renameMsgPattern.FindStringSubmatch(rev.Message); r != nil {
+ return "/hypha/" + r[1]
+ }
+ return "/hypha/" + hyphae[0]
}
-// FileAtRevision shows how the file with the given file path looked at the commit with the hash. It may return an error if git fails.
-func FileAtRevision(filepath, hash string) (string, error) {
- out, err := gitsh("show", hash+":"+strings.TrimPrefix(filepath, files.HyphaeDir()+"/"))
+func getDiffTrees(commit *object.Commit) (*object.Tree, *object.Tree, error) {
+ var oldTree *object.Tree
+ newTree, err := commit.Tree()
if err != nil {
- return "", err
+ return nil, nil, err
+ }
+ parent, err := commit.Parent(0)
+ if err == nil {
+ oldTree, err = parent.Tree()
}
- return out.String(), err
-}
-
-// PrimitiveDiffAtRevision generates a plain-text diff for the given filepath at the commit with the given hash. It may return an error if git fails.
-func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
- out, err := silentGitsh("diff", "--unified=1", "--no-color", hash+"~", hash, "--", filepath)
if err != nil {
- return "", err
+ treeCopy := *newTree
+ oldTree = &treeCopy
+ oldTree.Entries = nil
+ oldTree.Hash = plumbing.ZeroHash
}
- return out.String(), err
+ return oldTree, newTree, nil
}
diff --git a/history/revlog.go b/history/revlog.go
new file mode 100644
index 0000000..a339344
--- /dev/null
+++ b/history/revlog.go
@@ -0,0 +1,142 @@
+package history
+
+import (
+ "io"
+ "log"
+ "strings"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+
+ "github.com/bouncepaw/mycorrhiza/util"
+)
+
+// RevLogStream is an iterator over revisions.
+type RevLogStream struct {
+ commitIter object.CommitIter
+}
+
+// NewRevLogStream creates a revision log stream.
+func NewRevLogStream() RevLogStream {
+ var (
+ s RevLogStream
+ err error
+ )
+ if _, err := repo.Head(); err != nil {
+ return s
+ }
+ s.commitIter, err = repo.Log(&git.LogOptions{})
+ if err != nil {
+ panic(err)
+ }
+ return s
+}
+
+// Next returns the next revision from the stream, ordered most recent first.
+// If there are no revisions remaining, the err returned will be io.EOF.
+func (s RevLogStream) Next() (*Revision, error) {
+ if s.commitIter == nil {
+ return nil, io.EOF
+ }
+ commit, err := s.commitIter.Next()
+ if err != nil {
+ return nil, err
+ }
+ return parseRevision(commit), nil
+}
+
+// ForEach invokes a callback for every revision. If the callback returns an
+// error, ForEach stops and returns the error.
+func (s RevLogStream) ForEach(fn func(*Revision) error) error {
+ for {
+ rev, err := s.Next()
+ if rev == nil || err == io.EOF {
+ return nil
+ }
+ if err := fn(rev); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// RecentChanges gathers an arbitrary number of latest changes in form of
+// revisions slice, ordered most recent first.
+func RecentChanges(n int) []*Revision {
+ stream := NewRevLogStream()
+ var revs []*Revision
+ for len(revs) < n {
+ rev, err := stream.Next()
+ if rev == nil || err != nil {
+ break
+ }
+ revs = append(revs, rev)
+ }
+ log.Printf("Found %d recent changes", len(revs))
+ return revs
+}
+
+// Revisions returns revisions of the given hypha, ordered most recent first.
+func Revisions(hyphaName string) (revs []*Revision) {
+ fileName := hyphaName + ".myco"
+ ref, err := repo.Head()
+ if err != nil {
+ log.Printf("failed to resolve the HEAD reference: %v", err)
+ return
+ }
+ commit, err := repo.CommitObject(ref.Hash())
+ if err != nil {
+ log.Printf("failed to retrieve the HEAD commit: %v", err)
+ return
+ }
+ for commit != nil {
+ rev := parseRevision(commit)
+ parent, err := commit.Parent(0)
+ if err != nil {
+ commit = nil
+ } else {
+ commit = parent
+ }
+ changes, err := rev.changes()
+ if err != nil {
+ continue
+ }
+ for _, change := range changes {
+ if change.To.Name == fileName {
+ rev.HyphaName = strings.TrimSuffix(fileName, ".myco")
+ revs = append(revs, rev)
+ if change.From.Name != "" {
+ fileName = change.From.Name
+ }
+ }
+ }
+ }
+ return revs
+}
+
+// FileAtRevision shows how the file with the given file path looked at the
+// commit with the hash. It may return an error if git fails.
+func FileAtRevision(filepath, hash string) (string, error) {
+ filepath = util.ShorterPath(filepath)
+ commit, err := repo.CommitObject(plumbing.NewHash(hash))
+ if err != nil {
+ return "", err
+ }
+ file, err := commit.File(filepath)
+ if err != nil {
+ return "", err
+ }
+ bin, err := file.IsBinary()
+ if err != nil {
+ return "", err
+ }
+ if bin {
+ return "(binary file)", nil
+ }
+ contents, err := file.Contents()
+ if err != nil {
+ return "", err
+ }
+ return contents, nil
+}
diff --git a/history/view.qtpl b/history/view.qtpl
index 67e5cfd..dd9be10 100644
--- a/history/view.qtpl
+++ b/history/view.qtpl
@@ -3,8 +3,9 @@
HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
{% func (rev Revision) HyphaeLinksHTML() %}
+{% code hyphae, _ := rev.hyphaeAffected() %}
{% stripspace %}
- {% for i, hyphaName := range rev.hyphaeAffected() %}
+ {% for i, hyphaName := range hyphae %}
{% if i > 0 %}
<span aria-hidden="true">, </span>
{% endif %}
@@ -21,7 +22,7 @@ descriptionForFeed generates a good enough HTML contents for a web feed.
{% endfunc %}
WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
-{% func WithRevisions(hyphaName string, revs []Revision) %}
+{% func WithRevisions(hyphaName string, revs []*Revision) %}
{% for _, grp := range groupRevisionsByMonth(revs) %}
{% code
currentYear := grp[0].Time.Year()
@@ -42,14 +43,20 @@ WithRevisions returns an html representation of `revs` that is meant to be inser
{% endfunc %}
{% func (rev *Revision) asHistoryEntry(hyphaName string) %}
+{% code
+ changedHypha := rev.HyphaName
+ if changedHypha == "" {
+ changedHypha = hyphaName
+ }
+%}
<li class="history__entry">
- <a class="history-entry" href="/rev/{%s rev.Hash %}/{%s hyphaName %}">
+ <a class="history-entry" href="/rev/{%s rev.Hash %}/{%s changedHypha %}">
<time class="history-entry__time">{%s rev.timeToDisplay() %}</time>
</a>
- <span class="history-entry__hash"><a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s rev.Hash %}</a></span>
+ <span class="history-entry__hash"><a href="/primitive-diff/{%s rev.Hash %}/{%s changedHypha %}">{%s rev.ShortHash() %}</a></span>
<span class="history-entry__msg">{%s rev.Message %}</span>
{% if rev.Username != "anon" %}
<span class="history-entry__author">by <a href="/hypha/{%s cfg.UserHypha %}/{%s rev.Username %}" rel="author">{%s rev.Username %}</a></span>
{% endif %}
</li>
-{% endfunc %}
\ No newline at end of file
+{% endfunc %}
diff --git a/history/view.qtpl.go b/history/view.qtpl.go
index 64bc380..db9f804 100644
--- a/history/view.qtpl.go
+++ b/history/view.qtpl.go
@@ -30,297 +30,312 @@ func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) {
//line history/view.qtpl:5
qw422016.N().S(`
`)
-//line history/view.qtpl:7
- for i, hyphaName := range rev.hyphaeAffected() {
+//line history/view.qtpl:6
+ hyphae, _ := rev.hyphaeAffected()
+
+//line history/view.qtpl:6
+ qw422016.N().S(`
+`)
//line history/view.qtpl:8
+ for i, hyphaName := range hyphae {
+//line history/view.qtpl:9
if i > 0 {
-//line history/view.qtpl:8
+//line history/view.qtpl:9
qw422016.N().S(`<span aria-hidden="true">, </span>`)
-//line history/view.qtpl:10
+//line history/view.qtpl:11
}
-//line history/view.qtpl:10
- qw422016.N().S(`<a href="/hypha/`)
//line history/view.qtpl:11
+ qw422016.N().S(`<a href="/hypha/`)
+//line history/view.qtpl:12
qw422016.E().S(hyphaName)
-//line history/view.qtpl:11
+//line history/view.qtpl:12
qw422016.N().S(`">`)
-//line history/view.qtpl:11
+//line history/view.qtpl:12
qw422016.E().S(hyphaName)
-//line history/view.qtpl:11
- qw422016.N().S(`</a>`)
//line history/view.qtpl:12
- }
+ qw422016.N().S(`</a>`)
//line history/view.qtpl:13
+ }
+//line history/view.qtpl:14
qw422016.N().S(`
`)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
}
-//line history/view.qtpl:14
+//line history/view.qtpl:15
func (rev Revision) WriteHyphaeLinksHTML(qq422016 qtio422016.Writer) {
-//line history/view.qtpl:14
+//line history/view.qtpl:15
qw422016 := qt422016.AcquireWriter(qq422016)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
rev.StreamHyphaeLinksHTML(qw422016)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
qt422016.ReleaseWriter(qw422016)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
}
-//line history/view.qtpl:14
+//line history/view.qtpl:15
func (rev Revision) HyphaeLinksHTML() string {
-//line history/view.qtpl:14
+//line history/view.qtpl:15
qb422016 := qt422016.AcquireByteBuffer()
-//line history/view.qtpl:14
+//line history/view.qtpl:15
rev.WriteHyphaeLinksHTML(qb422016)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
qs422016 := string(qb422016.B)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
qt422016.ReleaseByteBuffer(qb422016)
-//line history/view.qtpl:14
+//line history/view.qtpl:15
return qs422016
-//line history/view.qtpl:14
+//line history/view.qtpl:15
}
// descriptionForFeed generates a good enough HTML contents for a web feed.
-//line history/view.qtpl:17
+//line history/view.qtpl:18
func (rev *Revision) streamdescriptionForFeed(qw422016 *qt422016.Writer) {
-//line history/view.qtpl:17
+//line history/view.qtpl:18
qw422016.N().S(`
<p><b>`)
-//line history/view.qtpl:18
+//line history/view.qtpl:19
qw422016.E().S(rev.Message)
-//line history/view.qtpl:18
+//line history/view.qtpl:19
qw422016.N().S(`</b> (by `)
-//line history/view.qtpl:18
+//line history/view.qtpl:19
qw422016.E().S(rev.Username)
-//line history/view.qtpl:18
+//line history/view.qtpl:19
qw422016.N().S(` at `)
-//line history/view.qtpl:18
+//line history/view.qtpl:19
qw422016.E().S(rev.TimeString())
-//line history/view.qtpl:18
+//line history/view.qtpl:19
qw422016.N().S(`)</p>
<p>Hyphae affected: `)
-//line history/view.qtpl:19
+//line history/view.qtpl:20
rev.StreamHyphaeLinksHTML(qw422016)
-//line history/view.qtpl:19
+//line history/view.qtpl:20
qw422016.N().S(`</p>
<pre><code>`)
-//line history/view.qtpl:20
+//line history/view.qtpl:21
qw422016.E().S(rev.textDiff())
-//line history/view.qtpl:20
+//line history/view.qtpl:21
qw422016.N().S(`</code></pre>
`)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
}
-//line history/view.qtpl:21
+//line history/view.qtpl:22
func (rev *Revision) writedescriptionForFeed(qq422016 qtio422016.Writer) {
-//line history/view.qtpl:21
+//line history/view.qtpl:22
qw422016 := qt422016.AcquireWriter(qq422016)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
rev.streamdescriptionForFeed(qw422016)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
qt422016.ReleaseWriter(qw422016)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
}
-//line history/view.qtpl:21
+//line history/view.qtpl:22
func (rev *Revision) descriptionForFeed() string {
-//line history/view.qtpl:21
+//line history/view.qtpl:22
qb422016 := qt422016.AcquireByteBuffer()
-//line history/view.qtpl:21
+//line history/view.qtpl:22
rev.writedescriptionForFeed(qb422016)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
qs422016 := string(qb422016.B)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
qt422016.ReleaseByteBuffer(qb422016)
-//line history/view.qtpl:21
+//line history/view.qtpl:22
return qs422016
-//line history/view.qtpl:21
+//line history/view.qtpl:22
}
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
-//line history/view.qtpl:24
-func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []Revision) {
-//line history/view.qtpl:24
+//line history/view.qtpl:25
+func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []*Revision) {
+//line history/view.qtpl:25
qw422016.N().S(`
`)
-//line history/view.qtpl:25
+//line history/view.qtpl:26
for _, grp := range groupRevisionsByMonth(revs) {
-//line history/view.qtpl:25
+//line history/view.qtpl:26
qw422016.N().S(`
`)
-//line history/view.qtpl:27
+//line history/view.qtpl:28
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
-//line history/view.qtpl:30
+//line history/view.qtpl:31
qw422016.N().S(`
<section class="history__month">
<a href="#`)
-//line history/view.qtpl:32
+//line history/view.qtpl:33
qw422016.E().S(sectionId)
-//line history/view.qtpl:32
+//line history/view.qtpl:33
qw422016.N().S(`" class="history__month-anchor">
<h2 id="`)
-//line history/view.qtpl:33
+//line history/view.qtpl:34
qw422016.E().S(sectionId)
-//line history/view.qtpl:33
+//line history/view.qtpl:34
qw422016.N().S(`" class="history__month-title">`)
-//line history/view.qtpl:33
+//line history/view.qtpl:34
qw422016.N().D(currentYear)
-//line history/view.qtpl:33
+//line history/view.qtpl:34
qw422016.N().S(` `)
-//line history/view.qtpl:33
+//line history/view.qtpl:34
qw422016.E().S(currentMonth.String())
-//line history/view.qtpl:33
+//line history/view.qtpl:34
qw422016.N().S(`</h2>
</a>
<ul class="history__entries">
`)
-//line history/view.qtpl:36
+//line history/view.qtpl:37
for _, rev := range grp {
-//line history/view.qtpl:36
+//line history/view.qtpl:37
qw422016.N().S(`
`)
-//line history/view.qtpl:37
+//line history/view.qtpl:38
rev.streamasHistoryEntry(qw422016, hyphaName)
-//line history/view.qtpl:37
+//line history/view.qtpl:38
qw422016.N().S(`
`)
-//line history/view.qtpl:38
+//line history/view.qtpl:39
}
-//line history/view.qtpl:38
+//line history/view.qtpl:39
qw422016.N().S(`
</ul>
</section>
`)
-//line history/view.qtpl:41
+//line history/view.qtpl:42
}
-//line history/view.qtpl:41
+//line history/view.qtpl:42
qw422016.N().S(`
`)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
}
-//line history/view.qtpl:42
-func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []Revision) {
-//line history/view.qtpl:42
+//line history/view.qtpl:43
+func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []*Revision) {
+//line history/view.qtpl:43
qw422016 := qt422016.AcquireWriter(qq422016)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
StreamWithRevisions(qw422016, hyphaName, revs)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
qt422016.ReleaseWriter(qw422016)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
}
-//line history/view.qtpl:42
-func WithRevisions(hyphaName string, revs []Revision) string {
-//line history/view.qtpl:42
+//line history/view.qtpl:43
+func WithRevisions(hyphaName string, revs []*Revision) string {
+//line history/view.qtpl:43
qb422016 := qt422016.AcquireByteBuffer()
-//line history/view.qtpl:42
+//line history/view.qtpl:43
WriteWithRevisions(qb422016, hyphaName, revs)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
qs422016 := string(qb422016.B)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
qt422016.ReleaseByteBuffer(qb422016)
-//line history/view.qtpl:42
+//line history/view.qtpl:43
return qs422016
-//line history/view.qtpl:42
+//line history/view.qtpl:43
}
-//line history/view.qtpl:44
+//line history/view.qtpl:45
func (rev *Revision) streamasHistoryEntry(qw422016 *qt422016.Writer, hyphaName string) {
-//line history/view.qtpl:44
+//line history/view.qtpl:45
+ qw422016.N().S(`
+`)
+//line history/view.qtpl:47
+ changedHypha := rev.HyphaName
+ if changedHypha == "" {
+ changedHypha = hyphaName
+ }
+
+//line history/view.qtpl:51
qw422016.N().S(`
<li class="history__entry">
<a class="history-entry" href="/rev/`)
-//line history/view.qtpl:46
+//line history/view.qtpl:53
qw422016.E().S(rev.Hash)
-//line history/view.qtpl:46
+//line history/view.qtpl:53
qw422016.N().S(`/`)
-//line history/view.qtpl:46
- qw422016.E().S(hyphaName)
-//line history/view.qtpl:46
+//line history/view.qtpl:53
+ qw422016.E().S(changedHypha)
+//line history/view.qtpl:53
qw422016.N().S(`">
<time class="history-entry__time">`)
-//line history/view.qtpl:47
+//line history/view.qtpl:54
qw422016.E().S(rev.timeToDisplay())
-//line history/view.qtpl:47
+//line history/view.qtpl:54
qw422016.N().S(`</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/`)
-//line history/view.qtpl:49
+//line history/view.qtpl:56
qw422016.E().S(rev.Hash)
-//line history/view.qtpl:49
+//line history/view.qtpl:56
qw422016.N().S(`/`)
-//line history/view.qtpl:49
- qw422016.E().S(hyphaName)
-//line history/view.qtpl:49
+//line history/view.qtpl:56
+ qw422016.E().S(changedHypha)
+//line history/view.qtpl:56
qw422016.N().S(`">`)
-//line history/view.qtpl:49
- qw422016.E().S(rev.Hash)
-//line history/view.qtpl:49
+//line history/view.qtpl:56
+ qw422016.E().S(rev.ShortHash())
+//line history/view.qtpl:56
qw422016.N().S(`</a></span>
<span class="history-entry__msg">`)
-//line history/view.qtpl:50
+//line history/view.qtpl:57
qw422016.E().S(rev.Message)
-//line history/view.qtpl:50
+//line history/view.qtpl:57
qw422016.N().S(`</span>
`)
-//line history/view.qtpl:51
+//line history/view.qtpl:58
if rev.Username != "anon" {
-//line history/view.qtpl:51
+//line history/view.qtpl:58
qw422016.N().S(`
<span class="history-entry__author">by <a href="/hypha/`)
-//line history/view.qtpl:52
+//line history/view.qtpl:59
qw422016.E().S(cfg.UserHypha)
-//line history/view.qtpl:52
+//line history/view.qtpl:59
qw422016.N().S(`/`)
-//line history/view.qtpl:52
+//line history/view.qtpl:59
qw422016.E().S(rev.Username)
-//line history/view.qtpl:52
+//line history/view.qtpl:59
qw422016.N().S(`" rel="author">`)
-//line history/view.qtpl:52
+//line history/view.qtpl:59
qw422016.E().S(rev.Username)
-//line history/view.qtpl:52
+//line history/view.qtpl:59
qw422016.N().S(`</a></span>
`)
-//line history/view.qtpl:53
+//line history/view.qtpl:60
}
-//line history/view.qtpl:53
+//line history/view.qtpl:60
qw422016.N().S(`
</li>
`)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
}
-//line history/view.qtpl:55
+//line history/view.qtpl:62
func (rev *Revision) writeasHistoryEntry(qq422016 qtio422016.Writer, hyphaName string) {
-//line history/view.qtpl:55
+//line history/view.qtpl:62
qw422016 := qt422016.AcquireWriter(qq422016)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
rev.streamasHistoryEntry(qw422016, hyphaName)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
qt422016.ReleaseWriter(qw422016)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
}
-//line history/view.qtpl:55
+//line history/view.qtpl:62
func (rev *Revision) asHistoryEntry(hyphaName string) string {
-//line history/view.qtpl:55
+//line history/view.qtpl:62
qb422016 := qt422016.AcquireByteBuffer()
-//line history/view.qtpl:55
+//line history/view.qtpl:62
rev.writeasHistoryEntry(qb422016, hyphaName)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
qs422016 := string(qb422016.B)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
qt422016.ReleaseByteBuffer(qb422016)
-//line history/view.qtpl:55
+//line history/view.qtpl:62
return qs422016
-//line history/view.qtpl:55
+//line history/view.qtpl:62
}
diff --git a/l18n/l18n.go b/l18n/l18n.go
index b557de0..d352d61 100644
--- a/l18n/l18n.go
+++ b/l18n/l18n.go
@@ -40,6 +40,7 @@ type Localizer struct {
}
// locales is a filesystem containing all localization files.
+//
//go:embed en ru
var locales embed.FS
diff --git a/main.go b/main.go
index ad764f1..65587d1 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,10 @@
+// Command mycorrhiza is a program that runs a mycorrhiza wiki.
+//
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=tree
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=history
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=mycoopts
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=auth
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=hypview
-// Command mycorrhiza is a program that runs a mycorrhiza wiki.
package main
import (
@@ -48,8 +49,7 @@ func main() {
backlinks.IndexBacklinks()
go backlinks.RunBacklinksConveyor()
user.InitUserDatabase()
- history.Start()
- history.InitGitRepo()
+ history.Init()
migration.MigrateRocketsMaybe()
migration.MigrateHeadingsMaybe()
shroom.SetHeaderLinks()
diff --git a/migration/migration.go b/migration/migration.go
index 7f4a683..00bf1ff 100644
--- a/migration/migration.go
+++ b/migration/migration.go
@@ -3,8 +3,8 @@
// Migrations are meant to be removed couple of versions after being introduced.
//
// Available migrations:
-// * Rocket links
-// * Headings
+// - Rocket links
+// - Headings
package migration
import (
diff --git a/shroom/upload.go b/shroom/upload.go
index 5599b01..a9127c6 100644
--- a/shroom/upload.go
+++ b/shroom/upload.go
@@ -4,17 +4,17 @@ import (
"bytes"
"errors"
"fmt"
+ "io"
+ "mime/multipart"
+ "os"
+ "path/filepath"
+
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/files"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/mimetype"
"github.com/bouncepaw/mycorrhiza/user"
- "io"
- "log"
- "mime/multipart"
- "os"
- "path/filepath"
)
func historyMessageForTextUpload(h hyphae.Hypha, userMessage string) string {
@@ -183,6 +183,7 @@ func UploadBinary(h hyphae.Hypha, mime string, file multipart.File, u *user.User
if err != nil {
return err
}
+ filesChanged := []string{uploadedFilePath}
switch h := h.(type) {
case *hyphae.EmptyHypha:
@@ -193,10 +194,10 @@ func UploadBinary(h hyphae.Hypha, mime string, file multipart.File, u *user.User
case *hyphae.MediaHypha: // If this is not the first media the hypha gets
prevFilePath := h.MediaFilePath()
if prevFilePath != uploadedFilePath {
- if err := history.Rename(prevFilePath, uploadedFilePath); err != nil {
+ if err := os.Remove(prevFilePath); err != nil {
return err
}
- log.Printf("Move ā%sā to ā%sā\n", prevFilePath, uploadedFilePath)
+ filesChanged = append(filesChanged, prevFilePath)
h.SetMediaFilePath(uploadedFilePath)
}
}
diff --git a/util/util.go b/util/util.go
index 74d641f..2fb27b2 100644
--- a/util/util.go
+++ b/util/util.go
@@ -3,13 +3,14 @@ package util
import (
"crypto/rand"
"encoding/hex"
- "github.com/bouncepaw/mycorrhiza/files"
"log"
"net/http"
"strings"
"github.com/bouncepaw/mycomarkup/v5/util"
+
"github.com/bouncepaw/mycorrhiza/cfg"
+ "github.com/bouncepaw/mycorrhiza/files"
)
// PrepareRq strips the trailing / in rq.URL.Path. In the future it might do more stuff for making all request structs uniform.
--
2.32.1 (Apple Git-133)