Add go-git reader for trees, blobs, log and diffs
DDavid committed on 2026-06-28 01:20 commit b752eb9
go.mod +26 −0
module gitgud
go 1.25.0
require (
github.com/go-chi/chi v1.5.5
github.com/joho/godotenv v1.5.1
)
require (
github.com/mattn/go-sqlite3 v1.14.47
golang.org/x/crypto v0.53.0
)
require (
github.com/a-h/templ v0.3.1020
+ github.com/alecthomas/chroma/v2 v2.27.0
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de
github.com/alexedwards/scs/v2 v2.9.0
+ github.com/go-git/go-git/v5 v5.19.1
+ github.com/yuin/goldmark v1.8.2
+)
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.1.6 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
+ github.com/dlclark/regexp2/v2 v2.2.1 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.9.0 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/pjbgf/sha1cd v0.6.0 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+ github.com/skeema/knownhosts v1.3.1 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ golang.org/x/net v0.55.0 // indirect
+ golang.org/x/sys v0.46.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
)
go.sum +114 −2
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
+github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
+github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
+github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
+github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de h1:c72K9HLu6K442et0j3BUL/9HEYaUJouLkkVANdmqTOo=
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+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/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
+github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
+github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
+github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
+github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
+github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
+github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
+github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
+github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
+github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
+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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
+github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
+github.com/stretchr/objx v0.1.0/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+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-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.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
internal/infra/git/cli_git.go +5 −1
package git
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
type CLIGit struct {
reposDir string
}
func NewCLIGit(reposDir string) CLIGit {
return CLIGit{reposDir: reposDir}
}
func (g CLIGit) InitBare(ctx context.Context, owner, name, defaultBranch string) error {
path, err := g.repoPath(owner, name)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
cmd := exec.CommandContext(ctx, "git", "init", "--bare",
"--initial-branch="+defaultBranch, path)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git init: %v: %s", err, out)
}
cmd = exec.CommandContext(ctx, "git", "-C", path, "config", "http.receivepack", "true")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git config http.receivepack: %v: %s", err, out)
}
return nil
}
func (g CLIGit) RemovePath(ctx context.Context, owner, name string) error {
path, err := g.repoPath(owner, name)
if err != nil {
return err
}
return os.RemoveAll(path)
}
func (g CLIGit) repoPath(owner, name string) (string, error) {
+ return repoPath(g.reposDir, owner, name)
+}
+
+func repoPath(reposDir, owner, name string) (string, error) {
if err := safeSegment(owner); err != nil {
return "", err
}
if err := safeSegment(name); err != nil {
return "", err
}
- return filepath.Join(g.reposDir, owner, name+".git"), nil
+ return filepath.Join(reposDir, owner, name+".git"), nil
}
func safeSegment(s string) error {
if s == "" || s == "." || s == ".." ||
strings.HasPrefix(s, ".") || strings.ContainsAny(s, `/\`) {
return fmt.Errorf("invalid path segment %q", s)
}
return nil
}
internal/infra/git/gogit_reader.go +327 −0
+package git
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+ "path"
+ "sort"
+ "strings"
+
+ gogit "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/filemode"
+ "github.com/go-git/go-git/v5/plumbing/object"
+
+ "gitgud/internal/domain"
+)
+
+type GoGitReader struct {
+ reposDir string
+}
+
+func NewGoGitReader(reposDir string) *GoGitReader {
+ return &GoGitReader{reposDir: reposDir}
+}
+
+func (g *GoGitReader) open(owner, name string) (*gogit.Repository, error) {
+ p, err := repoPath(g.reposDir, owner, name)
+ if err != nil {
+ return nil, err
+ }
+ repo, err := gogit.PlainOpen(p)
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ return repo, nil
+}
+
+func (g *GoGitReader) resolveCommit(repo *gogit.Repository, ref string) (*object.Commit, error) {
+ if strings.TrimSpace(ref) == "" {
+ ref = "HEAD"
+ }
+ hash, err := repo.ResolveRevision(plumbing.Revision(ref))
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ c, err := repo.CommitObject(*hash)
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ return c, nil
+}
+
+func (g *GoGitReader) IsEmpty(ctx context.Context, owner, name string) (bool, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return false, err
+ }
+ _, err = repo.Head()
+ if errors.Is(err, plumbing.ErrReferenceNotFound) {
+ return true, nil
+ }
+ if err != nil {
+ return false, mapErr(err)
+ }
+ return false, nil
+}
+
+func (g *GoGitReader) Branches(ctx context.Context, owner, name string) ([]string, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return nil, err
+ }
+ iter, err := repo.Branches()
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ var names []string
+ err = iter.ForEach(func(ref *plumbing.Reference) error {
+ names = append(names, ref.Name().Short())
+ return nil
+ })
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ sort.Strings(names)
+ return names, nil
+}
+
+func (g *GoGitReader) Tip(ctx context.Context, owner, name, ref string) (*domain.Commit, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return nil, err
+ }
+ c, err := g.resolveCommit(repo, ref)
+ if err != nil {
+ return nil, err
+ }
+ return commitDTO(c), nil
+}
+
+func (g *GoGitReader) Tree(ctx context.Context, owner, name, ref, p string) ([]domain.TreeEntry, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return nil, err
+ }
+ c, err := g.resolveCommit(repo, ref)
+ if err != nil {
+ return nil, err
+ }
+ tree, err := c.Tree()
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ p = strings.Trim(p, "/")
+ if p != "" {
+ tree, err = tree.Tree(p)
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ }
+
+ entries := make([]domain.TreeEntry, 0, len(tree.Entries))
+ for _, e := range tree.Entries {
+ isDir := e.Mode == filemode.Dir
+ var size int64
+ if !isDir {
+ if f, err := tree.TreeEntryFile(&e); err == nil {
+ size = f.Size
+ }
+ }
+ entries = append(entries, domain.TreeEntry{
+ Name: e.Name,
+ Path: path.Join(p, e.Name),
+ IsDir: isDir,
+ Size: size,
+ Mode: e.Mode.String(),
+ })
+ }
+
+ sort.Slice(entries, func(i, j int) bool {
+ if entries[i].IsDir != entries[j].IsDir {
+ return entries[i].IsDir
+ }
+ return entries[i].Name < entries[j].Name
+ })
+ return entries, nil
+}
+
+func (g *GoGitReader) Blob(ctx context.Context, owner, name, ref, p string) (*domain.FileBlob, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return nil, err
+ }
+ c, err := g.resolveCommit(repo, ref)
+ if err != nil {
+ return nil, err
+ }
+ tree, err := c.Tree()
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ f, err := tree.File(strings.Trim(p, "/"))
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ reader, err := f.Reader()
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ defer reader.Close()
+ content, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ return &domain.FileBlob{
+ Path: strings.Trim(p, "/"),
+ Content: content,
+ IsBinary: isBinary(content),
+ Size: f.Size,
+ }, nil
+}
+
+func (g *GoGitReader) Log(ctx context.Context, owner, name, ref string, limit, offset int) ([]domain.Commit, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return nil, err
+ }
+ c, err := g.resolveCommit(repo, ref)
+ if err != nil {
+ return nil, err
+ }
+ iter, err := repo.Log(&gogit.LogOptions{From: c.Hash})
+ if err != nil {
+ return nil, mapErr(err)
+ }
+ defer iter.Close()
+
+ var commits []domain.Commit
+ skipped := 0
+ err = iter.ForEach(func(commit *object.Commit) error {
+ if skipped < offset {
+ skipped++
+ return nil
+ }
+ if limit > 0 && len(commits) >= limit {
+ return storerStop
+ }
+ commits = append(commits, *commitDTO(commit))
+ return nil
+ })
+ if err != nil && !errors.Is(err, storerStop) {
+ return nil, mapErr(err)
+ }
+ return commits, nil
+}
+
+func (g *GoGitReader) CommitDiff(ctx context.Context, owner, name, hash string) (*domain.Commit, []domain.FileDiff, error) {
+ repo, err := g.open(owner, name)
+ if err != nil {
+ return nil, nil, err
+ }
+ c, err := repo.CommitObject(plumbing.NewHash(hash))
+ if err != nil {
+ return nil, nil, mapErr(err)
+ }
+
+ thisTree, err := c.Tree()
+ if err != nil {
+ return nil, nil, mapErr(err)
+ }
+
+ var parentTree *object.Tree
+ if parent, err := c.Parents().Next(); err == nil {
+ parentTree, err = parent.Tree()
+ if err != nil {
+ return nil, nil, mapErr(err)
+ }
+ } else if !errors.Is(err, io.EOF) {
+ return nil, nil, mapErr(err)
+ }
+
+ changes, err := object.DiffTree(parentTree, thisTree)
+ if err != nil {
+ return nil, nil, mapErr(err)
+ }
+ patch, err := changes.Patch()
+ if err != nil {
+ return nil, nil, mapErr(err)
+ }
+
+ var diffs []domain.FileDiff
+ for _, fp := range patch.FilePatches() {
+ from, to := fp.Files()
+ p := ""
+ if to != nil {
+ p = to.Path()
+ } else if from != nil {
+ p = from.Path()
+ }
+
+ var sb strings.Builder
+ added, deleted := 0, 0
+ for _, chunk := range fp.Chunks() {
+ lines := strings.SplitAfter(chunk.Content(), "\n")
+ for _, line := range lines {
+ if line == "" {
+ continue
+ }
+ switch chunk.Type() {
+ case 1: // Add
+ sb.WriteString("+" + line)
+ added++
+ case 2: // Delete
+ sb.WriteString("-" + line)
+ deleted++
+ default: // Equal
+ sb.WriteString(" " + line)
+ }
+ }
+ }
+ diffs = append(diffs, domain.FileDiff{
+ Path: p,
+ Patch: sb.String(),
+ Added: added,
+ Deleted: deleted,
+ })
+ }
+
+ return commitDTO(c), diffs, nil
+}
+
+var storerStop = errors.New("stop")
+
+func commitDTO(c *object.Commit) *domain.Commit {
+ return &domain.Commit{
+ Hash: c.Hash.String(),
+ ShortHash: c.Hash.String()[:7],
+ Message: c.Message,
+ Author: c.Author.Name,
+ Email: c.Author.Email,
+ When: c.Author.When,
+ }
+}
+
+func isBinary(content []byte) bool {
+ n := len(content)
+ if n > 8000 {
+ n = 8000
+ }
+ return bytes.IndexByte(content[:n], 0) >= 0
+}
+
+func mapErr(err error) error {
+ switch {
+ case errors.Is(err, gogit.ErrRepositoryNotExists),
+ errors.Is(err, plumbing.ErrReferenceNotFound),
+ errors.Is(err, plumbing.ErrObjectNotFound),
+ errors.Is(err, object.ErrFileNotFound),
+ errors.Is(err, object.ErrDirectoryNotFound),
+ errors.Is(err, object.ErrEntryNotFound):
+ return domain.ErrNotFound
+ default:
+ return err
+ }
+}