add submodule diff links (#33097)

This adds links to submodules in diffs, similar to the existing link
when viewing a repo at a specific commit. It does this by expanding diff
parsing to recognize changes to submodules, and find the specific refs
that are added, deleted or changed.

Related #25888

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Rowan Bohde
2025-01-07 19:38:30 -06:00
committed by GitHub
parent ec84687df9
commit a8e7caedfa
23 changed files with 688 additions and 339 deletions

File diff suppressed because one or more lines are too long

25
go.mod
View File

@ -24,7 +24,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/ProtonMail/go-crypto v1.0.0
github.com/ProtonMail/go-crypto v1.1.4
github.com/PuerkitoBio/goquery v1.10.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
github.com/alecthomas/chroma/v2 v2.14.0
@ -54,8 +54,8 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.1
github.com/go-git/go-billy/v5 v5.6.0
github.com/go-git/go-git/v5 v5.12.0
github.com/go-git/go-billy/v5 v5.6.1
github.com/go-git/go-git/v5 v5.13.1
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-redsync/redsync/v4 v4.13.0
github.com/go-sql-driver/mysql v1.8.1
@ -106,7 +106,7 @@ require (
github.com/sassoftware/go-rpmutils v0.4.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.12
@ -118,14 +118,14 @@ require (
github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.31.0
golang.org/x/crypto v0.32.0
golang.org/x/image v0.21.0
golang.org/x/net v0.33.0
golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0
golang.org/x/sys v0.29.0
golang.org/x/text v0.21.0
golang.org/x/tools v0.26.0
golang.org/x/tools v0.29.0
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.35.1
gopkg.in/ini.v1 v1.67.0
@ -186,7 +186,7 @@ require (
github.com/couchbase/gomemcached v0.3.2 // indirect
github.com/couchbase/goutils v0.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.3.4 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@ -219,7 +219,7 @@ require (
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.3 // indirect
@ -253,6 +253,7 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mmcloughlin/avo v0.6.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
@ -264,7 +265,7 @@ require (
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pjbgf/sha1cd v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
@ -304,7 +305,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect

70
go.sum
View File

@ -71,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
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.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.1.4 h1:G5U5asvD5N/6/36oIw3k2bOfBn5XVcZrb7PBjzzKKoE=
github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
@ -188,7 +188,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buildkite/terminal-to-html/v3 v3.16.3 h1:IGuJjboHjuMLWOGsKZKNxbbn41emOLiHzXPmQZk31fk=
github.com/buildkite/terminal-to-html/v3 v3.16.3/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0=
github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
@ -205,7 +204,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -223,8 +221,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -254,8 +252,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJ
github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w=
github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
@ -313,12 +311,12 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5Hql
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
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.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
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.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
@ -385,8 +383,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@ -581,6 +579,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -631,8 +631,8 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI=
github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -737,8 +737,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@ -823,16 +823,14 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
@ -846,8 +844,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -859,18 +857,16 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -910,8 +906,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -921,14 +915,12 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
@ -936,15 +928,13 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@ -964,8 +954,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -784,60 +784,10 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo
return &repo, err
}
func parseRepositoryURL(ctx context.Context, repoURL string) (ret struct {
OwnerName, RepoName, RemainingPath string
},
) {
// possible urls for git:
// https://my.domain/sub-path/<owner>/<repo>[.git]
// git+ssh://user@my.domain/<owner>/<repo>[.git]
// ssh://user@my.domain/<owner>/<repo>[.git]
// user@my.domain:<owner>/<repo>[.git]
fillPathParts := func(s string) {
s = strings.TrimPrefix(s, "/")
fields := strings.SplitN(s, "/", 3)
if len(fields) >= 2 {
ret.OwnerName = fields[0]
ret.RepoName = strings.TrimSuffix(fields[1], ".git")
if len(fields) == 3 {
ret.RemainingPath = "/" + fields[2]
}
}
}
parsed, err := giturl.ParseGitURL(repoURL)
if err != nil {
return ret
}
if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" {
if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) {
return ret
}
fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL))
} else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" {
domainSSH := setting.SSH.Domain
domainCur := httplib.GuessCurrentHostDomain(ctx)
urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host)
urlDomain = util.IfZero(urlDomain, parsed.URL.Host)
if urlDomain == "" {
return ret
}
// check whether URL domain is the App domain
domainMatches := domainSSH == urlDomain
// check whether URL domain is current domain from context
domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain)
if domainMatches {
fillPathParts(parsed.URL.Path)
}
}
return ret
}
// GetRepositoryByURL returns the repository by given url
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
ret := parseRepositoryURL(ctx, repoURL)
if ret.OwnerName == "" {
ret, err := giturl.ParseRepositoryURL(ctx, repoURL)
if err != nil || ret.OwnerName == "" {
return nil, fmt.Errorf("unknown or malformed repository URL")
}
return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)

View File

@ -4,16 +4,12 @@
package repo
import (
"context"
"net/http"
"net/url"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@ -132,79 +128,6 @@ func TestMetas(t *testing.T) {
assert.Equal(t, ",owners,team1,", metas["teams"])
}
func TestParseRepositoryURLPathSegments(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000")()
ctxURL, _ := url.Parse("https://gitea")
ctxReq := &http.Request{URL: ctxURL, Header: http.Header{}}
ctxReq.Host = ctxURL.Host
ctxReq.Header.Add("X-Forwarded-Proto", ctxURL.Scheme)
ctx := context.WithValue(context.Background(), httplib.RequestContextKey, ctxReq)
cases := []struct {
input string
ownerName, repoName, remaining string
}{
{input: "/user/repo"},
{input: "https://localhost:3000/user/repo", ownerName: "user", repoName: "repo"},
{input: "https://external:3000/user/repo"},
{input: "https://localhost:3000/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"},
{input: "https://gitea/user/repo", ownerName: "user", repoName: "repo"},
{input: "https://gitea:3333/user/repo"},
{input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"},
{input: "ssh://external:2222/user/repo"},
{input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"},
{input: "git+ssh://user@external/user/repo.git"},
{input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"},
{input: "root@gitea:user/repo.git", ownerName: "user", repoName: "repo"},
{input: "root@external:user/repo.git"},
}
for _, c := range cases {
t.Run(c.input, func(t *testing.T) {
ret := parseRepositoryURL(ctx, c.input)
assert.Equal(t, c.ownerName, ret.OwnerName)
assert.Equal(t, c.repoName, ret.RepoName)
assert.Equal(t, c.remaining, ret.RemainingPath)
})
}
t.Run("WithSubpath", func(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")()
defer test.MockVariableValue(&setting.AppSubURL, "/subpath")()
cases = []struct {
input string
ownerName, repoName, remaining string
}{
{input: "https://localhost:3000/user/repo"},
{input: "https://localhost:3000/subpath/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"},
{input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"},
{input: "ssh://external:2222/user/repo"},
{input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"},
{input: "git+ssh://user@external/user/repo.git"},
{input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"},
{input: "root@external:user/repo.git"},
}
for _, c := range cases {
t.Run(c.input, func(t *testing.T) {
ret := parseRepositoryURL(ctx, c.input)
assert.Equal(t, c.ownerName, ret.OwnerName)
assert.Equal(t, c.repoName, ret.RepoName)
assert.Equal(t, c.remaining, ret.RemainingPath)
})
}
})
}
func TestGetRepositoryByURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -7,5 +7,5 @@ package git
type CommitInfo struct {
Entry *TreeEntry
Commit *Commit
SubModuleFile *CommitSubModuleFile
SubmoduleFile *CommitSubmoduleFile
}

View File

@ -85,8 +85,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String())
commitsInfo[i].SubModuleFile = subModuleFile
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
commitsInfo[i].SubmoduleFile = subModuleFile
}
}

View File

@ -79,8 +79,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
} else if subModule != nil {
subModuleURL = subModule.URL
}
subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String())
commitsInfo[i].SubModuleFile = subModuleFile
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
commitsInfo[i].SubmoduleFile = subModuleFile
}
}

View File

@ -3,6 +3,10 @@
package git
type SubmoduleWebLink struct {
RepoWebLink, CommitWebLink string
}
// GetSubModules get all the submodules of current revision git tree
func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
if c.submoduleCache != nil {

View File

@ -5,107 +5,50 @@
package git
import (
"fmt"
"net"
"net/url"
"path"
"regexp"
"strings"
"context"
giturl "code.gitea.io/gitea/modules/git/url"
)
var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`)
// CommitSubModuleFile represents a file with submodule type.
type CommitSubModuleFile struct {
refURL string
refID string
// CommitSubmoduleFile represents a file with submodule type.
type CommitSubmoduleFile struct {
refURL string
parsedURL *giturl.RepositoryURL
parsed bool
refID string
repoLink string
}
// NewCommitSubModuleFile create a new submodule file
func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile {
return &CommitSubModuleFile{
refURL: refURL,
refID: refID,
}
// NewCommitSubmoduleFile create a new submodule file
func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile {
return &CommitSubmoduleFile{refURL: refURL, refID: refID}
}
func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
if refURL == "" {
return ""
func (sf *CommitSubmoduleFile) RefID() string {
return sf.refID // this function is only used in templates
}
// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver
func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
if sf == nil {
return nil
}
refURI := strings.TrimSuffix(refURL, ".git")
prefixURL, _ := url.Parse(urlPrefix)
urlPrefixHostname, _, err := net.SplitHostPort(prefixURL.Host)
if err != nil {
urlPrefixHostname = prefixURL.Host
}
urlPrefix = strings.TrimSuffix(urlPrefix, "/")
// FIXME: Need to consider branch - which will require changes in modules/git/commit.go:GetSubModules
// Relative url prefix check (according to git submodule documentation)
if strings.HasPrefix(refURI, "./") || strings.HasPrefix(refURI, "../") {
return urlPrefix + path.Clean(path.Join("/", repoFullName, refURI))
}
if !strings.Contains(refURI, "://") {
// scp style syntax which contains *no* port number after the : (and is not parsed by net/url)
// ex: git@try.gitea.io:go-gitea/gitea
match := scpSyntax.FindAllStringSubmatch(refURI, -1)
if len(match) > 0 {
m := match[0]
refHostname := m[2]
pth := m[3]
if !strings.HasPrefix(pth, "/") {
pth = "/" + pth
}
if urlPrefixHostname == refHostname || refHostname == sshDomain {
return urlPrefix + path.Clean(path.Join("/", pth))
}
return "http://" + refHostname + pth
if !sf.parsed {
sf.parsed = true
parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
if err != nil {
return nil
}
sf.parsedURL = parsedURL
sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL)
}
ref, err := url.Parse(refURI)
if err != nil {
return ""
var commitLink string
if len(optCommitID) == 2 {
commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
} else if len(optCommitID) == 1 {
commitLink = sf.repoLink + "/commit/" + optCommitID[0]
} else {
commitLink = sf.repoLink + "/commit/" + sf.refID
}
refHostname, _, err := net.SplitHostPort(ref.Host)
if err != nil {
refHostname = ref.Host
}
supportedSchemes := []string{"http", "https", "git", "ssh", "git+ssh"}
for _, scheme := range supportedSchemes {
if ref.Scheme == scheme {
if ref.Scheme == "http" || ref.Scheme == "https" {
if len(ref.User.Username()) > 0 {
return ref.Scheme + "://" + fmt.Sprintf("%v", ref.User) + "@" + ref.Host + ref.Path
}
return ref.Scheme + "://" + ref.Host + ref.Path
} else if urlPrefixHostname == refHostname || refHostname == sshDomain {
return urlPrefix + path.Clean(path.Join("/", ref.Path))
}
return "http://" + refHostname + ref.Path
}
}
return ""
}
// RefURL guesses and returns reference URL.
// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore)
func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string {
return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain)
}
// RefID returns reference ID.
func (sf *CommitSubModuleFile) RefID() string {
return sf.refID
return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
}

View File

@ -1,42 +1,30 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommitSubModuleFileGetRefURL(t *testing.T) {
kases := []struct {
refURL string
prefixURL string
parentPath string
SSHDomain string
expect string
}{
{"git://github.com/user1/repo1", "/", "user1/repo2", "", "http://github.com/user1/repo1"},
{"https://localhost/user1/repo1.git", "/", "user1/repo2", "", "https://localhost/user1/repo1"},
{"http://localhost/user1/repo1.git", "/", "owner/reponame", "", "http://localhost/user1/repo1"},
{"git@github.com:user1/repo1.git", "/", "owner/reponame", "", "http://github.com/user1/repo1"},
{"ssh://git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/zefie/lge_g6_kernel_scripts"},
{"git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/2222/zefie/lge_g6_kernel_scripts"},
{"git@try.gitea.io:go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"},
{"ssh://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"},
{"git://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"},
{"ssh://git@127.0.0.1:9999/go-gitea/gitea", "https://127.0.0.1:3000/", "go-gitea/sdk", "", "https://127.0.0.1:3000/go-gitea/gitea"},
{"https://gitea.com:3000/user1/repo1.git", "https://127.0.0.1:3000/", "user/repo2", "", "https://gitea.com:3000/user1/repo1"},
{"https://example.gitea.com/gitea/user1/repo1.git", "https://example.gitea.com/gitea/", "", "user/repo2", "https://example.gitea.com/gitea/user1/repo1"},
{"https://username:password@github.com/username/repository.git", "/", "username/repository2", "", "https://username:password@github.com/username/repository"},
{"somethingbad", "https://127.0.0.1:3000/go-gitea/gitea", "/", "", ""},
{"git@localhost:user/repo", "https://localhost/", "user2/repo1", "", "https://localhost/user/repo"},
{"../path/to/repo.git/", "https://localhost/", "user/repo2", "", "https://localhost/user/path/to/repo.git"},
{"ssh://git@ssh.gitea.io:2222/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "ssh.gitea.io", "https://try.gitea.io/go-gitea/gitea"},
}
func TestCommitSubmoduleLink(t *testing.T) {
sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa")
for _, kase := range kases {
assert.EqualValues(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath, kase.SSHDomain))
}
wl := sf.SubmoduleWebLink(context.Background())
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
assert.Equal(t, "https://github.com/user/repo/commit/aaaa", wl.CommitWebLink)
wl = sf.SubmoduleWebLink(context.Background(), "1111")
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
assert.Equal(t, "https://github.com/user/repo/commit/1111", wl.CommitWebLink)
wl = sf.SubmoduleWebLink(context.Background(), "1111", "2222")
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(context.Background())
assert.Nil(t, wl)
}

View File

@ -4,9 +4,15 @@
package url
import (
"context"
"fmt"
"net"
stdurl "net/url"
"strings"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// ErrWrongURLFormat represents an error with wrong url format
@ -90,3 +96,86 @@ func ParseGitURL(remote string) (*GitURL, error) {
extraMark: 2,
}, nil
}
type RepositoryURL struct {
GitURL *GitURL
// if the URL belongs to current Gitea instance, then the below fields have values
OwnerName string
RepoName string
RemainingPath string
}
// ParseRepositoryURL tries to parse a Git URL and extract the owner/repository name if it belongs to current Gitea instance.
func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, error) {
// possible urls for git:
// https://my.domain/sub-path/<owner>/<repo>[.git]
// git+ssh://user@my.domain/<owner>/<repo>[.git]
// ssh://user@my.domain/<owner>/<repo>[.git]
// user@my.domain:<owner>/<repo>[.git]
parsed, err := ParseGitURL(repoURL)
if err != nil {
return nil, err
}
ret := &RepositoryURL{}
ret.GitURL = parsed
fillPathParts := func(s string) {
s = strings.TrimPrefix(s, "/")
fields := strings.SplitN(s, "/", 3)
if len(fields) >= 2 {
ret.OwnerName = fields[0]
ret.RepoName = strings.TrimSuffix(fields[1], ".git")
if len(fields) == 3 {
ret.RemainingPath = "/" + fields[2]
}
}
}
if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" {
if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) {
return ret, nil
}
fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL))
} else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" {
domainSSH := setting.SSH.Domain
domainCur := httplib.GuessCurrentHostDomain(ctx)
urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host)
urlDomain = util.IfZero(urlDomain, parsed.URL.Host)
if urlDomain == "" {
return ret, nil
}
// check whether URL domain is the App domain
domainMatches := domainSSH == urlDomain
// check whether URL domain is current domain from context
domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain)
if domainMatches {
fillPathParts(parsed.URL.Path)
}
}
return ret, nil
}
// MakeRepositoryWebLink generates a web link (http/https) for a git repository (by guessing sometimes)
func MakeRepositoryWebLink(repoURL *RepositoryURL) string {
if repoURL.OwnerName != "" {
return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + repoURL.RepoName
}
// now, let's guess, for example:
// * git@github.com:owner/submodule.git
// * https://github.com/example/submodule1.git
if repoURL.GitURL.Scheme == "http" || repoURL.GitURL.Scheme == "https" {
return strings.TrimSuffix(repoURL.GitURL.String(), ".git")
} else if repoURL.GitURL.Scheme == "ssh" || repoURL.GitURL.Scheme == "git+ssh" {
hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host)
hostname = util.IfZero(hostname, repoURL.GitURL.Host)
urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git")
urlPath = strings.TrimPrefix(urlPath, "/")
urlFull := fmt.Sprintf("https://%s/%s", hostname, urlPath)
urlFull = strings.TrimSuffix(urlFull, "/")
return urlFull
}
return ""
}

View File

@ -4,9 +4,15 @@
package url
import (
"context"
"net/http"
"net/url"
"testing"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
@ -164,3 +170,98 @@ func TestParseGitURLs(t *testing.T) {
})
}
}
func TestParseRepositoryURL(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000")()
defer test.MockVariableValue(&setting.SSH.Domain, "try.gitea.io")()
ctxURL, _ := url.Parse("https://gitea")
ctxReq := &http.Request{URL: ctxURL, Header: http.Header{}}
ctxReq.Host = ctxURL.Host
ctxReq.Header.Add("X-Forwarded-Proto", ctxURL.Scheme)
ctx := context.WithValue(context.Background(), httplib.RequestContextKey, ctxReq)
cases := []struct {
input string
ownerName, repoName, remaining string
}{
{input: "/user/repo"},
{input: "https://localhost:3000/user/repo", ownerName: "user", repoName: "repo"},
{input: "https://external:3000/user/repo"},
{input: "https://localhost:3000/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"},
{input: "https://gitea/user/repo", ownerName: "user", repoName: "repo"},
{input: "https://gitea:3333/user/repo"},
{input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"},
{input: "ssh://external:2222/user/repo"},
{input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"},
{input: "git+ssh://user@external/user/repo.git"},
{input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"},
{input: "root@gitea:user/repo.git", ownerName: "user", repoName: "repo"},
{input: "root@external:user/repo.git"},
}
for _, c := range cases {
t.Run(c.input, func(t *testing.T) {
ret, _ := ParseRepositoryURL(ctx, c.input)
assert.Equal(t, c.ownerName, ret.OwnerName)
assert.Equal(t, c.repoName, ret.RepoName)
assert.Equal(t, c.remaining, ret.RemainingPath)
})
}
t.Run("WithSubpath", func(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")()
defer test.MockVariableValue(&setting.AppSubURL, "/subpath")()
cases = []struct {
input string
ownerName, repoName, remaining string
}{
{input: "https://localhost:3000/user/repo"},
{input: "https://localhost:3000/subpath/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"},
{input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"},
{input: "ssh://external:2222/user/repo"},
{input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"},
{input: "git+ssh://user@external/user/repo.git"},
{input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"},
{input: "root@external:user/repo.git"},
}
for _, c := range cases {
t.Run(c.input, func(t *testing.T) {
ret, _ := ParseRepositoryURL(ctx, c.input)
assert.Equal(t, c.ownerName, ret.OwnerName)
assert.Equal(t, c.repoName, ret.RepoName)
assert.Equal(t, c.remaining, ret.RemainingPath)
})
}
})
}
func TestMakeRepositoryBaseLink(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")()
defer test.MockVariableValue(&setting.AppSubURL, "/subpath")()
u, err := ParseRepositoryURL(context.Background(), "https://localhost:3000/subpath/user/repo.git")
assert.NoError(t, err)
assert.Equal(t, "/subpath/user/repo", MakeRepositoryWebLink(u))
u, err = ParseRepositoryURL(context.Background(), "https://github.com/owner/repo.git")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u))
u, err = ParseRepositoryURL(context.Background(), "git@github.com:owner/repo.git")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u))
u, err = ParseRepositoryURL(context.Background(), "git+ssh://other:123/owner/repo.git")
assert.NoError(t, err)
assert.Equal(t, "https://other/owner/repo", MakeRepositoryWebLink(u))
}

View File

@ -6,6 +6,7 @@ package repository
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@ -51,6 +52,9 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
{
branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil {
if strings.Contains(err.Error(), "ref file is empty") {
return 0, nil
}
return 0, err
}
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)

View File

@ -2628,6 +2628,9 @@ diff.image.overlay = Overlay
diff.has_escaped = This line has hidden Unicode characters
diff.show_file_tree = Show file tree
diff.hide_file_tree = Hide file tree
diff.submodule_added = Submodule %[1]s added at %[2]s
diff.submodule_deleted = Submodule %[1]s deleted from %[2]s
diff.submodule_updated = Submodule %[1]s updated: %[2]s
releases.desc = Track project versions and downloads.
release.releases = Releases

View File

@ -309,7 +309,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
}
ctx.Data["TreeLink"] = treeLink
ctx.Data["SSHDomain"] = setting.SSH.Domain
return allEntries
}

View File

@ -360,7 +360,6 @@ type DiffFile struct {
IsLFSFile bool
IsRenamed bool
IsAmbiguous bool
IsSubmodule bool
Sections []*DiffSection
IsIncomplete bool
IsIncompleteLineTooLong bool
@ -372,6 +371,9 @@ type DiffFile struct {
Language string
Mode string
OldMode string
IsSubmodule bool // if IsSubmodule==true, then there must be a SubmoduleDiffInfo
SubmoduleDiffInfo *SubmoduleDiffInfo
}
// GetType returns type of diff file.
@ -609,9 +611,8 @@ parsingLoop:
if strings.HasPrefix(line, "new mode ") {
curFile.Mode = prepareValue(line, "new mode ")
}
if strings.HasSuffix(line, " 160000\n") {
curFile.IsSubmodule = true
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
}
case strings.HasPrefix(line, "rename from "):
curFile.IsRenamed = true
@ -646,17 +647,17 @@ parsingLoop:
curFile.Mode = prepareValue(line, "new file mode ")
}
if strings.HasSuffix(line, " 160000\n") {
curFile.IsSubmodule = true
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
}
case strings.HasPrefix(line, "deleted"):
curFile.Type = DiffFileDel
curFile.IsDeleted = true
if strings.HasSuffix(line, " 160000\n") {
curFile.IsSubmodule = true
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
}
case strings.HasPrefix(line, "index"):
if strings.HasSuffix(line, " 160000\n") {
curFile.IsSubmodule = true
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
}
case strings.HasPrefix(line, "similarity index 100%"):
curFile.Type = DiffFileRename
@ -915,6 +916,13 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
}
}
curSection.Lines = append(curSection.Lines, diffLine)
// Parse submodule additions
if curFile.SubmoduleDiffInfo != nil {
if ref, found := bytes.CutPrefix(lineBytes, []byte("+Subproject commit ")); found {
curFile.SubmoduleDiffInfo.NewRefID = string(bytes.TrimSpace(ref))
}
}
case '-':
curFileLinesCount++
curFile.Deletion++
@ -936,6 +944,13 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
lastLeftIdx = len(curSection.Lines)
}
curSection.Lines = append(curSection.Lines, diffLine)
// Parse submodule deletion
if curFile.SubmoduleDiffInfo != nil {
if ref, found := bytes.CutPrefix(lineBytes, []byte("-Subproject commit ")); found {
curFile.SubmoduleDiffInfo.PreviousRefID = string(bytes.TrimSpace(ref))
}
}
case ' ':
curFileLinesCount++
if maxLines > -1 && curFileLinesCount >= maxLines {
@ -1195,6 +1210,11 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
}
}
// Populate Submodule URLs
if diffFile.SubmoduleDiffInfo != nil {
diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, commit)
}
if !isVendored.Has() {
isVendored = optional.Some(analyze.IsVendor(diffFile.Name))
}

View File

@ -0,0 +1,65 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitdiff
import (
"context"
"html/template"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
)
type SubmoduleDiffInfo struct {
SubmoduleName string
SubmoduleFile *git.CommitSubmoduleFile // it might be nil if the submodule is not found or unable to parse
NewRefID string
PreviousRefID string
}
func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) {
si.SubmoduleName = diffFile.Name
submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit
if diffFile.IsDeleted {
submoduleCommit = leftCommit // If the submodule is deleted, check at the left commit
}
if submoduleCommit == nil {
return
}
submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName())
if err != nil {
log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err)
return // ignore the error, do not cause 500 errors for end users
}
if submodule != nil {
si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String())
}
}
func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML {
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID)
if webLink == nil {
return htmlutil.HTMLFormat("%s", base.ShortSha(commitID))
}
return htmlutil.HTMLFormat(`<a href="%s">%s</a>`, webLink.CommitWebLink, base.ShortSha(commitID))
}
func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML {
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID)
if webLink == nil {
return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID))
}
return htmlutil.HTMLFormat(`<a href="%s">%s...%s</a>`, webLink.CommitWebLink, base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID))
}
func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML {
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx)
if webLink == nil {
return htmlutil.HTMLFormat("%s", si.SubmoduleName)
}
return htmlutil.HTMLFormat(`<a href="%s">%s</a>`, webLink.RepoWebLink, si.SubmoduleName)
}

View File

@ -0,0 +1,236 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitdiff
import (
"context"
"strings"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestParseSubmoduleInfo(t *testing.T) {
type testcase struct {
name string
gitdiff string
infos map[int]SubmoduleDiffInfo
}
tests := []testcase{
{
name: "added",
gitdiff: `diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..4ac13c1
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "gitea-mirror"]
+ path = gitea-mirror
+ url = https://gitea.com/gitea/gitea-mirror
diff --git a/gitea-mirror b/gitea-mirror
new file mode 160000
index 0000000..68972a9
--- /dev/null
+++ b/gitea-mirror
@@ -0,0 +1 @@
+Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8
`,
infos: map[int]SubmoduleDiffInfo{
1: {NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8"},
},
},
{
name: "updated",
gitdiff: `diff --git a/gitea-mirror b/gitea-mirror
index 68972a9..c8ffe77 160000
--- a/gitea-mirror
+++ b/gitea-mirror
@@ -1 +1 @@
-Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8
+Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
`,
infos: map[int]SubmoduleDiffInfo{
0: {
PreviousRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8",
NewRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
},
},
},
{
name: "rename",
gitdiff: `diff --git a/.gitmodules b/.gitmodules
index 4ac13c1..0510edd 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "gitea-mirror"]
- path = gitea-mirror
+ path = gitea
url = https://gitea.com/gitea/gitea-mirror
diff --git a/gitea-mirror b/gitea
similarity index 100%
rename from gitea-mirror
rename to gitea
`,
},
{
name: "deleted",
gitdiff: `diff --git a/.gitmodules b/.gitmodules
index 0510edd..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "gitea-mirror"]
- path = gitea
- url = https://gitea.com/gitea/gitea-mirror
diff --git a/gitea b/gitea
deleted file mode 160000
index c8ffe77..0000000
--- a/gitea
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
`,
infos: map[int]SubmoduleDiffInfo{
1: {
PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
},
},
},
{
name: "moved and updated",
gitdiff: `diff --git a/.gitmodules b/.gitmodules
index 0510edd..bced3d8 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "gitea-mirror"]
- path = gitea
+ path = gitea-1.22
url = https://gitea.com/gitea/gitea-mirror
diff --git a/gitea b/gitea
deleted file mode 160000
index c8ffe77..0000000
--- a/gitea
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
diff --git a/gitea-1.22 b/gitea-1.22
new file mode 160000
index 0000000..8eefa1f
--- /dev/null
+++ b/gitea-1.22
@@ -0,0 +1 @@
+Subproject commit 8eefa1f6dedf2488db2c9e12c916e8e51f673160
`,
infos: map[int]SubmoduleDiffInfo{
1: {
PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
},
2: {
NewRefID: "8eefa1f6dedf2488db2c9e12c916e8e51f673160",
},
},
},
{
name: "converted to file",
gitdiff: `diff --git a/.gitmodules b/.gitmodules
index 0510edd..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "gitea-mirror"]
- path = gitea
- url = https://gitea.com/gitea/gitea-mirror
diff --git a/gitea b/gitea
deleted file mode 160000
index c8ffe77..0000000
--- a/gitea
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
diff --git a/gitea b/gitea
new file mode 100644
index 0000000..33a9488
--- /dev/null
+++ b/gitea
@@ -0,0 +1 @@
+example
`,
infos: map[int]SubmoduleDiffInfo{
1: {
PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
},
},
},
{
name: "converted to submodule",
gitdiff: `diff --git a/.gitmodules b/.gitmodules
index e69de29..14ee267 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "gitea"]
+ path = gitea
+ url = https://gitea.com/gitea/gitea-mirror
diff --git a/gitea b/gitea
deleted file mode 100644
index 33a9488..0000000
--- a/gitea
+++ /dev/null
@@ -1 +0,0 @@
-example
diff --git a/gitea b/gitea
new file mode 160000
index 0000000..68972a9
--- /dev/null
+++ b/gitea
@@ -0,0 +1 @@
+Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8
`,
infos: map[int]SubmoduleDiffInfo{
2: {
NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8",
},
},
},
}
for _, testcase := range tests {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
diff, err := ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "")
assert.NoError(t, err)
for i, expected := range testcase.infos {
actual := diff.Files[i]
assert.NotNil(t, actual)
assert.Equal(t, expected, *actual.SubmoduleDiffInfo)
}
})
}
}
func TestSubmoduleInfo(t *testing.T) {
sdi := &SubmoduleDiffInfo{
SubmoduleName: "name",
PreviousRefID: "aaaa",
NewRefID: "bbbb",
}
ctx := context.Background()
assert.EqualValues(t, "1111", sdi.CommitRefIDLinkHTML(ctx, "1111"))
assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx))
assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx))
sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234")
assert.EqualValues(t, `<a href="https://github.com/owner/repo/commit/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx))
assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx))
}

View File

@ -58,7 +58,7 @@
</div>
{{end}}
<script id="diff-data-script" type="module">
const diffDataFiles = [{{range $i, $file := .Diff.Files}}{Name:"{{$file.Name}}",NameHash:"{{$file.NameHash}}",Type:{{$file.Type}},IsBin:{{$file.IsBin}},Addition:{{$file.Addition}},Deletion:{{$file.Deletion}},IsViewed:{{$file.IsViewed}}},{{end}}];
const diffDataFiles = [{{range $i, $file := .Diff.Files}}{Name:"{{$file.Name}}",NameHash:"{{$file.NameHash}}",Type:{{$file.Type}},IsBin:{{$file.IsBin}},IsSubmodule:{{$file.IsSubmodule}},Addition:{{$file.Addition}},Deletion:{{$file.Deletion}},IsViewed:{{$file.IsViewed}}},{{end}}];
const diffData = {
isIncomplete: {{.Diff.IsIncomplete}},
tooManyFilesMessage: "{{ctx.Locale.Tr "repo.diff.too_many_files"}}",
@ -161,23 +161,25 @@
<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{ctx.Locale.Tr "repo.pulls.has_viewed_file"}}
</label>
{{end}}
<button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button>
<div class="tippy-target">
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
<button class="unescape-button item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
<button class="escape-button tw-hidden item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
{{end}}
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
{{if $file.IsDeleted}}
<a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
{{else}}
<a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
{{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}}
<a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a>
{{if not $file.IsSubmodule}}
<button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button>
<div class="tippy-target">
{{if not (or $file.IsIncomplete $file.IsBin)}}
<button class="unescape-button item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
<button class="escape-button tw-hidden item" data-unicode-content-selector="#diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
{{end}}
{{if not $.PageIsWiki}}
{{if $file.IsDeleted}}
<a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
{{else}}
<a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
{{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}}
<a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a>
{{end}}
{{end}}
{{end}}
{{end}}
</div>
</div>
{{end}}
</div>
</h4>
<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>
@ -195,6 +197,17 @@
{{ctx.Locale.Tr "repo.diff.bin_not_shown"}}
{{end}}
</div>
{{else if $file.SubmoduleDiffInfo}}
<div class="tw-p-3">{{svg "octicon-file-submodule"}} {{$submoduleDiffInfo := $file.SubmoduleDiffInfo -}}
{{- $submoduleName := $submoduleDiffInfo.SubmoduleRepoLinkHTML ctx -}}
{{- if $file.IsDeleted -}}
{{- ctx.Locale.Tr "repo.diff.submodule_deleted" $submoduleName ($submoduleDiffInfo.CommitRefIDLinkHTML ctx $submoduleDiffInfo.PreviousRefID) -}}
{{- else if $file.IsCreated -}}
{{- ctx.Locale.Tr "repo.diff.submodule_added" $submoduleName ($submoduleDiffInfo.CommitRefIDLinkHTML ctx $submoduleDiffInfo.NewRefID) -}}
{{- else -}}
{{- ctx.Locale.Tr "repo.diff.submodule_updated" $submoduleName ($submoduleDiffInfo.CompareRefIDLinkHTML ctx) -}}
{{end}}
</div>
{{else}}
<table class="chroma" data-new-comment-url="{{$.Issue.Link}}/files/reviews/new_comment" data-path="{{$file.Name}}">
{{if $.IsSplitStyle}}

View File

@ -13,15 +13,15 @@
<div class="repo-file-item">
{{$entry := $item.Entry}}
{{$commit := $item.Commit}}
{{$subModuleFile := $item.SubModuleFile}}
{{$submoduleFile := $item.SubmoduleFile}}
<div class="repo-file-cell name {{if not $commit}}notready{{end}}">
{{if $entry.IsSubModule}}
{{svg "octicon-file-submodule"}}
{{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{/* FIXME: the usage of AppUrl seems incorrect, it would be fixed in the future, use AppSubUrl instead */}}
{{if $refURL}}
<a class="muted" href="{{$refURL}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}}
{{if $submoduleLink}}
<a class="muted" href="{{$submoduleLink.RepoWebLink}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a>
{{else}}
{{$entry.Name}} <span class="at">@</span> {{ShortSha $subModuleFile.RefID}}
{{$entry.Name}} <span class="at">@</span> {{ShortSha $submoduleFile.RefID}}
{{end}}
{{else}}
{{if $entry.IsDir}}

View File

@ -8,6 +8,7 @@ type File = {
NameHash: string;
Type: number;
IsViewed: boolean;
IsSubmodule: boolean;
}
type Item = {
@ -34,6 +35,13 @@ function getIconForDiffType(pType) {
};
return diffTypes[pType];
}
function fileIcon(file) {
if (file.IsSubmodule) {
return 'octicon-file-submodule';
}
return 'octicon-file';
}
</script>
<template>
@ -44,7 +52,7 @@ function getIconForDiffType(pType) {
:title="item.name" :href="'#diff-' + item.file.NameHash"
>
<!-- file -->
<SvgIcon name="octicon-file"/>
<SvgIcon :name="fileIcon(item.file)"/>
<span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
<SvgIcon :name="getIconForDiffType(item.file.Type).name" :class="getIconForDiffType(item.file.Type).classes"/>
</a>

View File

@ -28,6 +28,7 @@ import octiconEye from '../../public/assets/img/svg/octicon-eye.svg';
import octiconFile from '../../public/assets/img/svg/octicon-file.svg';
import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-directory-fill.svg';
import octiconFileDirectoryOpenFill from '../../public/assets/img/svg/octicon-file-directory-open-fill.svg';
import octiconFileSubmodule from '../../public/assets/img/svg/octicon-file-submodule.svg';
import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg';
@ -104,6 +105,7 @@ const svgs = {
'octicon-file': octiconFile,
'octicon-file-directory-fill': octiconFileDirectoryFill,
'octicon-file-directory-open-fill': octiconFileDirectoryOpenFill,
'octicon-file-submodule': octiconFileSubmodule,
'octicon-filter': octiconFilter,
'octicon-gear': octiconGear,
'octicon-git-branch': octiconGitBranch,