Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eaa916a786 | |||
| 91901c2a60 | |||
| 20cf4b7849 | |||
| 5e7207d428 | |||
| e3bfee80dd | |||
| f93e2cf301 | |||
| 1b01d6de82 | |||
| d67cd622d0 | |||
| 15f3e9d5a5 | |||
| 01fa8b2b7e | |||
| 1d9ae7ac23 | |||
| 01873a99c1 | |||
| ce70863793 | |||
| 327f2207dc | |||
| db876d8f17 | |||
| 2b71bf283b | |||
| 1ca4fef611 | |||
| 70ee6b9029 | |||
| e5b404ec53 | |||
| 5842cd23a6 | |||
| 289bd9694b | |||
| 154d7521a5 | |||
| 24189dcced | |||
| f84bf259ad |
@@ -25,6 +25,10 @@ insert_final_newline = false
|
||||
[templates/user/auth/oidc_wellknown.tmpl]
|
||||
indent_style = space
|
||||
|
||||
[templates/shared/actions/runner_badge_*.tmpl]
|
||||
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
|
||||
charset = unset
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
|
||||
@@ -4,6 +4,35 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23
|
||||
|
||||
* SECURITY
|
||||
* Upgrade golang.org/x/crypto to 0.45.0 (#35985) (#35988)
|
||||
* Fix various permission & login related bugs (#36002) (#36004)
|
||||
* ENHANCEMENTS
|
||||
* Display source code downloads last for release attachments (#35897) (#35903)
|
||||
* Change project default column icon to 'star' (#35967) (#35979)
|
||||
* BUGFIXES
|
||||
* Allow empty commit when merging pull request with squash style (#35989) (#36003)
|
||||
* Fix container push tag overwriting (#35936) (#35954)
|
||||
* Fix corrupted external render content (#35946) and upgrade golang.org/x packages (#35950)
|
||||
* Limit reading bytes instead of ReadAll (#35928) (#35934)
|
||||
* Use correct form field for allowed force push users in branch protection API (#35894) (#35908)
|
||||
* Fix team member access check (#35899) (#35905)
|
||||
* Fix conda null depend issue (#35900) (#35902)
|
||||
* Set the dates to now when not specified by the caller (#35861) (#35874)
|
||||
* Fix gogit ListEntriesRecursiveWithSize (#35862)
|
||||
* Misc CSS fixes (#35888) (#35981)
|
||||
* Don't show unnecessary error message to end users for DeleteBranchAfterMerge (#35937) (#35941)
|
||||
* Load jQuery as early as possible to support custom scripts (#35926) (#35929)
|
||||
* Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage (#35882) (#35917)
|
||||
* Make OAuth2 issuer configurable (#35915) (#35916)
|
||||
* Fix #35763: Add proper page title for project pages (#35773) (#35909)
|
||||
* Fix avatar upload error handling (#35887) (#35890)
|
||||
* Contribution heatmap improvements (#35876) (#35880)
|
||||
* Remove padding override on `.ui .sha.label` (#35864) (#35873)
|
||||
* Fix pull description code label background (#35865) (#35870)
|
||||
|
||||
## [1.25.1](https://github.com/go-gitea/gitea/releases/tag/v1.25.1) - 2025-11-03
|
||||
|
||||
* BUGFIXES
|
||||
|
||||
@@ -567,6 +567,11 @@ ENABLED = true
|
||||
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
|
||||
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
|
||||
;;
|
||||
;; The "issuer" claim identifies the principal that issued the JWT.
|
||||
;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard.
|
||||
;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash".
|
||||
;JWT_CLAIM_ISSUER =
|
||||
;;
|
||||
;; Lifetime of an OAuth2 access token in seconds
|
||||
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
||||
;;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module code.gitea.io/gitea
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.4
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@@ -116,13 +116,13 @@ require (
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.4
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/text v0.30.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
google.golang.org/protobuf v1.36.8
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
@@ -279,9 +279,9 @@ require (
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -840,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -878,8 +878,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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -908,8 +908,8 @@ 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -932,8 +932,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -975,8 +975,8 @@ golang.org/x/sys v0.17.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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=
|
||||
@@ -987,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1002,8 +1002,8 @@ 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=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
@@ -1039,8 +1039,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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
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=
|
||||
|
||||
@@ -466,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c
|
||||
return currentWhitelist, nil
|
||||
}
|
||||
|
||||
prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
whitelist = make([]int64, 0, len(newWhitelist))
|
||||
for _, userID := range newWhitelist {
|
||||
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
|
||||
return nil, err
|
||||
} else if !reader {
|
||||
if !prUserIDs.Contains(userID) {
|
||||
continue
|
||||
}
|
||||
whitelist = append(whitelist, userID)
|
||||
|
||||
@@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
|
||||
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(teamIDs) == 0 {
|
||||
return teams, nil
|
||||
}
|
||||
err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams)
|
||||
return teams, err
|
||||
}
|
||||
|
||||
func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) {
|
||||
sub := builder.Select("team_id").From("team_unit").
|
||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||
|
||||
err := db.GetEngine(ctx).
|
||||
err = db.GetEngine(ctx).
|
||||
Select("team.id").
|
||||
Table("team").
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
|
||||
And(builder.Or(
|
||||
builder.Expr("team.authorize >= ?", mode),
|
||||
builder.In("team.id", sub),
|
||||
)).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
|
||||
return teams, err
|
||||
Find(&teamIDs)
|
||||
return teamIDs, err
|
||||
}
|
||||
|
||||
func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) {
|
||||
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(teamIDs) == 0 {
|
||||
return userIDs, nil
|
||||
}
|
||||
err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs)
|
||||
return userIDs, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -458,54 +459,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
|
||||
return perm.HasAnyUnitAccess(), nil
|
||||
}
|
||||
|
||||
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
|
||||
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) {
|
||||
userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
if len(userIDs) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Leave a seat for owner itself to append later, but if owner is an organization
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
users := make([]*user_model.User, 0, len(accesses)+1)
|
||||
if len(accesses) > 0 {
|
||||
userIDs := make([]int64, len(accesses))
|
||||
for i := 0; i < len(accesses); i++ {
|
||||
userIDs[i] = accesses[i].UserID
|
||||
}
|
||||
|
||||
if err = e.In("id", userIDs).Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
users = append(users, repo.Owner)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
|
||||
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
|
||||
}
|
||||
|
||||
// GetRepoWriters returns all users that have write access to the repository.
|
||||
func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
|
||||
}
|
||||
|
||||
// IsRepoReader returns true if user has explicit read access or higher to the repository.
|
||||
func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
|
||||
if repo.OwnerID == userID {
|
||||
return true, nil
|
||||
func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) {
|
||||
userIDs := container.Set[int64]{}
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
|
||||
for _, a := range accesses {
|
||||
userIDs.Add(a.UserID)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
userIDs.Add(repo.Owner.ID)
|
||||
} else {
|
||||
teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs.AddMultiple(teamUserIDs...)
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||
|
||||
@@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||
require.NoError(t, db.Insert(ctx, team))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
|
||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
@@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||
|
||||
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, user.ID, users[0].ID)
|
||||
|
||||
users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, users)
|
||||
})
|
||||
|
||||
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||
@@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
||||
|
||||
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, user.ID, users[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
@@ -77,7 +77,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := io.ReadAll(f)
|
||||
content, err := util.ReadWithLimit(f, 1024*1024)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -19,12 +19,17 @@ type TreeEntry struct {
|
||||
gogitTreeEntry *object.TreeEntry
|
||||
ptree *Tree
|
||||
|
||||
fullName string
|
||||
|
||||
size int64
|
||||
sized bool
|
||||
}
|
||||
|
||||
// Name returns the name of the entry
|
||||
func (te *TreeEntry) Name() string {
|
||||
if te.fullName != "" {
|
||||
return te.fullName
|
||||
}
|
||||
return te.gogitTreeEntry.Name
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
seen := map[plumbing.Hash]bool{}
|
||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
||||
for {
|
||||
_, entry, err := walker.Next()
|
||||
fullName, entry, err := walker.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
@@ -84,6 +84,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
ID: ParseGogitHash(entry.Hash),
|
||||
gogitTreeEntry: &entry,
|
||||
ptree: t,
|
||||
fullName: fullName,
|
||||
}
|
||||
entries = append(entries, convertedEntry)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
@@ -76,7 +75,7 @@ func unmarshalFromEntry(entry *git.TreeEntry, filename string) (*api.IssueTempla
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
content, err := io.ReadAll(r)
|
||||
content, err := util.ReadWithLimit(r, 1024*1024)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read all: %w", err)
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||
if p.Metadata.Readme != "" {
|
||||
f, err := archive.Open(p.Metadata.Readme)
|
||||
if err == nil {
|
||||
buf, _ := io.ReadAll(f)
|
||||
buf, _ := util.ReadWithLimit(f, 1024*1024)
|
||||
m.Readme = string(buf)
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.EqualFold(hd.Name, "readme.md") {
|
||||
data, err := io.ReadAll(tr)
|
||||
data, err := util.ReadWithLimit(tr, 1024*1024)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ var OAuth2 = struct {
|
||||
InvalidateRefreshTokens bool
|
||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
|
||||
MaxTokenLength int
|
||||
DefaultApplications []string
|
||||
}{
|
||||
|
||||
@@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error {
|
||||
func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
|
||||
blobClient := a.getBlobClient(path)
|
||||
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage
|
||||
startTime := time.Now()
|
||||
u, err := blobClient.GetSASURL(sas.BlobPermissions{
|
||||
Read: true,
|
||||
|
||||
@@ -279,20 +279,44 @@ func (m *MinioStorage) Delete(path string) error {
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
// copy serveDirectReqParams
|
||||
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
|
||||
reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
|
||||
|
||||
// Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head.
|
||||
// So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI.
|
||||
// Detect content type by extension name, only support the well-known safe types for inline rendering.
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
|
||||
ext := path.Ext(name)
|
||||
inlineExtMimeTypes := map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".avif": "image/avif",
|
||||
// ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline
|
||||
".pdf": "application/pdf",
|
||||
|
||||
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
|
||||
}
|
||||
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
|
||||
reqParams.Set("response-content-type", mimeType)
|
||||
reqParams.Set("response-content-disposition", "inline")
|
||||
} else {
|
||||
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)))
|
||||
}
|
||||
|
||||
expires := 5 * time.Minute
|
||||
if method == http.MethodHead {
|
||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
|
||||
// ReadWithLimit reads at most "limit" bytes from r into buf.
|
||||
// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil.
|
||||
func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) {
|
||||
return readWithLimit(r, 1024, n)
|
||||
return readWithLimit(r, 4*1024, n)
|
||||
}
|
||||
|
||||
func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) {
|
||||
|
||||
@@ -1481,6 +1481,7 @@ projects.column.new_submit = "Create Column"
|
||||
projects.column.new = "New Column"
|
||||
projects.column.set_default = "Set Default"
|
||||
projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
|
||||
projects.column.default_column_hint = "New issues added to this project will be added to this column"
|
||||
projects.column.delete = "Delete Column"
|
||||
projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
|
||||
projects.column.color = "Color"
|
||||
|
||||
@@ -148,7 +148,7 @@ func EnumeratePackages(ctx *context.Context) {
|
||||
Timestamp: fileMetadata.Timestamp,
|
||||
Build: fileMetadata.Build,
|
||||
BuildNumber: fileMetadata.BuildNumber,
|
||||
Dependencies: fileMetadata.Dependencies,
|
||||
Dependencies: util.SliceNilAsEmpty(fileMetadata.Dependencies),
|
||||
License: versionMetadata.License,
|
||||
LicenseFamily: versionMetadata.LicenseFamily,
|
||||
HashMD5: pfd.Blob.HashMD5,
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
@@ -260,6 +259,13 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// "docker buildx imagetools create" multi-arch operations:
|
||||
// {"type":"oci","is_tagged":false,"platform":"unknown/unknown"}
|
||||
// {"type":"oci","is_tagged":false,"platform":"linux/amd64","layer_creation":["ADD file:9233f6f2237d79659a9521f7e390df217cec49f1a8aa3a12147bbca1956acdb9 in /","CMD [\"/bin/sh\"]"]}
|
||||
// {"type":"oci","is_tagged":false,"platform":"unknown/unknown"}
|
||||
// {"type":"oci","is_tagged":false,"platform":"linux/arm64","layer_creation":["ADD file:df53811312284306901fdaaff0a357a4bf40d631e662fe9ce6d342442e494b6c in /","CMD [\"/bin/sh\"]"]}
|
||||
// {"type":"oci","is_tagged":true,"manifests":[{"platform":"linux/amd64","digest":"sha256:72bb73e706c0dec424d00a1febb21deaf1175a70ead009ad8b159729cfcf5769","size":2819478},{"platform":"linux/arm64","digest":"sha256:9e1426dd084a3221663b85ca1ee99d140c50b153917a5c5604c1f9b78229fd24","size":2716499},{"platform":"unknown/unknown","digest":"sha256:b93f03d0ae11b988243e1b2cd8d29accf5b9670547b7bd8c7d96abecc7283e6e","size":1798},{"platform":"unknown/unknown","digest":"sha256:f034b182ba66366c63a5d195c6dfcd3333c027409c0ac98e55ade36aaa3b2963","size":1798}]}
|
||||
|
||||
_pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: mci.Creator.ID,
|
||||
@@ -273,25 +279,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if container_module.IsMediaTypeImageIndex(mci.MediaType) {
|
||||
if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) {
|
||||
if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// keep download count on overwriting
|
||||
_pv.DownloadCount = pv.DownloadCount
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// keep download count on overwriting
|
||||
_pv.DownloadCount = pv.DownloadCount
|
||||
pv, err = packages_model.GetOrInsertVersion(ctx, _pv)
|
||||
if err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/activitypub"
|
||||
"code.gitea.io/gitea/routers/api/v1/admin"
|
||||
@@ -791,7 +792,9 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnauthorized, err)
|
||||
msg, ok := auth.ErrAsUserAuthMessage(err)
|
||||
msg = util.Iif(ok, msg, "invalid username, password or token")
|
||||
ctx.APIError(http.StatusUnauthorized, msg)
|
||||
return
|
||||
}
|
||||
ctx.Doer = ar.Doer
|
||||
|
||||
@@ -897,7 +897,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
whitelistUsers = protectBranch.WhitelistUserIDs
|
||||
}
|
||||
if form.ForcePushAllowlistDeployKeys != nil {
|
||||
if form.ForcePushAllowlistUsernames != nil {
|
||||
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
|
||||
@@ -369,11 +369,11 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
|
||||
},
|
||||
Signoff: commonOpts.Signoff,
|
||||
}
|
||||
if commonOpts.Dates.Author.IsZero() {
|
||||
commonOpts.Dates.Author = time.Now()
|
||||
if changeFileOpts.Dates.Author.IsZero() {
|
||||
changeFileOpts.Dates.Author = time.Now()
|
||||
}
|
||||
if commonOpts.Dates.Committer.IsZero() {
|
||||
commonOpts.Dates.Committer = time.Now()
|
||||
if changeFileOpts.Dates.Committer.IsZero() {
|
||||
changeFileOpts.Dates.Committer = time.Now()
|
||||
}
|
||||
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
|
||||
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -262,7 +262,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
|
||||
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
@@ -58,6 +59,13 @@ func GetReleaseByTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if release.IsDraft { // only the users with write access can see draft releases
|
||||
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -436,6 +436,7 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["Project"] = project
|
||||
ctx.Data["IssuesMap"] = issuesMap
|
||||
ctx.Data["Columns"] = columns
|
||||
ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName())
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
|
||||
@@ -147,7 +147,13 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
||||
// rely on the results of Contexter
|
||||
if !ctx.IsSigned {
|
||||
// TODO: support digit auth - which would be Authorization header with digit
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
if setting.OAuth2.Enabled {
|
||||
// `Basic realm="Gitea"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
} else {
|
||||
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea (Basic Auth)"`)
|
||||
}
|
||||
ctx.HTTPError(http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -206,12 +206,11 @@ func SoftDeleteContentHistory(ctx *context.Context) {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
if history.CommentID != commentID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
if commentID != 0 {
|
||||
if history.CommentID != commentID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
|
||||
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
||||
return
|
||||
|
||||
@@ -1208,7 +1208,11 @@ func MergePullRequest(ctx *context.Context) {
|
||||
func deleteBranchAfterMergeAndFlashMessage(ctx *context.Context, prID int64) {
|
||||
var fullBranchName string
|
||||
err := repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, prID, &fullBranchName)
|
||||
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
|
||||
// no need to show error to end users if no permission or branch not exist
|
||||
log.Debug("DeleteBranchAfterMerge (ignore unnecessary error): %v", err)
|
||||
return
|
||||
} else if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||
ctx.Flash.Error(errTr.Translate(ctx.Locale))
|
||||
return
|
||||
} else if err == nil {
|
||||
|
||||
@@ -73,10 +73,9 @@ func SettingsProtectedBranch(c *context.Context) {
|
||||
|
||||
c.Data["PageIsSettingsBranches"] = true
|
||||
c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
|
||||
|
||||
users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
|
||||
users, err := access_model.GetUsersWithUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
c.ServerError("Repo.Repository.GetReaders", err)
|
||||
c.ServerError("GetUsersWithUnitAccess", err)
|
||||
return
|
||||
}
|
||||
c.Data["Users"] = users
|
||||
|
||||
@@ -149,9 +149,9 @@ func setTagsContext(ctx *context.Context) error {
|
||||
}
|
||||
ctx.Data["ProtectedTags"] = protectedTags
|
||||
|
||||
users, err := access_model.GetRepoReaders(ctx, ctx.Repo.Repository)
|
||||
users, err := access_model.GetUsersWithUnitAccess(ctx, ctx.Repo.Repository, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
ctx.ServerError("Repo.Repository.GetReaders", err)
|
||||
ctx.ServerError("GetUsersWithUnitAccess", err)
|
||||
return err
|
||||
}
|
||||
ctx.Data["Users"] = users
|
||||
|
||||
@@ -95,6 +95,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b
|
||||
|
||||
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
|
||||
if err != nil { // fallback to a plain file
|
||||
fi.lfsMeta = &pointer
|
||||
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
|
||||
return buf, dataRc, fi, nil
|
||||
}
|
||||
|
||||
@@ -92,8 +92,6 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy
|
||||
ctx.ServerError("Render", err)
|
||||
return true
|
||||
}
|
||||
// to prevent iframe from loading third-party url
|
||||
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -241,14 +239,17 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
|
||||
// * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
|
||||
// * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
|
||||
|
||||
utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
contentReader := io.MultiReader(bytes.NewReader(buf), dataRc)
|
||||
if fInfo.st.IsRepresentableAsText() {
|
||||
contentReader = charset.ToUTF8WithFallbackReader(contentReader, charset.ConvertOpts{})
|
||||
}
|
||||
switch {
|
||||
case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize:
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader):
|
||||
case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, contentReader):
|
||||
// it also sets ctx.Data["FileContent"] and more
|
||||
ctx.Data["IsMarkup"] = true
|
||||
case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader):
|
||||
case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, contentReader):
|
||||
// it also sets ctx.Data["FileContent"] and more
|
||||
ctx.Data["IsDisplayingSource"] = true
|
||||
case handleFileViewRenderImage(ctx, fInfo, buf):
|
||||
|
||||
@@ -133,7 +133,7 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
|
||||
return nil
|
||||
}
|
||||
defer reader.Close()
|
||||
content, err := io.ReadAll(reader)
|
||||
content, err := util.ReadWithLimit(reader, 5*1024*1024) // 5MB should be enough for a wiki page
|
||||
if err != nil {
|
||||
ctx.ServerError("ReadAll", err)
|
||||
return nil
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -40,6 +41,20 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
}
|
||||
})
|
||||
|
||||
type ErrUserAuthMessage string
|
||||
|
||||
func (e ErrUserAuthMessage) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func ErrAsUserAuthMessage(err error) (string, bool) {
|
||||
var msg ErrUserAuthMessage
|
||||
if errors.As(err, &msg) {
|
||||
return msg.Error(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Init should be called exactly once when the application starts to allow plugins
|
||||
// to allocate necessary resources
|
||||
func Init() {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
@@ -146,7 +145,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||
return nil, err
|
||||
}
|
||||
if hasWebAuthn {
|
||||
return nil, errors.New("basic authorization is not allowed while WebAuthn enrolled")
|
||||
return nil, ErrUserAuthMessage("basic authorization is not allowed while WebAuthn enrolled")
|
||||
}
|
||||
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
|
||||
@@ -105,9 +105,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
}
|
||||
}
|
||||
if source.AttributeAvatar != "" {
|
||||
if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil {
|
||||
return user, err
|
||||
}
|
||||
_ = user_service.UploadAvatar(ctx, user, sr.Avatar)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T,
|
||||
|
||||
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
|
||||
func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection {
|
||||
readers, err := access_model.GetRepoReaders(ctx, repo)
|
||||
readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
log.Error("GetRepoReaders: %v", err)
|
||||
}
|
||||
@@ -542,8 +542,9 @@ func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerifi
|
||||
}
|
||||
if verif.SigningUser != nil {
|
||||
commitVerification.Signer = &api.PayloadUser{
|
||||
Name: verif.SigningUser.Name,
|
||||
Email: verif.SigningUser.Email,
|
||||
UserName: verif.SigningUser.Name,
|
||||
Name: verif.SigningUser.DisplayName(),
|
||||
Email: verif.SigningEmail, // Use the email from the signature, not from the user profile
|
||||
}
|
||||
}
|
||||
return commitVerification
|
||||
@@ -720,7 +721,7 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.
|
||||
|
||||
// ToTagProtection convert a git.ProtectedTag to an api.TagProtection
|
||||
func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection {
|
||||
readers, err := access_model.GetRepoReaders(ctx, repo)
|
||||
readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
log.Error("GetRepoReaders: %v", err)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package issue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/issue/template"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -65,7 +65,7 @@ func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit)
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
configContent, err := io.ReadAll(reader)
|
||||
configContent, err := util.ReadWithLimit(reader, 1024*1024)
|
||||
if err != nil {
|
||||
return GetDefaultTemplateConfig(), err
|
||||
}
|
||||
|
||||
@@ -112,8 +112,12 @@ func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt
|
||||
// to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer.
|
||||
// * https://accounts.google.com/.well-known/openid-configuration
|
||||
// * https://github.com/login/oauth/.well-known/openid-configuration
|
||||
issuer := setting.OAuth2.JWTClaimIssuer
|
||||
if issuer == "" {
|
||||
issuer = strings.TrimSuffix(setting.AppURL, "/")
|
||||
}
|
||||
return jwt.RegisteredClaims{
|
||||
Issuer: strings.TrimSuffix(setting.AppURL, "/"),
|
||||
Issuer: issuer,
|
||||
Audience: []string{clientID},
|
||||
Subject: strconv.FormatInt(grantUserID, 10),
|
||||
ExpiresAt: exp,
|
||||
|
||||
@@ -546,11 +546,15 @@ var escapedSymbols = regexp.MustCompile(`([*[?! \\])`)
|
||||
|
||||
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
|
||||
func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {
|
||||
return isUserAllowedToMergeInRepoBranch(ctx, pr.BaseRepoID, pr.BaseBranch, p, user)
|
||||
}
|
||||
|
||||
func isUserAllowedToMergeInRepoBranch(ctx context.Context, repoID int64, branch string, p access_model.Permission, user *user_model.User) (bool, error) {
|
||||
if user == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repoID, branch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
|
||||
}
|
||||
cmdCommit := gitcmd.NewCommand("commit").
|
||||
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
|
||||
AddOptionFormat("--message=%s", message)
|
||||
AddOptionFormat("--message=%s", message).
|
||||
AddArguments("--allow-empty")
|
||||
if ctx.signKey == nil {
|
||||
cmdCommit.AddArguments("--no-gpg-sign")
|
||||
} else {
|
||||
|
||||
+27
-32
@@ -97,11 +97,11 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
}
|
||||
|
||||
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
|
||||
// update PR means send new commits to PR head branch from base branch
|
||||
func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
|
||||
if pull.Flow == issues_model.PullRequestFlowAGit {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return false, false, nil
|
||||
}
|
||||
@@ -117,54 +117,46 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
pr := &issues_model.PullRequest{
|
||||
HeadRepoID: pull.BaseRepoID,
|
||||
HeadRepo: pull.BaseRepo,
|
||||
BaseRepoID: pull.HeadRepoID,
|
||||
BaseRepo: pull.HeadRepo,
|
||||
HeadBranch: pull.BaseBranch,
|
||||
BaseBranch: pull.HeadBranch,
|
||||
}
|
||||
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
// 1. check base repository's AllowRebaseUpdate configuration
|
||||
// it is a config in base repo but controls the head (fork) repo's "Update" behavior
|
||||
{
|
||||
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
return false, false, nil
|
||||
return false, false, nil // the PR unit is disabled in base repo
|
||||
} else if err != nil {
|
||||
return false, false, fmt.Errorf("get base repo unit: %v", err)
|
||||
}
|
||||
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
|
||||
return false, false, err
|
||||
rebaseAllowed = prBaseUnit.PullRequestsConfig().AllowRebaseUpdate
|
||||
}
|
||||
|
||||
rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate
|
||||
|
||||
// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
|
||||
if pb != nil {
|
||||
pb.Repo = pull.BaseRepo
|
||||
if !pb.CanUserForcePush(ctx, user) {
|
||||
rebaseAllowed = false
|
||||
// 2. check head branch protection whether rebase is allowed, if pb not found then rebase depends on the above setting
|
||||
{
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.HeadRepoID, pull.HeadBranch)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
|
||||
if pb != nil {
|
||||
pb.Repo = pull.HeadRepo
|
||||
rebaseAllowed = rebaseAllowed && pb.CanUserForcePush(ctx, user)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. check whether user has write access to head branch
|
||||
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
mergeAllowed, err = IsUserAllowedToMerge(ctx, pr, headRepoPerm, user)
|
||||
mergeAllowed, err = isUserAllowedToMergeInRepoBranch(ctx, pull.HeadRepoID, pull.HeadBranch, headRepoPerm, user)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
// 4. if the pull creator allows maintainer to edit, it means the write permissions of the head branch has been
|
||||
// granted to the user with write permission of the base repository
|
||||
if pull.AllowMaintainerEdit {
|
||||
mergeAllowedMaintainer, err := IsUserAllowedToMerge(ctx, pr, baseRepoPerm, user)
|
||||
mergeAllowedMaintainer, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
@@ -172,6 +164,9 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
|
||||
mergeAllowed = mergeAllowed || mergeAllowedMaintainer
|
||||
}
|
||||
|
||||
// if merge is not allowed, rebase is also not allowed
|
||||
rebaseAllowed = rebaseAllowed && mergeAllowed
|
||||
|
||||
return mergeAllowed, rebaseAllowed, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetProtectedTags: %w", err)
|
||||
}
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -138,31 +140,37 @@ func (gt *giteaTemplateFileMatcher) Match(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) {
|
||||
localPath := filepath.Join(tmpDir, ".gitea", "template")
|
||||
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
func readLocalTmpRepoFileContent(localPath string, limit int) ([]byte, error) {
|
||||
ok, err := util.IsRegularFile(localPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(localPath)
|
||||
f, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return util.ReadWithLimit(f, limit)
|
||||
}
|
||||
|
||||
func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) {
|
||||
localPath := filepath.Join(tmpDir, ".gitea", "template")
|
||||
content, err := readLocalTmpRepoFileContent(localPath, 1024*1024)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newGiteaTemplateFileMatcher(localPath, content), nil
|
||||
}
|
||||
|
||||
func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error {
|
||||
tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath)
|
||||
if ok, err := util.IsRegularFile(tmpFullPath); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(tmpFullPath)
|
||||
content, err := readLocalTmpRepoFileContent(tmpFullPath, 1024*1024)
|
||||
if err != nil {
|
||||
return err
|
||||
return util.Iif(errors.Is(err, fs.ErrNotExist), nil, err)
|
||||
}
|
||||
if err := util.Remove(tmpFullPath); err != nil {
|
||||
return err
|
||||
@@ -172,7 +180,7 @@ func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, t
|
||||
substSubPath := filepath.Clean(filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo)))
|
||||
newLocalPath := filepath.Join(tmpDir, substSubPath)
|
||||
regular, err := util.IsRegularFile(newLocalPath)
|
||||
if canWrite := regular || os.IsNotExist(err); !canWrite {
|
||||
if canWrite := regular || errors.Is(err, fs.ErrNotExist); !canWrite {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil {
|
||||
@@ -242,15 +250,15 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
|
||||
|
||||
// Variable expansion
|
||||
fileMatcher, err := readGiteaTemplateFile(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("readGiteaTemplateFile: %w", err)
|
||||
}
|
||||
|
||||
if fileMatcher != nil {
|
||||
if err == nil {
|
||||
err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("processGiteaTemplateFile: %w", err)
|
||||
}
|
||||
} else if errors.Is(err, fs.ErrNotExist) {
|
||||
log.Debug("skip processing repo template files: no available .gitea/template")
|
||||
} else {
|
||||
return fmt.Errorf("readGiteaTemplateFile: %w", err)
|
||||
}
|
||||
|
||||
if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -175,6 +176,31 @@ func TestProcessGiteaTemplateFile(t *testing.T) {
|
||||
// subst from a link, skip, and the target is unchanged
|
||||
assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target")
|
||||
}
|
||||
|
||||
{
|
||||
templateFilePath := tmpDir + "/.gitea/template"
|
||||
|
||||
_ = os.Remove(templateFilePath)
|
||||
_, err := os.Lstat(templateFilePath)
|
||||
require.ErrorIs(t, err, fs.ErrNotExist)
|
||||
_, err = readGiteaTemplateFile(tmpDir) // no template file
|
||||
require.ErrorIs(t, err, fs.ErrNotExist)
|
||||
|
||||
_ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644)
|
||||
_ = os.Symlink(templateFilePath+".target", templateFilePath)
|
||||
content, _ := os.ReadFile(templateFilePath)
|
||||
require.Equal(t, "test-data-target", string(content))
|
||||
_, err = readGiteaTemplateFile(tmpDir) // symlinked template file
|
||||
require.ErrorIs(t, err, fs.ErrNotExist)
|
||||
|
||||
_ = os.Remove(templateFilePath)
|
||||
_ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644)
|
||||
content, _ = os.ReadFile(templateFilePath)
|
||||
require.Equal(t, "test-data-regular", string(content))
|
||||
fm, err := readGiteaTemplateFile(tmpDir) // regular template file
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, fm.globs, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformers(t *testing.T) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
)
|
||||
|
||||
@@ -264,7 +265,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
|
||||
t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
|
||||
}
|
||||
|
||||
p, err := io.ReadAll(resp.Body)
|
||||
p, err := util.ReadWithLimit(resp.Body, 1024*1024)
|
||||
if err != nil {
|
||||
t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
|
||||
return fmt.Errorf("unable to deliver webhook task[%d] in %s as unable to read response body: %w", t.ID, w.URL, err)
|
||||
|
||||
@@ -78,7 +78,9 @@
|
||||
<div class="ui circular label project-column-issue-count">
|
||||
{{.NumIssues}}
|
||||
</div>
|
||||
<div class="project-column-title-text gt-ellipsis">{{.Title}}</div>
|
||||
<div class="project-column-title-text flex-text-inline gt-ellipsis" {{if .Default}}data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.default_column_hint"}}"{{end}}>
|
||||
{{if .Default}}{{svg "octicon-star"}} {{end}}{{.Title}}
|
||||
</div>
|
||||
{{if $canWriteProject}}
|
||||
<div class="ui dropdown tw-p-1">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
@@ -98,7 +100,7 @@
|
||||
data-modal-confirm-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}"
|
||||
data-modal-confirm-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}"
|
||||
>
|
||||
{{svg "octicon-pin"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}}
|
||||
{{svg "octicon-star"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}}
|
||||
</a>
|
||||
<a class="item button link-action" data-url="{{$.Link}}/{{.ID}}" data-link-action-method="DELETE"
|
||||
data-modal-confirm-header="{{ctx.Locale.Tr "repo.projects.column.delete"}}"
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div class="flex-item-title">
|
||||
<a class="item muted" href="{{.RepoLink}}/releases">
|
||||
{{ctx.Locale.Tr "repo.releases"}}
|
||||
<span class="ui small label">{{.NumReleases}}</span>
|
||||
</a>
|
||||
<span class="ui small label">{{.NumReleases}}</span>
|
||||
</div>
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-leading">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">{{ctx.Locale.Tr "repo.repo_desc"}}</div>
|
||||
<div class="flex-item-body tw-text-16">
|
||||
<div class="flex-item-body tw-text-15">
|
||||
<div class="tw-flex tw-flex-col tw-gap-2 tw-mt-2">
|
||||
<div class="repo-description tw-break-anywhere tw-gap-2">
|
||||
{{- $description := .Repository.DescriptionHTML ctx -}}
|
||||
|
||||
@@ -78,18 +78,6 @@
|
||||
{{ctx.Locale.Tr "repo.release.downloads"}}
|
||||
</summary>
|
||||
<ul class="ui divided list attachment-list">
|
||||
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
|
||||
<li class="item">
|
||||
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow">
|
||||
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong>
|
||||
</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">
|
||||
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{range $att := $release.Attachments}}
|
||||
<li class="item">
|
||||
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
|
||||
@@ -105,6 +93,18 @@
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
|
||||
<li class="item">
|
||||
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow">
|
||||
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong>
|
||||
</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">
|
||||
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{{range $result := .SearchResults}}
|
||||
{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
|
||||
<div class="diff-file-box file-content non-diff-file-content repo-search-result">
|
||||
<h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap">
|
||||
<h4 class="ui top attached header tw-font-normal flex-text-block tw-flex-wrap tw-py-2">
|
||||
{{if not $.Repo}}
|
||||
<span class="file tw-flex-1">
|
||||
<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{if .HeatmapData}}
|
||||
<div class="activity-heatmap-container">
|
||||
<div id="user-heatmap" class="is-loading"
|
||||
data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}"
|
||||
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" (ctx.Locale.PrettyNumber .HeatmapTotalContributions)}}"
|
||||
@@ -6,5 +7,6 @@
|
||||
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
|
||||
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"
|
||||
></div>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ref: refs/heads/master
|
||||
@@ -1,6 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
@@ -1 +0,0 @@
|
||||
The repository will be used to test third-party renderer in TestExternalMarkupRenderer
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
data=$(cat)
|
||||
exitcodes=""
|
||||
hookname=$(basename $0)
|
||||
GIT_DIR=${GIT_DIR:-$(dirname $0)}
|
||||
|
||||
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
|
||||
test -x "${hook}" && test -f "${hook}" || continue
|
||||
echo "${data}" | "${hook}"
|
||||
exitcodes="${exitcodes} $?"
|
||||
done
|
||||
|
||||
for i in ${exitcodes}; do
|
||||
[ ${i} -eq 0 ] || exit ${i}
|
||||
done
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
data=$(cat)
|
||||
exitcodes=""
|
||||
hookname=$(basename $0)
|
||||
GIT_DIR=${GIT_DIR:-$(dirname $0)}
|
||||
|
||||
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
|
||||
test -x "${hook}" && test -f "${hook}" || continue
|
||||
echo "${data}" | "${hook}"
|
||||
exitcodes="${exitcodes} $?"
|
||||
done
|
||||
|
||||
for i in ${exitcodes}; do
|
||||
[ ${i} -eq 0 ] || exit ${i}
|
||||
done
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
exitcodes=""
|
||||
hookname=$(basename $0)
|
||||
GIT_DIR=${GIT_DIR:-$(dirname $0)}
|
||||
|
||||
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
|
||||
test -x "${hook}" && test -f "${hook}" || continue
|
||||
"${hook}" $1 $2 $3
|
||||
exitcodes="${exitcodes} $?"
|
||||
done
|
||||
|
||||
for i in ${exitcodes}; do
|
||||
[ ${i} -eq 0 ] || exit ${i}
|
||||
done
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
# pack-refs with: peeled fully-peeled sorted
|
||||
c961cc4d1ba6b7ee1ba228a9a02b00b7746d8033 refs/heads/master
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIAuth(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2", "wrong-password")
|
||||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user-not-exist")
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth("Bearer wrong_token")
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func enableRepoDependencies(t *testing.T, repoID int64) {
|
||||
t.Helper()
|
||||
|
||||
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
|
||||
repoUnit.IssuesConfig().EnableDependencies = true
|
||||
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
|
||||
}
|
||||
|
||||
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
|
||||
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.True(t, dependencyRepo.IsPrivate)
|
||||
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
|
||||
|
||||
enableRepoDependencies(t, targetIssue.RepoID)
|
||||
enableRepoDependencies(t, dependencyRepo.ID)
|
||||
|
||||
// remove user 40 access from target repository
|
||||
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
|
||||
assert.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
|
||||
dependencyMeta := &api.IssueMeta{
|
||||
Owner: "org3",
|
||||
Name: "repo3",
|
||||
Index: dependencyIssue.Index,
|
||||
}
|
||||
|
||||
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
||||
// user40 has no access to both target issue and dependency issue
|
||||
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestWithJSON(t, "POST", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to dependency repository with read permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
|
||||
|
||||
// try again after getting read permission to dependency repository
|
||||
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to target repository with write permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIDeleteIssueDependencyCrossRepoPermission(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
|
||||
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.True(t, dependencyRepo.IsPrivate)
|
||||
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
|
||||
|
||||
enableRepoDependencies(t, targetIssue.RepoID)
|
||||
enableRepoDependencies(t, dependencyRepo.ID)
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
|
||||
|
||||
// remove user 40 access from target repository
|
||||
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
|
||||
assert.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
|
||||
dependencyMeta := &api.IssueMeta{
|
||||
Owner: "org3",
|
||||
Name: "repo3",
|
||||
Index: dependencyIssue.Index,
|
||||
}
|
||||
|
||||
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
||||
// user40 has no access to both target issue and dependency issue
|
||||
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to dependency repository with read permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
|
||||
|
||||
// try again after getting read permission to dependency repository
|
||||
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to target repository with write permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
|
||||
|
||||
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
}
|
||||
@@ -237,6 +237,8 @@ func TestPackageConda(t *testing.T) {
|
||||
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5)
|
||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256)
|
||||
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size)
|
||||
assert.NotNil(t, packageInfo.Dependencies)
|
||||
assert.Empty(t, packageInfo.Dependencies)
|
||||
})
|
||||
|
||||
t.Run(".conda", func(t *testing.T) {
|
||||
@@ -268,6 +270,8 @@ func TestPackageConda(t *testing.T) {
|
||||
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5)
|
||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256)
|
||||
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size)
|
||||
assert.NotNil(t, packageInfo.Dependencies)
|
||||
assert.Empty(t, packageInfo.Dependencies)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPackageContainer(t *testing.T) {
|
||||
@@ -70,13 +71,12 @@ func TestPackageContainer(t *testing.T) {
|
||||
|
||||
manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6"
|
||||
manifestContent := `{"schemaVersion":2,"mediaType":"` + container_module.ContentTypeDockerDistributionManifestV2 + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
|
||||
manifestContentType := container_module.ContentTypeDockerDistributionManifestV2
|
||||
|
||||
untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d"
|
||||
untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
|
||||
|
||||
indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec"
|
||||
indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}`
|
||||
indexManifestDigest := "sha256:2c6b5afb967d5de02795ee1d177c3746d005df4b4c2b829385b0d186b3414b6b"
|
||||
indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","is_tagged":true,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}`
|
||||
|
||||
anonymousToken := ""
|
||||
userToken := ""
|
||||
@@ -467,15 +467,16 @@ func TestPackageContainer(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pv.DownloadCount)
|
||||
|
||||
// Overwrite existing tag should keep the download count
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", oci.MediaTypeImageManifest)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
t.Run("OverwriteTagKeepDownloadCount", func(t *testing.T) {
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", oci.MediaTypeImageManifest)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
pv, err = packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, tag)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pv.DownloadCount)
|
||||
pv, err = packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, tag)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pv.DownloadCount)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HeadManifest", func(t *testing.T) {
|
||||
@@ -505,7 +506,7 @@ func TestPackageContainer(t *testing.T) {
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Equal(t, strconv.Itoa(len(manifestContent)), resp.Header().Get("Content-Length"))
|
||||
assert.Equal(t, manifestContentType, resp.Header().Get("Content-Type"))
|
||||
assert.Equal(t, oci.MediaTypeImageManifest, resp.Header().Get("Content-Type")) // the manifest is overwritten by above OverwriteTagKeepDownloadCount
|
||||
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||
assert.Equal(t, manifestContent, resp.Body.String())
|
||||
})
|
||||
@@ -599,6 +600,17 @@ func TestPackageContainer(t *testing.T) {
|
||||
assert.True(t, pd.Files[0].File.IsLead)
|
||||
assert.Equal(t, oci.MediaTypeImageIndex, pd.Files[0].Properties.GetByName(container_module.PropertyMediaType))
|
||||
assert.Equal(t, indexManifestDigest, pd.Files[0].Properties.GetByName(container_module.PropertyDigest))
|
||||
|
||||
lastPackageVersionID := pv.ID
|
||||
t.Run("UploadAgain", func(t *testing.T) {
|
||||
req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, multiTag), strings.NewReader(indexManifestContent)).
|
||||
AddTokenAuth(userToken).
|
||||
SetHeader("Content-Type", oci.MediaTypeImageIndex)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, multiTag)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, lastPackageVersionID, pv.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HeadBlob", func(t *testing.T) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -269,6 +271,42 @@ func TestAPIGetReleaseByTag(t *testing.T) {
|
||||
assert.NotEmpty(t, err.Message)
|
||||
}
|
||||
|
||||
func TestAPIGetDraftReleaseByTag(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
tag := "draft-release"
|
||||
// anonymous should not be able to get draft release
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag))
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// user 40 should be able to get draft release because he has write access to the repository
|
||||
token := getUserToken(t, "user40", auth_model.AccessTokenScopeReadRepository)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
release := api.Release{}
|
||||
DecodeJSON(t, resp, &release)
|
||||
assert.Equal(t, "draft-release", release.Title)
|
||||
|
||||
// remove user 40 access from the repository
|
||||
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// user 40 should not be able to get draft release
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// user 2 should be able to get draft release because he is the publisher
|
||||
user2Token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(user2Token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
release = api.Release{}
|
||||
DecodeJSON(t, resp, &release)
|
||||
assert.Equal(t, "draft-release", release.Title)
|
||||
}
|
||||
|
||||
func TestAPIDeleteReleaseByTagName(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
||||
@@ -41,17 +41,6 @@ func TestAPIUserReposNotLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIUserReposWithWrongToken(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
wrongToken := "Bearer " + "wrong_token"
|
||||
req := NewRequestf(t, "GET", "/api/v1/users/%s/repos", user.Name).
|
||||
AddTokenAuth(wrongToken)
|
||||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
assert.Contains(t, resp.Body.String(), "user does not exist")
|
||||
}
|
||||
|
||||
func TestAPISearchRepo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
const keyword = "test"
|
||||
|
||||
@@ -271,8 +271,8 @@ type RequestWrapper struct {
|
||||
*http.Request
|
||||
}
|
||||
|
||||
func (req *RequestWrapper) AddBasicAuth(username string) *RequestWrapper {
|
||||
req.Request.SetBasicAuth(username, userPassword)
|
||||
func (req *RequestWrapper) AddBasicAuth(username string, password ...string) *RequestWrapper {
|
||||
req.Request.SetBasicAuth(username, util.OptionalArg(password, userPassword))
|
||||
return req
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/external"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -25,29 +26,45 @@ import (
|
||||
func TestExternalMarkupRenderer(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
t.Skip("only SQLite3 test config supports external markup renderer")
|
||||
return
|
||||
}
|
||||
|
||||
const binaryContentPrefix = "any prefix text."
|
||||
const binaryContent = binaryContentPrefix + "\xfe\xfe\xfe\x00\xff\xff"
|
||||
detectedEncoding, _ := charset.DetectEncoding([]byte(binaryContent))
|
||||
assert.NotEqual(t, binaryContent, strings.ToValidUTF8(binaryContent, "?"))
|
||||
assert.Equal(t, "ISO-8859-2", detectedEncoding) // even if the binary content can be detected as text encoding, it shouldn't affect the raw rendering
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`)
|
||||
_, err := createFileInBranch(user2, repo1, createFileInBranchOptions{}, map[string]string{
|
||||
"test.html": `<div><any attr="val"><script></script></div>`,
|
||||
"html.no-sanitizer": `<script>foo("raw")</script>`,
|
||||
"bin.no-sanitizer": binaryContent,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("RenderNoSanitizer", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer")
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/html.no-sanitizer")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
div := doc.Find("div.file-view")
|
||||
div := NewHTMLParser(t, resp.Body).Find("div.file-view")
|
||||
data, err := div.Html()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<script>window.alert("hi")</script>`, strings.TrimSpace(data))
|
||||
assert.Equal(t, `<script>foo("raw")</script>`, strings.TrimSpace(data))
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/repo1/src/branch/master/bin.no-sanitizer")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
div = NewHTMLParser(t, resp.Body).Find("div.file-view")
|
||||
data, err = div.Html()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ReplaceAll(binaryContent, "\x00", ""), strings.TrimSpace(data)) // HTML template engine removes the null bytes
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("RenderContentDirectly", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/test.html")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
|
||||
@@ -55,18 +72,21 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
||||
div := doc.Find("div.file-view")
|
||||
data, err := div.Html()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
||||
// the content is fully sanitized
|
||||
assert.Equal(t, `<div><script></script></div>`, strings.TrimSpace(data))
|
||||
})
|
||||
|
||||
// above tested "no-sanitizer" mode, then we test iframe mode below
|
||||
// above tested in-page rendering (no iframe), then we test iframe mode below
|
||||
r := markup.GetRendererByFileName("any-file.html").(*external.Renderer)
|
||||
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
|
||||
assert.True(t, r.NeedPostProcess())
|
||||
r = markup.GetRendererByFileName("any-file.no-sanitizer").(*external.Renderer)
|
||||
defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
|
||||
assert.False(t, r.NeedPostProcess())
|
||||
|
||||
t.Run("RenderContentInIFrame", func(t *testing.T) {
|
||||
t.Run("DefaultSandbox", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/test.html")
|
||||
|
||||
t.Run("ParentPage", func(t *testing.T) {
|
||||
respParent := MakeRequest(t, req, http.StatusOK)
|
||||
@@ -77,31 +97,42 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
||||
|
||||
// default sandbox on parent page
|
||||
assert.Equal(t, "allow-scripts allow-popups", iframe.AttrOr("sandbox", ""))
|
||||
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("data-src", ""))
|
||||
assert.Equal(t, "/user2/repo1/render/branch/master/test.html", iframe.AttrOr("data-src", ""))
|
||||
})
|
||||
t.Run("SubPage", func(t *testing.T) {
|
||||
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
|
||||
req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/test.html")
|
||||
respSub := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/html; charset=utf-8", respSub.Header().Get("Content-Type"))
|
||||
|
||||
// default sandbox in sub page response
|
||||
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy"))
|
||||
assert.Equal(t, "<script src=\"/assets/js/external-render-iframe.js\"></script><link rel=\"stylesheet\" href=\"/assets/css/external-render-iframe.css\"><div>\n\ttest external renderer\n</div>\n", respSub.Body.String())
|
||||
// FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "<script>" tag, but it indeed is the sanitizer's job
|
||||
assert.Equal(t, `<script src="/assets/js/external-render-iframe.js"></script><link rel="stylesheet" href="/assets/css/external-render-iframe.css"><div><any attr="val"><script></script></any></div>`, respSub.Body.String())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("NoSanitizerNoSandbox", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer")
|
||||
respParent := MakeRequest(t, req, http.StatusOK)
|
||||
iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe")
|
||||
assert.Equal(t, "/user2/repo1/render/branch/master/file.no-sanitizer", iframe.AttrOr("data-src", ""))
|
||||
t.Run("BinaryContent", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/bin.no-sanitizer")
|
||||
respParent := MakeRequest(t, req, http.StatusOK)
|
||||
iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe")
|
||||
assert.Equal(t, "/user2/repo1/render/branch/master/bin.no-sanitizer", iframe.AttrOr("data-src", ""))
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/file.no-sanitizer")
|
||||
respSub := MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/bin.no-sanitizer")
|
||||
respSub := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, binaryContent, respSub.Body.String()) // raw content should keep the raw bytes (including invalid UTF-8 bytes), and no "external-render-iframe" helpers
|
||||
|
||||
// no sandbox (disabled by RENDER_CONTENT_SANDBOX)
|
||||
assert.Empty(t, iframe.AttrOr("sandbox", ""))
|
||||
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
|
||||
// no sandbox (disabled by RENDER_CONTENT_SANDBOX)
|
||||
assert.Empty(t, iframe.AttrOr("sandbox", ""))
|
||||
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
|
||||
t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer")
|
||||
respSub := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, `<script src="/assets/js/external-render-iframe.js"></script><link rel="stylesheet" href="/assets/css/external-render-iframe.css"><script>foo("raw")</script>`, respSub.Body.String())
|
||||
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -919,20 +919,32 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
|
||||
}
|
||||
|
||||
func testOAuth2WellKnown(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
|
||||
urlOpenidConfiguration := "/.well-known/openid-configuration"
|
||||
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
|
||||
req := NewRequest(t, "GET", urlOpenidConfiguration)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var respMap map[string]any
|
||||
DecodeJSON(t, resp, &respMap)
|
||||
assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"])
|
||||
assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
|
||||
t.Run("WellKnown", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", urlOpenidConfiguration)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var respMap map[string]any
|
||||
DecodeJSON(t, resp, &respMap)
|
||||
assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"])
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"])
|
||||
assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
|
||||
})
|
||||
|
||||
t.Run("WellKnownWithIssuer", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTClaimIssuer, "https://try.gitea.io/")()
|
||||
req := NewRequest(t, "GET", urlOpenidConfiguration)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var respMap map[string]any
|
||||
DecodeJSON(t, resp, &respMap)
|
||||
assert.Equal(t, "https://try.gitea.io/", respMap["issuer"]) // has trailing by JWTClaimIssuer
|
||||
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
|
||||
})
|
||||
|
||||
defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
|
||||
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
|
||||
|
||||
@@ -1110,3 +1110,33 @@ func TestPullNonMergeForAdminWithBranchProtection(t *testing.T) {
|
||||
session.MakeRequest(t, mergeReq, http.StatusMethodNotAllowed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullSquashMergeEmpty(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "pr-squash-empty", "README.md", "Hello, World (Edited)\n")
|
||||
resp := testPullCreate(t, session, "user2", "repo1", false, "master", "pr-squash-empty", "This is a pull title")
|
||||
|
||||
elem := strings.Split(test.RedirectURL(resp), "/")
|
||||
assert.Equal(t, "pulls", elem[3])
|
||||
|
||||
httpContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
|
||||
dstPath := t.TempDir()
|
||||
|
||||
u.Path = httpContext.GitPath()
|
||||
u.User = url.UserPassword("user2", userPassword)
|
||||
|
||||
t.Run("Clone", doGitClone(dstPath, u))
|
||||
doGitCheckoutBranch(dstPath, "-b", "pr-squash-empty", "remotes/origin/pr-squash-empty")(t)
|
||||
doGitCheckoutBranch(dstPath, "master")(t)
|
||||
_, _, err := gitcmd.NewCommand("cherry-pick").AddArguments("pr-squash-empty").
|
||||
RunStdString(t.Context(), &gitcmd.RunOpts{
|
||||
Dir: dstPath,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
doGitPushTestRepository(dstPath)(t)
|
||||
|
||||
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@ import (
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
@@ -50,6 +54,14 @@ func TestAPIPullUpdate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func enableRepoAllowUpdateWithRebase(t *testing.T, repoID int64, allow bool) {
|
||||
t.Helper()
|
||||
|
||||
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
|
||||
repoUnit.PullRequestsConfig().AllowRebaseUpdate = allow
|
||||
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
|
||||
}
|
||||
|
||||
func TestAPIPullUpdateByRebase(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
// Create PR to test
|
||||
@@ -65,10 +77,32 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
|
||||
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
assert.NoError(t, pr.LoadIssue(t.Context()))
|
||||
|
||||
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
|
||||
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||
|
||||
// use a user which have write access to the pr but not write permission to the head repository to do the rebase
|
||||
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
||||
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite)
|
||||
assert.NoError(t, err)
|
||||
token40 := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token40)
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token40)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Test GetDiverging after update
|
||||
@@ -79,6 +113,49 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIPullUpdateByRebase2(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
// Create PR to test
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
||||
pr := createOutdatedPR(t, user, org26)
|
||||
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
assert.NoError(t, pr.LoadIssue(t.Context()))
|
||||
|
||||
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
|
||||
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||
|
||||
// add a protected branch rule to the head branch to block rebase
|
||||
pb := git_model.ProtectedBranch{
|
||||
RepoID: pr.HeadRepo.ID,
|
||||
RuleName: pr.HeadBranch,
|
||||
CanPush: false,
|
||||
CanForcePush: false,
|
||||
}
|
||||
err := git_model.UpdateProtectBranch(t.Context(), pr.HeadRepo, &pb, git_model.WhitelistOptions{})
|
||||
assert.NoError(t, err)
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// remove the protected branch rule to allow rebase
|
||||
err = git_model.DeleteProtectedBranch(t.Context(), pr.HeadRepo, pb.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
|
||||
Name: "repo-pr-update",
|
||||
|
||||
@@ -147,12 +147,18 @@ func TestRepushTag(t *testing.T) {
|
||||
// delete the tag
|
||||
_, _, err = gitcmd.NewCommand("push", "origin", "--delete", "v2.0").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
|
||||
assert.NoError(t, err)
|
||||
// query the release by API and it should be a draft
|
||||
|
||||
// query the release by API with no auth and it should be 404
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// query the release by API and it should be a draft
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var respRelease *api.Release
|
||||
DecodeJSON(t, resp, &respRelease)
|
||||
assert.True(t, respRelease.IsDraft)
|
||||
|
||||
// re-push the tag
|
||||
_, _, err = gitcmd.NewCommand("push", "origin", "--tags", "v2.0").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -122,7 +122,7 @@ RENDER_CONTENT_MODE = sanitized
|
||||
[markup.no-sanitizer]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .no-sanitizer
|
||||
RENDER_COMMAND = echo '<script>window.alert("hi")</script>'
|
||||
RENDER_COMMAND = go run build/test-echo.go
|
||||
; This test case is reused, at first it is used to test "no-sanitizer" (sandbox doesn't take effect here)
|
||||
; Then it will be updated and used to test "iframe + sandbox-disabled"
|
||||
RENDER_CONTENT_MODE = no-sanitizer
|
||||
|
||||
@@ -626,7 +626,6 @@ img.ui.avatar,
|
||||
font-family: var(--fonts-monospace);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: 3px 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,44 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* before the Vue component is mounted, show a loading indicator with dummy size */
|
||||
/* the ratio is guesswork, see https://github.com/razorness/vue3-calendar-heatmap/issues/26 */
|
||||
#user-heatmap.is-loading {
|
||||
aspect-ratio: 5.415; /* the size is about 790 x 145 */
|
||||
.activity-heatmap-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
.user.profile #user-heatmap.is-loading {
|
||||
aspect-ratio: 5.645; /* the size is about 953 x 169 */
|
||||
|
||||
@container (width > 0) {
|
||||
#user-heatmap {
|
||||
/* Set element to fixed height so that it does not resize after load. The calculation is complex
|
||||
because the element does not scale with a fixed aspect ratio. */
|
||||
height: calc((100cqw / 5) - (100cqw / 25) + 20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback height adjustment above for browsers that don't support container queries */
|
||||
@supports not (container-type: inline-size) {
|
||||
/* Before the Vue component is mounted, show a loading indicator with dummy size */
|
||||
/* The ratio is guesswork for legacy browsers, new browsers use the "@container" approach above */
|
||||
#user-heatmap.is-loading {
|
||||
aspect-ratio: 5.4823972051; /* the size is about 816 x 148.84 */
|
||||
}
|
||||
.user.profile #user-heatmap.is-loading {
|
||||
aspect-ratio: 5.6290608387; /* the size is about 953 x 169.3 */
|
||||
}
|
||||
}
|
||||
|
||||
#user-heatmap text {
|
||||
fill: currentcolor !important;
|
||||
}
|
||||
|
||||
/* root legend */
|
||||
#user-heatmap .vch__container > .vch__legend {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* for the "Less" and "More" legend */
|
||||
#user-heatmap .vch__legend .vch__legend {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
@@ -34,25 +55,3 @@
|
||||
#user-heatmap .vch__day__square:hover {
|
||||
outline: 1.5px solid var(--color-text);
|
||||
}
|
||||
|
||||
/* move the "? contributions in the last ? months" text from top to bottom */
|
||||
#user-heatmap .total-contributions {
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
#user-heatmap .total-contributions {
|
||||
left: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
#user-heatmap .total-contributions {
|
||||
font-size: 10px;
|
||||
left: 17px;
|
||||
bottom: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-8
@@ -387,6 +387,7 @@ td .commit-summary {
|
||||
|
||||
.repository.view.issue .pull-desc code {
|
||||
color: var(--color-primary);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.repository.view.issue .pull-desc a[data-clipboard-text] {
|
||||
@@ -527,9 +528,12 @@ td .commit-summary {
|
||||
}
|
||||
|
||||
.repository.view.issue .comment-list .timeline-item .comment-text-line {
|
||||
/* TODO: this "line-height" is not ideal (actually it is abused), many layouts depend on this magic value,
|
||||
for example: alignment of the header arrow and the avatar, view PR commit list left icon layout, dismiss review with reason, etc */
|
||||
line-height: 32px;
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-light);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.repository.view.issue .comment-list .timeline-item .comment-text-line .ui.label {
|
||||
@@ -600,9 +604,6 @@ td .commit-summary {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.repository.view.issue .comment-list .comment .content .form .button:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.repository.view.issue .comment-list .comment .merge-section {
|
||||
@@ -653,7 +654,7 @@ td .commit-summary {
|
||||
|
||||
.repository.view.issue .comment-list .code-comment {
|
||||
border: 1px solid transparent;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.repository.view.issue .comment-list .code-comment .comment-header {
|
||||
@@ -663,6 +664,7 @@ td .commit-summary {
|
||||
}
|
||||
|
||||
.repository.view.issue .comment-list .code-comment .comment-content {
|
||||
margin-top: 6px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
@@ -1285,9 +1287,9 @@ td .commit-summary {
|
||||
box-shadow: 0 0 0 3px var(--color-primary-alpha-30) !important;
|
||||
}
|
||||
|
||||
.comment:target .header::before {
|
||||
.comment:target .comment-header::before {
|
||||
border-right-color: var(--color-primary) !important;
|
||||
filter: drop-shadow(-3px 0 0 var(--color-primary-alpha-30)) !important;
|
||||
filter: drop-shadow(-4px 0 0 var(--color-primary-alpha-30)) !important;
|
||||
}
|
||||
|
||||
.code-comment:target,
|
||||
@@ -1307,7 +1309,6 @@ td .commit-summary {
|
||||
padding: 0.5em 1rem;
|
||||
position: relative;
|
||||
color: var(--color-text);
|
||||
min-height: 41px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -1315,6 +1316,10 @@ td .commit-summary {
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.comment-header.avatar-content-left-arrow {
|
||||
min-height: 41px; /* for a comment header with left arrow, the arrow is absolutely positioned, but the header content varies (for example: no "roles", etc), so it needs a min-height */
|
||||
}
|
||||
|
||||
.comment-header.avatar-content-left-arrow::after {
|
||||
border-right-color: var(--color-box-header);
|
||||
}
|
||||
@@ -1338,7 +1343,7 @@ td .commit-summary {
|
||||
.comment-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.comment-header-right {
|
||||
@@ -1346,6 +1351,10 @@ td .commit-summary {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.comment-header-right > .item.action {
|
||||
padding: 4px; /* add some padding to make click area larger for the "item action ... ui dropdown" items */
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
background: var(--color-box-body);
|
||||
border: none !important;
|
||||
|
||||
@@ -41,16 +41,16 @@
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.ui.dropdown.select-reaction .menu {
|
||||
min-width: 170px; /* item-outer-width * 4 */
|
||||
.ui.dropdown.select-reaction .menu.visible {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.ui.dropdown.select-reaction .menu > .item {
|
||||
float: left;
|
||||
margin: 4px;
|
||||
font-size: 20px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
font-size: 16px;
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
.comment-code-cloud {
|
||||
padding: 0.5rem 1rem !important;
|
||||
padding: 0.5rem !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,6 @@ function handleDayClick(e: Event & {date: Date}) {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="total-contributions">
|
||||
{{ locale.textTotalContributions }}
|
||||
</div>
|
||||
<calendar-heatmap
|
||||
:locale="locale.heatMapLocale"
|
||||
:no-data-text="locale.noDataText"
|
||||
@@ -65,5 +62,7 @@ function handleDayClick(e: Event & {date: Date}) {
|
||||
:range-color="colorRange"
|
||||
@day-click="handleDayClick($event)"
|
||||
:tippy-props="{theme: 'tooltip'}"
|
||||
/>
|
||||
>
|
||||
<template #vch__legend-left>{{ locale.textTotalContributions }}</template>
|
||||
</calendar-heatmap>
|
||||
</template>
|
||||
|
||||
@@ -489,7 +489,7 @@ export default defineComponent({
|
||||
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
|
||||
{{ locale.cancel }}
|
||||
</button>
|
||||
<button class="ui basic small compact button link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
|
||||
<button class="ui basic small compact button link-action tw-shrink-0" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
|
||||
{{ locale.rerun_all }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -520,7 +520,7 @@ export default defineComponent({
|
||||
<span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
|
||||
</div>
|
||||
<span class="job-brief-item-right">
|
||||
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
|
||||
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action interact-fg" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
|
||||
<span class="step-summary-duration">{{ job.duration }}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import './globals.ts';
|
||||
import '../fomantic/build/fomantic.js';
|
||||
import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE"
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors
|
||||
import './bootstrap.ts';
|
||||
|
||||
// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
|
||||
// so load globals (including jQuery) as early as possible
|
||||
import './globals.ts';
|
||||
|
||||
import './webcomponents/index.ts';
|
||||
import {onDomReady} from './utils/dom.ts';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user