10 files changed,
748 insertions(+),
6 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-01-21 01:49:14 +0200
Authored at:
2026-01-18 19:45:08 +0200
Change ID:
qpvvmyqqypuryxsyqlzzrnlpolunqqwq
Parent:
b407568
M
go.mod
路路路 2 2 3 3 go 1.25.3 4 4 5 -require gopkg.in/yaml.v2 v2.4.0 5 +require ( 6 + github.com/bluekeyes/go-gitdiff v0.8.1 7 + github.com/go-git/go-git/v5 v5.16.4 8 + gopkg.in/yaml.v2 v2.4.0 9 +) 10 + 11 +require ( 12 + dario.cat/mergo v1.0.0 // indirect 13 + github.com/Microsoft/go-winio v0.6.2 // indirect 14 + github.com/ProtonMail/go-crypto v1.1.6 // indirect 15 + github.com/cloudflare/circl v1.6.1 // indirect 16 + github.com/cyphar/filepath-securejoin v0.4.1 // indirect 17 + github.com/emirpasic/gods v1.18.1 // indirect 18 + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 19 + github.com/go-git/go-billy/v5 v5.6.2 // indirect 20 + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 21 + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 22 + github.com/kevinburke/ssh_config v1.2.0 // indirect 23 + github.com/pjbgf/sha1cd v0.3.2 // indirect 24 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 25 + github.com/skeema/knownhosts v1.3.1 // indirect 26 + github.com/xanzy/ssh-agent v0.3.3 // indirect 27 + golang.org/x/crypto v0.37.0 // indirect 28 + golang.org/x/net v0.39.0 // indirect 29 + golang.org/x/sys v0.32.0 // indirect 30 + gopkg.in/warnings.v0 v0.1.2 // indirect 31 +)
M
go.sum
路路路 1 -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 1 +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 6 +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= 7 +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 8 +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 9 +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 10 +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 +github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= 13 +github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= 14 +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 15 +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 16 +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 17 +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 18 +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 22 +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 23 +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 24 +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 25 +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 26 +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 27 +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 28 +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 29 +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 30 +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 31 +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 32 +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 33 +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= 34 +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= 35 +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 36 +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 37 +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 38 +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 39 +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 40 +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 41 +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 42 +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 43 +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 44 +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 45 +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 46 +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 51 +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 52 +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 53 +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 54 +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 55 +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 56 +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 59 +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 60 +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 61 +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 62 +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 63 +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 64 +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 65 +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 67 +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 68 +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 69 +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 70 +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 71 +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 72 +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 73 +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 74 +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 75 +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 76 +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 77 +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 78 +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 79 +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 80 +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 87 +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 88 +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 89 +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 90 +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 91 +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 92 +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 93 +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 94 +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 2 95 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 97 +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 98 +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 99 +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 100 +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 101 +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 3 102 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 4 103 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 104 +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 105 +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A
internal/git/diff.go
路路路 1 +package git 2 + 3 +import ( 4 + "fmt" 5 + "strings" 6 + 7 + "github.com/bluekeyes/go-gitdiff/gitdiff" 8 + "github.com/go-git/go-git/v5/plumbing/object" 9 +) 10 + 11 +type TextFragment struct { 12 + Header string 13 + Lines []gitdiff.Line 14 +} 15 + 16 +type Diff struct { 17 + Name struct { 18 + Old string 19 + New string 20 + } 21 + TextFragments []TextFragment 22 + IsBinary bool 23 + IsNew bool 24 + IsDelete bool 25 +} 26 + 27 +type NiceDiff struct { 28 + Diff []Diff 29 + Commit struct { 30 + Message string 31 + Author object.Signature 32 + This string 33 + Parent string 34 + } 35 + Stat struct { 36 + FilesChanged int 37 + Insertions int 38 + Deletions int 39 + } 40 +} 41 + 42 +func (g *Repo) Diff() (*NiceDiff, error) { 43 + c, err := g.r.CommitObject(g.h) 44 + if err != nil { 45 + return nil, fmt.Errorf("commit object: %w", err) 46 + } 47 + 48 + patch, parent, err := g.getPatch(c) 49 + if err != nil { 50 + return nil, err 51 + } 52 + 53 + diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String())) 54 + if err != nil { 55 + return nil, fmt.Errorf("parsing diff: %w", err) 56 + } 57 + 58 + nd := NiceDiff{} 59 + nd.Commit.Message = c.Message 60 + nd.Commit.Author = c.Author 61 + nd.Commit.This = c.Hash.String() 62 + nd.Commit.Parent = getParentHash(parent) 63 + nd.Stat.FilesChanged = len(diffs) 64 + 65 + nd.Diff = make([]Diff, len(diffs)) 66 + for i, d := range diffs { 67 + diff := &nd.Diff[i] 68 + diff.Name.New = d.NewName 69 + diff.Name.Old = d.OldName 70 + diff.IsBinary = d.IsBinary 71 + diff.IsNew = d.IsNew 72 + diff.IsDelete = d.IsDelete 73 + 74 + for _, tf := range d.TextFragments { 75 + diff.TextFragments = append(diff.TextFragments, TextFragment{ 76 + Header: tf.Header(), 77 + Lines: tf.Lines, 78 + }) 79 + for _, l := range tf.Lines { 80 + switch l.Op { 81 + case gitdiff.OpAdd: 82 + nd.Stat.Insertions += 1 83 + case gitdiff.OpDelete: 84 + nd.Stat.Deletions += 1 85 + } 86 + } 87 + } 88 + } 89 + return &nd, nil 90 +} 91 + 92 +func (g *Repo) getPatch(c *object.Commit) (*object.Patch, *object.Commit, error) { 93 + commitTree, err := c.Tree() 94 + if err != nil { 95 + return nil, nil, err 96 + } 97 + 98 + var parentTree *object.Tree 99 + var parent *object.Commit 100 + 101 + if c.NumParents() != 0 { 102 + parent, err = c.Parents().Next() 103 + if err != nil { 104 + return nil, nil, err 105 + } 106 + parentTree, err = parent.Tree() 107 + if err != nil { 108 + return nil, nil, err 109 + } 110 + } else { 111 + parentTree = &object.Tree{} 112 + } 113 + 114 + patch, err := parentTree.Patch(commitTree) 115 + if err != nil { 116 + return nil, nil, fmt.Errorf("patch: %w", err) 117 + } 118 + 119 + return patch, parent, nil 120 +} 121 + 122 +func getParentHash(parent *object.Commit) string { 123 + if parent == nil || parent.Hash.IsZero() { 124 + return "" 125 + } 126 + return parent.Hash.String() 127 +}
A
internal/git/gitservice/gitservice.go
路路路 1 +package gitservice 2 + 3 +import ( 4 + "bytes" 5 + "fmt" 6 + "io" 7 + "os/exec" 8 + "strings" 9 + "syscall" 10 +) 11 + 12 +// Thanks https://git.icyphox.sh/legit/blob/master/git/service/service.go 13 + 14 +// InfoRefs executes git-upload-pack --advertise-refs for smart-HTTP discovery. 15 +func InfoRefs(dir string, out io.Writer) error { 16 + cmd := exec.Command("git", 17 + "upload-pack", 18 + "--stateless-rpc", 19 + "--advertise-refs", 20 + ".") 21 + cmd.Dir = dir 22 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 23 + stdoutPipe, _ := cmd.StdoutPipe() 24 + cmd.Stderr = cmd.Stdout 25 + defer stdoutPipe.Close() 26 + 27 + if err := cmd.Start(); err != nil { 28 + return fmt.Errorf("start git-upload-pack: %w", err) 29 + } 30 + 31 + if err := packLine(out, "# service=git-upload-pack\n"); err != nil { 32 + return fmt.Errorf("write pack line: %w", err) 33 + } 34 + 35 + if err := packFlush(out); err != nil { 36 + return fmt.Errorf("flush pack: %w", err) 37 + } 38 + 39 + var buf bytes.Buffer 40 + if _, err := io.Copy(&buf, stdoutPipe); err != nil { 41 + return fmt.Errorf("copy stdout to buffer: %w", err) 42 + } 43 + 44 + if err := cmd.Wait(); err != nil { 45 + var out strings.Builder 46 + io.Copy(&out, &buf) 47 + return fmt.Errorf("git-upload-pack: %w: %s", err, out.String()) 48 + } 49 + 50 + if _, err := io.Copy(out, &buf); err != nil { 51 + return fmt.Errorf("copy buffer to output: %w", err) 52 + } 53 + 54 + return nil 55 +} 56 + 57 +// UploadPack executes git-upload-pack for smart-HTTP git fetch/clone. 58 +func UploadPack(dir string, in io.Reader, out io.Writer) error { 59 + cmd := exec.Command("git", 60 + "-c", "uploadpack.allowFilter=true", 61 + "upload-pack", 62 + "--stateless-rpc", 63 + ".") 64 + cmd.Dir = dir 65 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 66 + 67 + stdoutPipe, _ := cmd.StdoutPipe() 68 + cmd.Stderr = cmd.Stdout 69 + defer stdoutPipe.Close() 70 + 71 + stdinPipe, err := cmd.StdinPipe() 72 + if err != nil { 73 + return fmt.Errorf("stdin pipe: %w", err) 74 + } 75 + defer stdinPipe.Close() 76 + 77 + if err := cmd.Start(); err != nil { 78 + return fmt.Errorf("start git-upload-pack: %w", err) 79 + } 80 + 81 + if _, err := io.Copy(stdinPipe, in); err != nil { 82 + return fmt.Errorf("copy to stdin: %w", err) 83 + } 84 + stdinPipe.Close() 85 + 86 + if _, err := io.Copy(out, stdoutPipe); err != nil { 87 + return fmt.Errorf("copy stdout: %w", err) 88 + } 89 + 90 + if err := cmd.Wait(); err != nil { 91 + return fmt.Errorf("git-upload-pack: %w", err) 92 + } 93 + 94 + return nil 95 +} 96 + 97 +func packLine(w io.Writer, s string) error { 98 + _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) 99 + return err 100 +} 101 + 102 +func packFlush(w io.Writer) error { 103 + _, err := fmt.Fprint(w, "0000") 104 + return err 105 +}
A
internal/git/repo.go
路路路 1 +package git 2 + 3 +import ( 4 + "fmt" 5 + "sort" 6 + 7 + "github.com/go-git/go-git/v5" 8 + "github.com/go-git/go-git/v5/plumbing" 9 + "github.com/go-git/go-git/v5/plumbing/object" 10 +) 11 + 12 +// Thanks https://git.icyphox.sh/legit/blob/master/git/git.go 13 + 14 +type Repo struct { 15 + path string 16 + r *git.Repository 17 + h plumbing.Hash 18 +} 19 + 20 +// Open opens a git repository at path. If ref is empty, HEAD is used. 21 +func Open(path string, ref string) (*Repo, error) { 22 + var err error 23 + g := Repo{} 24 + g.path = path 25 + g.r, err = git.PlainOpen(path) 26 + if err != nil { 27 + return nil, fmt.Errorf("opening %s: %w", path, err) 28 + } 29 + 30 + if ref == "" { 31 + head, err := g.r.Head() 32 + if err != nil { 33 + return nil, fmt.Errorf("getting head of %s: %w", path, err) 34 + } 35 + g.h = head.Hash() 36 + } else { 37 + hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 38 + if err != nil { 39 + return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) 40 + } 41 + g.h = *hash 42 + } 43 + return &g, nil 44 +} 45 + 46 +func (g *Repo) Commits() ([]*object.Commit, error) { 47 + ci, err := g.r.Log(&git.LogOptions{From: g.h}) 48 + if err != nil { 49 + return nil, fmt.Errorf("commits from ref: %w", err) 50 + } 51 + 52 + commits := []*object.Commit{} 53 + ci.ForEach(func(c *object.Commit) error { 54 + commits = append(commits, c) 55 + return nil 56 + }) 57 + 58 + return commits, nil 59 +} 60 + 61 +func (g *Repo) LastCommit() (*object.Commit, error) { 62 + c, err := g.r.CommitObject(g.h) 63 + if err != nil { 64 + return nil, fmt.Errorf("last commit: %w", err) 65 + } 66 + return c, nil 67 +} 68 + 69 +func (g *Repo) FileContent(path string) (string, error) { 70 + c, err := g.r.CommitObject(g.h) 71 + if err != nil { 72 + return "", fmt.Errorf("commit object: %w", err) 73 + } 74 + 75 + tree, err := c.Tree() 76 + if err != nil { 77 + return "", fmt.Errorf("file tree: %w", err) 78 + } 79 + 80 + file, err := tree.File(path) 81 + if err != nil { 82 + return "", err 83 + } 84 + 85 + isbin, _ := file.IsBinary() 86 + if !isbin { 87 + return file.Contents() 88 + } else { 89 + return "Not displaying binary file", nil 90 + } 91 +} 92 + 93 +func (g *Repo) Tags() ([]*TagReference, error) { 94 + iter, err := g.r.Tags() 95 + if err != nil { 96 + return nil, fmt.Errorf("tag objects: %w", err) 97 + } 98 + 99 + tags := make([]*TagReference, 0) 100 + if err := iter.ForEach(func(ref *plumbing.Reference) error { 101 + obj, err := g.r.TagObject(ref.Hash()) 102 + switch err { 103 + case nil: 104 + tags = append(tags, &TagReference{ 105 + ref: ref, 106 + tag: obj, 107 + }) 108 + case plumbing.ErrObjectNotFound: 109 + tags = append(tags, &TagReference{ 110 + ref: ref, 111 + }) 112 + default: 113 + return err 114 + } 115 + return nil 116 + }); err != nil { 117 + return nil, err 118 + } 119 + 120 + tagList := &TagList{r: g.r, refs: tags} 121 + sort.Sort(tagList) 122 + return tags, nil 123 +} 124 + 125 +func (g *Repo) Branches() ([]*plumbing.Reference, error) { 126 + bi, err := g.r.Branches() 127 + if err != nil { 128 + return nil, fmt.Errorf("branch: %w", err) 129 + } 130 + 131 + branches := []*plumbing.Reference{} 132 + err = bi.ForEach(func(ref *plumbing.Reference) error { 133 + branches = append(branches, ref) 134 + return nil 135 + }) 136 + return branches, err 137 +} 138 + 139 +func (g *Repo) Description() (string, error) { 140 + c, err := g.r.Config() 141 + if err != nil { 142 + return "", fmt.Errorf("failed to read config: %w", err) 143 + } 144 + 145 + s := c.Raw.Section("mugit") 146 + return s.Options.Get("description"), nil 147 +} 148 + 149 +func (g *Repo) IsPrivate() (bool, error) { 150 + c, err := g.r.Config() 151 + if err != nil { 152 + return false, fmt.Errorf("failed to read config: %w", err) 153 + } 154 + 155 + s := c.Raw.Section("mugit") 156 + return s.Options.Get("private") == "true", nil 157 +} 158 + 159 +func (g *Repo) IsGoMod() bool { 160 + _, err := g.FileContent("go.mod") 161 + return err == nil 162 +} 163 + 164 +func (g *Repo) FindMasterBranch(masters []string) (string, error) { 165 + for _, b := range masters { 166 + if _, err := g.r.ResolveRevision(plumbing.Revision(b)); err == nil { 167 + return b, nil 168 + } 169 + } 170 + return "", fmt.Errorf("unable to find master branch") 171 +}
A
internal/git/tree.go
路路路 1 +package git 2 + 3 +import ( 4 + "fmt" 5 + 6 + "github.com/go-git/go-git/v5/plumbing/object" 7 +) 8 + 9 +type NiceTree struct { 10 + Name string 11 + Mode string 12 + Size int64 13 + IsFile bool 14 + IsSubtree bool 15 +} 16 + 17 +func makeNiceTree(t *object.Tree) []NiceTree { 18 + nts := []NiceTree{} 19 + for _, e := range t.Entries { 20 + mode, _ := e.Mode.ToOSFileMode() 21 + sz, _ := t.Size(e.Name) 22 + nts = append(nts, NiceTree{ 23 + Name: e.Name, 24 + Mode: mode.String(), 25 + IsFile: e.Mode.IsFile(), 26 + Size: sz, 27 + }) 28 + } 29 + return nts 30 +} 31 + 32 +func (g *Repo) FileTree(path string) ([]NiceTree, error) { 33 + c, err := g.r.CommitObject(g.h) 34 + if err != nil { 35 + return nil, fmt.Errorf("commit object: %w", err) 36 + } 37 + 38 + files := []NiceTree{} 39 + tree, err := c.Tree() 40 + if err != nil { 41 + return nil, fmt.Errorf("file tree: %w", err) 42 + } 43 + 44 + if path == "" { 45 + files = makeNiceTree(tree) 46 + } else { 47 + o, err := tree.FindEntry(path) 48 + if err != nil { 49 + return nil, err 50 + } 51 + 52 + if !o.Mode.IsFile() { 53 + subtree, err := tree.Tree(path) 54 + if err != nil { 55 + return nil, err 56 + } 57 + files = makeNiceTree(subtree) 58 + } 59 + } 60 + 61 + return files, nil 62 +}
M
internal/handlers/git.go
路路路 1 1 package handlers 2 + 3 +import ( 4 + "compress/gzip" 5 + "io" 6 + "log/slog" 7 + "net/http" 8 + "path/filepath" 9 + 10 + "olexsmir.xyz/mugit/internal/git/gitservice" 11 +) 12 + 13 +func (h *handlers) infoRefs(w http.ResponseWriter, r *http.Request) { 14 + // TODO: 404 for private repos 15 + 16 + name := filepath.Clean(r.PathValue("name")) 17 + 18 + w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") 19 + w.WriteHeader(http.StatusOK) 20 + 21 + if err := gitservice.InfoRefs( 22 + filepath.Join(h.c.Repo.Dir, name), // FIXME: use securejoin 23 + w, 24 + ); err != nil { 25 + slog.Error("git: info/refs", "err", err) 26 + return 27 + } 28 +} 29 + 30 +func (h *handlers) uploadPack(w http.ResponseWriter, r *http.Request) { 31 + // TODO: 404 for private repos 32 + 33 + name := filepath.Clean(r.PathValue("name")) 34 + 35 + w.Header().Set("content-type", "application/x-git-upload-pack-result") 36 + w.Header().Set("Connection", "Keep-Alive") 37 + w.Header().Set("Transfer-Encoding", "chunked") 38 + w.WriteHeader(http.StatusOK) 39 + 40 + reader := io.Reader(r.Body) 41 + if r.Header.Get("Content-Encoding") == "gzip" { 42 + gr, err := gzip.NewReader(r.Body) 43 + if err != nil { 44 + slog.Error("git: gzip reader", "err", err) 45 + return 46 + } 47 + defer gr.Close() 48 + reader = gr 49 + } 50 + 51 + if err := gitservice.UploadPack( 52 + filepath.Join(h.c.Repo.Dir, name), 53 + reader, 54 + newFlushWriter(w), 55 + ); err != nil { 56 + slog.Error("git: upload-pack", "err", err) 57 + return 58 + } 59 +} 60 + 61 +type flushWriter struct { 62 + w io.Writer 63 + f http.Flusher 64 +} 65 + 66 +func newFlushWriter(w http.ResponseWriter) io.Writer { 67 + f, _ := w.(http.Flusher) 68 + return &flushWriter{w: w, f: f} 69 +} 70 + 71 +func (fw *flushWriter) Write(p []byte) (int, error) { 72 + n, err := fw.w.Write(p) 73 + if fw.f != nil { 74 + fw.f.Flush() 75 + } 76 + return n, err 77 +}
M
internal/handlers/handles.go
路路路 24 24 25 25 return mux 26 26 } 27 + 28 +// multiplex if request smells like gitprotocol-http(5) passes it to the git 29 +// http service renders templates. 30 +func (h *handlers) multiplex(w http.ResponseWriter, r *http.Request) { 31 + if r.URL.RawQuery == "service=git-receive-pack" { 32 + w.WriteHeader(http.StatusBadRequest) 33 + w.Write([]byte("http pushing isn't supported")) 34 + return 35 + } 36 + 37 + path := r.PathValue("rest") 38 + if path == "info/refs" && 39 + r.URL.RawQuery == "service=git-upload-pack" && 40 + r.Method == "GET" { 41 + h.infoRefs(w, r) 42 + } else if path == "git-upload-pack" && r.Method == "POST" { 43 + h.uploadPack(w, r) 44 + } else if r.Method == "GET" { 45 + h.repoIndex(w, r) 46 + } 47 +}